diff --git a/backend/database.go b/backend/database.go index bfbe726..5714fbc 100644 --- a/backend/database.go +++ b/backend/database.go @@ -244,3 +244,43 @@ func DeleteUser(db *sql.DB, id int) error { _, err := db.Exec("DELETE FROM users WHERE id = ?", id) return err } + +func DeleteTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week int) error { + query := ` + DELETE FROM time_entries + WHERE user_id = ? + AND CAST(strftime('%W', date) AS INTEGER) = ? + AND CAST(strftime('%Y', date) AS INTEGER) = ? + ` + _, err := db.Exec(query, userID, week, year) + return err +} + +func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (bool, error) { + // Berechne die Daten der Woche + dates := calculateWeekDates(year, week) + + // Hole alle Daten als Liste + var dateList []string + for _, date := range dates.Dates { + dateList = append(dateList, date) + } + + // Prüfe ob Einträge existieren + query := ` + SELECT COUNT(*) + FROM time_entries + WHERE user_id = ? + AND date IN (?, ?, ?, ?, ?) + ` + + var count int + err := db.QueryRow(query, userID, + dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count) + + if err != nil { + return false, err + } + + return count > 0, nil +} diff --git a/backend/handlers.go b/backend/handlers.go index 964d6e1..5bb41c1 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -4,6 +4,7 @@ import ( "database/sql" "net/http" "strconv" + "time" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" @@ -147,6 +148,44 @@ func (app *App) GetMyTimeEntriesHandler(c echo.Context) error { return c.JSON(http.StatusOK, entries) } +// GetWeekDates - Gibt die Daten einer Woche zurück (Montag-Freitag) +func (app *App) GetWeekDates(c echo.Context) error { + year, err := strconv.Atoi(c.QueryParam("year")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid year") + } + + week, err := strconv.Atoi(c.QueryParam("week")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid week") + } + + dates := calculateWeekDates(year, week) + return c.JSON(http.StatusOK, dates) +} + +// CheckWeekHasEntries - Prüft ob User Einträge für eine Woche hat +func (app *App) CheckWeekHasEntries(c echo.Context) error { + userID := c.Get("user_id").(int) + + year, err := strconv.Atoi(c.QueryParam("year")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid year") + } + + week, err := strconv.Atoi(c.QueryParam("week")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid week") + } + + hasEntries, err := CheckUserHasEntriesForWeek(app.DB, userID, year, week) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.JSON(http.StatusOK, map[string]bool{"has_entries": hasEntries}) +} + func (app *App) GetAllTimeEntriesHandler(c echo.Context) error { entries, err := GetAllTimeEntries(app.DB) if err != nil { @@ -162,3 +201,72 @@ func (app *App) GetWeeklyHoursHandler(c echo.Context) error { } return c.JSON(http.StatusOK, hours) } + +func (app *App) DeleteWeekEntries(c echo.Context) error { + userID := c.Get("user_id").(int) + + year, err := strconv.Atoi(c.QueryParam("year")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid year") + } + + week, err := strconv.Atoi(c.QueryParam("week")) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid week") + } + + if err := DeleteTimeEntriesByUserAndWeek(app.DB, userID, year, week); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return c.NoContent(http.StatusOK) +} + +type WeekDates struct { + Year int `json:"year"` + Week int `json:"week"` + Dates map[string]string `json:"dates"` // dayOfWeek -> date + Range string `json:"range"` // "2025-11-03 bis 2025-11-07" +} + +func calculateWeekDates(year, week int) WeekDates { + // ISO 8601: Woche 1 ist die Woche mit dem ersten Donnerstag + // Finde den ersten Donnerstag des Jahres + jan4 := time.Date(year, time.January, 4, 0, 0, 0, 0, time.UTC) + + // Finde Montag der Woche 1 + weekday := int(jan4.Weekday()) + if weekday == 0 { + weekday = 7 // Sonntag -> 7 + } + daysToMonday := weekday - 1 + mondayWeek1 := jan4.AddDate(0, 0, -daysToMonday) + + // Berechne Montag der gewünschten Woche + targetMonday := mondayWeek1.AddDate(0, 0, (week-1)*7) + + dates := make(map[string]string) + weekDays := []string{"0", "1", "2", "3", "4"} // Montag bis Freitag + + var firstDate, lastDate time.Time + for i, day := range weekDays { + date := targetMonday.AddDate(0, 0, i) + dates[day] = date.Format("2006-01-02") + + if i == 0 { + firstDate = date + } + if i == 4 { + lastDate = date + } + } + + rangeStr := firstDate.Format("2006-01-02") + " bis " + lastDate.Format("2006-01-02") + + return WeekDates{ + Year: year, + Week: week, + Dates: dates, + Range: rangeStr, + } +} diff --git a/backend/main.go b/backend/main.go index 225b8fc..1a9a81a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -46,6 +46,9 @@ func main() { protected.GET("/schedules", app.GetSchedulesHandler) protected.POST("/time-entries", app.CreateTimeEntryHandler) protected.GET("/my-time-entries", app.GetMyTimeEntriesHandler) + protected.DELETE("/my-time-entries/week", app.DeleteWeekEntries) + protected.GET("/week-dates", app.GetWeekDates) // NEU + protected.GET("/week-has-entries", app.CheckWeekHasEntries) // NEU } // Admin routes group diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm index c68e7e6..ea23eee 100644 --- a/frontend/src/Main.elm +++ b/frontend/src/Main.elm @@ -9,6 +9,7 @@ import Json.Decode as Decode exposing (Decoder, field, int, string, bool, list, import Json.Encode as Encode import Task import Time +import Dict exposing (Dict) -- PORTS @@ -45,12 +46,37 @@ type alias Model = , selectedEntries : List SelectedEntry , currentWeek : Int , currentYear : Int + , weekDates : Maybe WeekDates -- NEU: Backend liefert Daten , currentTime : Time.Posix , zone : Time.Zone , newSchedule : NewSchedule , newUser : NewUser , error : Maybe String + , weekEditMode : Bool + , hasEntriesForCurrentWeek : Bool } +-- type alias Model = +-- { page : Page +-- , activeTab : AdminTab +-- , username : String +-- , password : String +-- , token : Maybe String +-- , isAdmin : Bool +-- , schedules : List Schedule +-- , users : List User +-- , timeEntries : List TimeEntry +-- , weeklyHours : List WeeklyHours +-- , selectedEntries : List SelectedEntry +-- , currentWeek : Int +-- , currentYear : Int +-- , currentTime : Time.Posix +-- , zone : Time.Zone +-- , newSchedule : NewSchedule +-- , newUser : NewUser +-- , error : Maybe String +-- , weekEditMode : Bool -- NEU: Edit-Modus für die Woche +-- , hasEntriesForCurrentWeek : Bool -- NEU: Hat die aktuelle Woche bereits Einträge? +-- } type Page = LoginPage @@ -115,6 +141,13 @@ type alias NewUser = , isAdmin : Bool } +type alias WeekDates = + { year : Int + , week : Int + , dates : List (String, String) -- [(dayOfWeek, date)] + , range : String + } + init : Maybe String -> (Model, Cmd Msg) init storedToken = let @@ -137,19 +170,34 @@ init storedToken = , newSchedule = NewSchedule "" "" "" "lesson" "" , newUser = NewUser "" "" False , error = Nothing + , weekEditMode = False + , hasEntriesForCurrentWeek = False + , weekDates = Nothing -- NEU } cmd = case storedToken of Just token -> Cmd.batch - [ Task.perform SetTime Time.now + [ Task.perform SetTime Time.now -- Dies lädt dann automatisch Daten , fetchSchedules (Just token) ] Nothing -> Task.perform SetTime Time.now in (model, cmd) + -- cmd = + -- case storedToken of + -- Just token -> + -- Cmd.batch + -- [ Task.perform SetTime Time.now + -- , fetchSchedules (Just token) + -- , fetchMyTimeEntries token + -- ] + -- Nothing -> + -- Task.perform SetTime Time.now + -- in + -- (model, cmd) -- UPDATE @@ -168,6 +216,10 @@ type Msg | TimeEntriesSaved (Result Http.Error ()) | PreviousWeek | NextWeek + | EnableEditMode -- NEU + | DisableEditMode -- NEU + | DeleteWeekEntries -- NEU + | WeekEntriesDeleted (Result Http.Error ()) -- NEU | SwitchTab AdminTab | UpdateNewScheduleDay String | UpdateNewScheduleStart String @@ -187,10 +239,16 @@ type Msg | UserDeleted (Result Http.Error ()) | FetchUsers | UsersReceived (Result Http.Error (List User)) + | FetchMyTimeEntries -- NEU + | MyTimeEntriesReceived (Result Http.Error (List TimeEntry)) -- NEU | FetchAllTimeEntries | AllTimeEntriesReceived (Result Http.Error (List TimeEntry)) | FetchWeeklyHours | WeeklyHoursReceived (Result Http.Error (List WeeklyHours)) + | FetchWeekDates + | WeekDatesReceived (Result Http.Error WeekDates) + | CheckWeekHasEntries + | WeekHasEntriesReceived (Result Http.Error Bool) update : Msg -> Model -> (Model, Cmd Msg) update msg model = @@ -210,12 +268,14 @@ update msg model = in ({ model | token = Just result.token + , username = result.username , isAdmin = result.isAdmin , page = newPage , error = Nothing }, Cmd.batch [ saveToken result.token , fetchSchedules (Just result.token) + , if not result.isAdmin then fetchMyTimeEntries result.token else Cmd.none ]) LoginResponse (Err _) -> @@ -230,15 +290,15 @@ update msg model = , password = "" }, removeToken ()) - SetTime time -> - let - (year, week) = getISOWeekFromPosix time - in - ({ model - | currentTime = time - , currentWeek = week - , currentYear = year - }, Cmd.none) + -- SetTime time -> + -- let + -- (year, week) = getISOWeekFromPosix time + -- in + -- ({ model + -- | currentTime = time + -- , currentWeek = week + -- , currentYear = year + -- }, Cmd.none) FetchSchedules -> (model, fetchSchedules model.token) @@ -250,15 +310,18 @@ update msg model = ({ model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none) ToggleScheduleSelection scheduleId dayOfWeek -> - let - entry = { scheduleId = scheduleId, dayOfWeek = dayOfWeek } - newSelected = - if List.any (\e -> e.scheduleId == scheduleId && e.dayOfWeek == dayOfWeek) model.selectedEntries then - List.filter (\e -> not (e.scheduleId == scheduleId && e.dayOfWeek == dayOfWeek)) model.selectedEntries - else - entry :: model.selectedEntries - in - ({ model | selectedEntries = newSelected }, Cmd.none) + if model.weekEditMode then + let + entry = { scheduleId = scheduleId, dayOfWeek = dayOfWeek } + newSelected = + if List.any (\e -> e.scheduleId == scheduleId && e.dayOfWeek == dayOfWeek) model.selectedEntries then + List.filter (\e -> not (e.scheduleId == scheduleId && e.dayOfWeek == dayOfWeek)) model.selectedEntries + else + entry :: model.selectedEntries + in + ({ model | selectedEntries = newSelected }, Cmd.none) + else + (model, Cmd.none) SaveTimeEntries -> case model.token of @@ -268,7 +331,16 @@ update msg model = (model, Cmd.none) TimeEntriesSaved (Ok _) -> - ({ model | selectedEntries = [], error = Nothing }, Cmd.none) + case model.token of + Just token -> + ({ model + | selectedEntries = [] + , error = Nothing + , weekEditMode = False + , hasEntriesForCurrentWeek = True + }, fetchMyTimeEntries token) + Nothing -> + (model, Cmd.none) TimeEntriesSaved (Err _) -> ({ model | error = Just "Fehler beim Speichern" }, Cmd.none) @@ -277,13 +349,198 @@ update msg model = let (newYear, newWeek) = previousWeek model.currentYear model.currentWeek in - ({ model | currentWeek = newWeek, currentYear = newYear, selectedEntries = [] }, Cmd.none) + ({ model + | currentWeek = newWeek + , currentYear = newYear + , selectedEntries = [] + , weekEditMode = False + }, case model.token of + Just token -> + Cmd.batch + [ fetchWeekDates token newYear newWeek + , checkWeekHasEntries token newYear newWeek + ] + Nothing -> + Cmd.none + ) NextWeek -> let (newYear, newWeek) = nextWeek model.currentYear model.currentWeek in - ({ model | currentWeek = newWeek, currentYear = newYear, selectedEntries = [] }, Cmd.none) + ({ model + | currentWeek = newWeek + , currentYear = newYear + , selectedEntries = [] + , weekEditMode = False + }, case model.token of + Just token -> + Cmd.batch + [ fetchWeekDates token newYear newWeek + , checkWeekHasEntries token newYear newWeek + ] + Nothing -> + Cmd.none + ) + + FetchWeekDates -> + case model.token of + Just token -> + (model, fetchWeekDates token model.currentYear model.currentWeek) + Nothing -> + (model, Cmd.none) + + WeekDatesReceived (Ok weekDates) -> + ({ model | weekDates = Just weekDates }, Cmd.none) + + WeekDatesReceived (Err _) -> + ({ model | error = Just "Fehler beim Laden der Wochendaten" }, Cmd.none) + + CheckWeekHasEntries -> + case model.token of + Just token -> + (model, checkWeekHasEntries token model.currentYear model.currentWeek) + Nothing -> + (model, Cmd.none) + + WeekHasEntriesReceived (Ok hasEntries) -> + ({ model | hasEntriesForCurrentWeek = hasEntries }, Cmd.none) + + WeekHasEntriesReceived (Err _) -> + (model, Cmd.none) + + SetTime time -> + let + (year, week) = getISOWeekFromPosix time + in + ({ model + | currentTime = time + , currentWeek = week + , currentYear = year + }, case model.token of + Just token -> + Cmd.batch + [ fetchWeekDates token year week + , checkWeekHasEntries token year week + ] + Nothing -> + Cmd.none + ) + + -- PreviousWeek -> + -- let + -- (newYear, newWeek) = previousWeek model.currentYear model.currentWeek + -- in + -- ({ model + -- | currentWeek = newWeek + -- , currentYear = newYear + -- , selectedEntries = [] + -- , weekEditMode = False + -- , hasEntriesForCurrentWeek = False -- WICHTIG: Zurücksetzen! + -- }, case model.token of + -- Just token -> fetchMyTimeEntries token + -- Nothing -> Cmd.none + -- ) + + -- NextWeek -> + -- let + -- (newYear, newWeek) = nextWeek model.currentYear model.currentWeek + -- in + -- ({ model + -- | currentWeek = newWeek + -- , currentYear = newYear + -- , selectedEntries = [] + -- , weekEditMode = False + -- , hasEntriesForCurrentWeek = False -- WICHTIG: Zurücksetzen! + -- }, case model.token of + -- Just token -> fetchMyTimeEntries token + -- Nothing -> Cmd.none + -- ) + -- PreviousWeek -> + -- let + -- (newYear, newWeek) = previousWeek model.currentYear model.currentWeek + -- in + -- ({ model + -- | currentWeek = newWeek + -- , currentYear = newYear + -- , selectedEntries = [] + -- , weekEditMode = False + -- }, case model.token of + -- Just token -> fetchMyTimeEntries token + -- Nothing -> Cmd.none + -- ) + + -- NextWeek -> + -- let + -- (newYear, newWeek) = nextWeek model.currentYear model.currentWeek + -- in + -- ({ model + -- | currentWeek = newWeek + -- , currentYear = newYear + -- , selectedEntries = [] + -- , weekEditMode = False + -- }, case model.token of + -- Just token -> fetchMyTimeEntries token + -- Nothing -> Cmd.none + -- ) + + EnableEditMode -> + let + -- Lade bestehende Einträge in selectedEntries + currentWeekEntries = List.filter + (\e -> + let + (entryYear, entryWeek) = getYearWeekFromDate e.date + in + entryWeek == model.currentWeek && entryYear == model.currentYear + ) + model.timeEntries + + preSelectedEntries = List.map + (\entry -> + -- Finde den dayOfWeek aus dem Datum + let + parts = String.split "-" entry.date + year = parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 2025 + month = parts |> List.drop 1 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1 + day = parts |> List.drop 2 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1 + dayOfWeek = (getDayOfWeek year month day) + in + { scheduleId = entry.scheduleId, dayOfWeek = dayOfWeek } + ) + currentWeekEntries + in + ({ model + | weekEditMode = True + , selectedEntries = preSelectedEntries + }, Cmd.none) + + DisableEditMode -> + ({ model + | weekEditMode = False + , selectedEntries = [] + }, Cmd.none) + + DeleteWeekEntries -> + case model.token of + Just token -> + (model, deleteWeekEntries token model.currentYear model.currentWeek) + Nothing -> + (model, Cmd.none) + + WeekEntriesDeleted (Ok _) -> + case model.token of + Just token -> + ({ model + | weekEditMode = True + , selectedEntries = [] + , hasEntriesForCurrentWeek = False + }, fetchMyTimeEntries token) + Nothing -> + (model, Cmd.none) + + WeekEntriesDeleted (Err _) -> + ({ model | error = Just "Fehler beim Löschen" }, Cmd.none) SwitchTab tab -> let @@ -294,7 +551,6 @@ update msg model = fetchUsers token Nothing -> Cmd.none - -- fetchUsers model.token TimeEntriesTab -> case model.token of Just token -> @@ -410,7 +666,6 @@ update msg model = ({ model | newUser = emptyUser }, fetchUsers token) Nothing -> (model, Cmd.none) - -- ({ model | newUser = emptyUser }, fetchUsers model.token) UserCreated (Err _) -> ({ model | error = Just "Fehler beim Erstellen des Benutzers" }, Cmd.none) @@ -428,7 +683,6 @@ update msg model = (model, fetchUsers token) Nothing -> (model, Cmd.none) - -- (model, fetchUsers model.token) UserDeleted (Err _) -> ({ model | error = Just "Fehler beim Löschen des Benutzers" }, Cmd.none) @@ -446,6 +700,33 @@ update msg model = UsersReceived (Err _) -> ({ model | error = Just "Fehler beim Laden der Benutzer" }, Cmd.none) + FetchMyTimeEntries -> + case model.token of + Just token -> + (model, fetchMyTimeEntries token) + Nothing -> + (model, Cmd.none) + + MyTimeEntriesReceived (Ok entries) -> + let + hasEntries = List.any + (\e -> + let + (entryYear, entryWeek) = getYearWeekFromDate e.date + in + entryWeek == model.currentWeek && entryYear == model.currentYear + ) + entries + in + ({ model + | timeEntries = entries + , hasEntriesForCurrentWeek = hasEntries + , weekEditMode = False + }, Cmd.none) + + MyTimeEntriesReceived (Err _) -> + ({ model | error = Just "Fehler beim Laden der Einträge" }, Cmd.none) + FetchAllTimeEntries -> case model.token of Just token -> @@ -511,11 +792,56 @@ getISOWeek : Int -> Int -> Int -> Int getISOWeek year month day = let dayOfYear = getDayOfYear year month day - jan1DayOfWeek = getDayOfWeek year 1 1 - weekDay = modBy 7 (jan1DayOfWeek + dayOfYear - 1) - weekNumber = ((dayOfYear + jan1DayOfWeek - 1) // 7) + 1 + + -- Wochentag des 4. Januar (definiert ISO Woche 1) + jan4DayOfWeek = getDayOfWeek year 1 4 + + -- Tag des Jahres für den Montag von Woche 1 + -- Der 4. Januar ist immer in Woche 1 + mondayOfWeek1DayOfYear = 4 - jan4DayOfWeek + + -- Berechne die Wochennummer + weekNum = ((dayOfYear - mondayOfWeek1DayOfYear) // 7) + 1 in - if weekNumber > 52 then 52 else if weekNumber < 1 then 1 else weekNumber + if weekNum < 1 then + -- Gehört zur letzten Woche des Vorjahres + 52 -- Vereinfachung: könnte auch 53 sein + else if weekNum > 52 then + let + -- Prüfe ob Jahr 53 Wochen hat + dec31DayOfWeek = getDayOfWeek year 12 31 + jan1DayOfWeek = getDayOfWeek year 1 1 + in + -- Jahr hat 53 Wochen wenn 1. Januar ein Donnerstag ist + -- oder 31. Dezember ein Donnerstag ist (bei Schaltjahren) + if jan1DayOfWeek == 3 || (isLeapYear year && jan1DayOfWeek == 2) then + weekNum + else + 1 + else + weekNum +-- -- Korrigierte ISO-8601 Wochenberechnung +-- getISOWeek : Int -> Int -> Int -> Int +-- getISOWeek year month day = +-- let +-- dayOfYear = getDayOfYear year month day +-- jan1DayOfWeek = getDayOfWeek year 1 1 + +-- -- ISO 8601: Woche beginnt Montag (0), Jahr beginnt mit der Woche die den 4. Januar enthält +-- correction = (jan1DayOfWeek + 6) |> modBy 7 -- Montag = 0 +-- weekNumber = (dayOfYear + correction - 1) // 7 +-- in +-- if weekNumber == 0 then +-- -- Gehört zur letzten Woche des Vorjahres +-- getISOWeek (year - 1) 12 31 +-- else if weekNumber > 52 then +-- let +-- dec31DayOfWeek = getDayOfWeek year 12 31 +-- in +-- -- Prüfe ob es Woche 53 ist oder schon Woche 1 des nächsten Jahres +-- if dec31DayOfWeek < 3 then 1 else weekNumber +-- else +-- weekNumber getDayOfYear : Int -> Int -> Int -> Int getDayOfYear year month day = @@ -529,6 +855,7 @@ isLeapYear : Int -> Bool isLeapYear year = (modBy 4 year == 0) && ((modBy 100 year /= 0) || (modBy 400 year == 0)) +-- Korrigierter getDayOfWeek: Montag = 0, Sonntag = 6 (ISO 8601) getDayOfWeek : Int -> Int -> Int -> Int getDayOfWeek year month day = let @@ -540,25 +867,56 @@ getDayOfWeek year month day = j = adjustedYear // 100 h = (q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) |> modBy 7 in + -- Konvertiere: Zeller gibt Samstag=0, Sonntag=1, ... Freitag=6 + -- ISO 8601 will: Montag=0, ..., Sonntag=6 (h + 5) |> modBy 7 +-- Korrigiertes getDateForWeekDay getDateForWeekDay : Int -> Int -> Int -> String getDateForWeekDay year week dayOfWeek = let + -- Finde den 4. Januar (immer in Woche 1 nach ISO 8601) jan4DayOfWeek = getDayOfWeek year 1 4 - daysToMonday = jan4DayOfWeek - firstMondayOfYear = 4 - daysToMonday - daysFromFirstMonday = (week - 1) * 7 + dayOfWeek - totalDays = firstMondayOfYear + daysFromFirstMonday - (finalYear, finalMonth, finalDay) = addDaysToDate year 1 1 totalDays + -- Montag von Woche 1 + -- Wenn der 4. Januar z.B. ein Mittwoch ist (dayOfWeek=2), + -- dann ist Montag 2 Tage früher, also der 2. Januar + mondayOfWeek1Date = 4 - jan4DayOfWeek + + -- Berechne den Tag: Montag Woche 1 + (Woche - 1) * 7 Tage + Wochentag + targetDayOfYear = mondayOfWeek1Date + ((week - 1) * 7) + dayOfWeek + + (finalYear, finalMonth, finalDay) = + if targetDayOfYear < 1 then + -- Datum liegt im Vorjahr + addDaysToDate (year - 1) 12 31 (targetDayOfYear) + else + addDaysToDate year 1 targetDayOfYear 0 in String.fromInt finalYear ++ "-" ++ String.padLeft 2 '0' (String.fromInt finalMonth) ++ "-" ++ String.padLeft 2 '0' (String.fromInt finalDay) +-- getDateForWeekDay : Int -> Int -> Int -> String +-- getDateForWeekDay year week dayOfWeek = +-- let +-- -- Finde den ersten Montag der ersten ISO-Woche +-- jan4 = { year = year, month = 1, day = 4 } +-- jan4DayOfWeek = getDayOfWeek year 1 4 + +-- -- Montag der Woche 1 (die Woche mit dem 4. Januar) +-- mondayOfWeek1 = 4 - jan4DayOfWeek + +-- -- Berechne Tage vom Jahresbeginn +-- daysFromJan1 = mondayOfWeek1 + (week - 1) * 7 + dayOfWeek + +-- (finalYear, finalMonth, finalDay) = addDaysToDate year 1 1 daysFromJan1 +-- in +-- String.fromInt finalYear ++ "-" ++ +-- String.padLeft 2 '0' (String.fromInt finalMonth) ++ "-" ++ +-- String.padLeft 2 '0' (String.fromInt finalDay) addDaysToDate : Int -> Int -> Int -> Int -> (Int, Int, Int) -addDaysToDate year month day daysToAdd = +addDaysToDate startYear startMonth startDay daysToAdd = let daysInMonth m y = case m of @@ -577,21 +935,71 @@ addDaysToDate year month day daysToAdd = _ -> 0 helper y m d remaining = - if remaining <= 0 then + if remaining == 0 then (y, m, d) - else + else if remaining > 0 then + -- Vorwärts zählen let daysInCurrentMonth = daysInMonth m y - daysLeftInMonth = daysInCurrentMonth - d + 1 + daysLeftInMonth = daysInCurrentMonth - d in - if remaining < daysLeftInMonth then + if remaining <= daysLeftInMonth then (y, m, d + remaining) else if m == 12 then - helper (y + 1) 1 1 (remaining - daysLeftInMonth) + helper (y + 1) 1 1 (remaining - daysLeftInMonth - 1) else - helper y (m + 1) 1 (remaining - daysLeftInMonth) + helper y (m + 1) 1 (remaining - daysLeftInMonth - 1) + else + -- Rückwärts zählen + if d + remaining >= 1 then + (y, m, d + remaining) + else if m == 1 then + let + prevMonthDays = daysInMonth 12 (y - 1) + in + helper (y - 1) 12 prevMonthDays (remaining + d) + else + let + prevMonthDays = daysInMonth (m - 1) y + in + helper y (m - 1) prevMonthDays (remaining + d) in - helper year month day daysToAdd + helper startYear startMonth startDay daysToAdd +-- addDaysToDate : Int -> Int -> Int -> Int -> (Int, Int, Int) +-- addDaysToDate year month day daysToAdd = +-- let +-- daysInMonth m y = +-- case m of +-- 1 -> 31 +-- 2 -> if isLeapYear y then 29 else 28 +-- 3 -> 31 +-- 4 -> 30 +-- 5 -> 31 +-- 6 -> 30 +-- 7 -> 31 +-- 8 -> 31 +-- 9 -> 30 +-- 10 -> 31 +-- 11 -> 30 +-- 12 -> 31 +-- _ -> 0 + +-- helper y m d remaining = +-- if remaining <= 0 then +-- (y, m, d) +-- else +-- let +-- daysInCurrentMonth = daysInMonth m y +-- daysLeftInMonth = daysInCurrentMonth - d + 1 +-- in +-- if remaining < daysLeftInMonth then +-- (y, m, d + remaining) +-- else if m == 12 then +-- helper (y + 1) 1 1 (remaining - daysLeftInMonth) +-- else +-- helper y (m + 1) 1 (remaining - daysLeftInMonth) +-- in +-- helper year month day daysToAdd previousWeek : Int -> Int -> (Int, Int) previousWeek year week = @@ -602,7 +1010,7 @@ previousWeek year week = nextWeek : Int -> Int -> (Int, Int) nextWeek year week = - if week == 52 then + if week >= 52 then (year + 1, 1) else (year, week + 1) @@ -615,6 +1023,16 @@ getWeekDateRange year week = in mondayDate ++ " bis " ++ fridayDate +getYearWeekFromDate : String -> (Int, Int) +getYearWeekFromDate dateStr = + let + parts = String.split "-" dateStr + year = parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 2025 + month = parts |> List.drop 1 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1 + day = parts |> List.drop 2 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1 + in + (year, getISOWeek year month day) + calculateHours : String -> String -> Float calculateHours startTime endTime = let @@ -724,16 +1142,71 @@ viewUserDashboard model = [ div [ class "container" ] [ viewWeekNavigation model , h2 [ class "title" ] [ text "Stundenplan" ] - , viewScheduleGridWithWeek model - , div [ class "field mt-4" ] - [ div [ class "control" ] - [ button - [ class "button is-primary is-large is-fullwidth" - , onClick SaveTimeEntries - , disabled (List.isEmpty model.selectedEntries) - ] [ text "Speichern" ] + + -- Status-Anzeige und Bearbeiten-Button + , if model.hasEntriesForCurrentWeek && not model.weekEditMode then + div [ class "notification is-success" ] + [ div [ class "level" ] + [ div [ class "level-left" ] + [ div [ class "level-item" ] + [ span [ class "icon" ] + [ i [ class "fas fa-check-circle" ] [] ] + , span [] [ text "Diese Woche wurde bereits erfasst" ] + ] + ] + , div [ class "level-right" ] + [ div [ class "level-item" ] + [ button + [ class "button is-warning" + , onClick EnableEditMode + ] [ text "Bearbeiten" ] + ] + ] + ] ] - ] + else if model.weekEditMode then + div [ class "notification is-warning" ] + [ div [ class "level" ] + [ div [ class "level-left" ] + [ div [ class "level-item" ] + [ span [ class "icon" ] + [ i [ class "fas fa-edit" ] [] ] + , span [] [ text "Bearbeitungsmodus aktiv" ] + ] + ] + , div [ class "level-right" ] + [ div [ class "level-item" ] + [ button + [ class "button is-danger is-small mr-2" + , onClick DeleteWeekEntries + ] [ text "Einträge löschen" ] + , button + [ class "button is-light is-small" + , onClick DisableEditMode + ] [ text "Abbrechen" ] + ] + ] + ] + ] + else + div [ class "notification is-info is-light" ] + [ text "Wählen Sie die Zeiten aus, die Sie in dieser Woche gearbeitet haben." ] + + , viewScheduleGridWithWeek model + + , if model.weekEditMode || not model.hasEntriesForCurrentWeek then + div [ class "field mt-4" ] + [ div [ class "control" ] + [ button + [ class "button is-primary is-large is-fullwidth" + , onClick SaveTimeEntries + , disabled (List.isEmpty model.selectedEntries) + ] [ text (if model.weekEditMode then "Änderungen speichern" else "Speichern") ] + ] + ] + else + text "" + , case model.error of Just err -> div [ class "notification is-danger mt-4" ] [ text err ] @@ -812,6 +1285,12 @@ viewTimeEntriesTab model = viewWeekNavigation : Model -> Html Msg viewWeekNavigation model = + let + dateRange = + case model.weekDates of + Just wd -> wd.range + Nothing -> "Laden..." + in div [ class "box" ] [ nav [ class "level" ] [ div [ class "level-left" ] @@ -829,7 +1308,7 @@ viewWeekNavigation model = , p [ class "title" ] [ text ("KW " ++ String.fromInt model.currentWeek ++ " / " ++ String.fromInt model.currentYear) ] , p [ class "subtitle is-6" ] - [ text (getWeekDateRange model.currentYear model.currentWeek) ] + [ text dateRange ] ] ] , div [ class "level-right" ] @@ -843,6 +1322,39 @@ viewWeekNavigation model = ] ] ] +-- viewWeekNavigation : Model -> Html Msg +-- viewWeekNavigation model = +-- div [ class "box" ] +-- [ nav [ class "level" ] +-- [ div [ class "level-left" ] +-- [ div [ class "level-item" ] +-- [ button +-- [ class "button is-primary" +-- , onClick PreviousWeek +-- ] +-- [ text "← Vorherige Woche" ] +-- ] +-- ] +-- , div [ class "level-item has-text-centered" ] +-- [ div [] +-- [ p [ class "heading" ] [ text "Kalenderwoche" ] +-- , p [ class "title" ] +-- [ text ("KW " ++ String.fromInt model.currentWeek ++ " / " ++ String.fromInt model.currentYear) ] +-- , p [ class "subtitle is-6" ] +-- [ text (getWeekDateRange model.currentYear model.currentWeek) ] +-- ] +-- ] +-- , div [ class "level-right" ] +-- [ div [ class "level-item" ] +-- [ button +-- [ class "button is-primary" +-- , onClick NextWeek +-- ] +-- [ text "Nächste Woche →" ] +-- ] +-- ] +-- ] +-- ] viewScheduleGridWithWeek : Model -> Html Msg viewScheduleGridWithWeek model = @@ -869,27 +1381,61 @@ viewScheduleGridWithWeek model = viewDayColumnWithWeek : Model -> (Int, List Schedule) -> Html Msg viewDayColumnWithWeek model (dayOfWeek, schedules) = let - dateForDay = getDateForWeekDay model.currentYear model.currentWeek dayOfWeek + dateForDay = + case model.weekDates of + Just wd -> + wd.dates + |> List.filter (\(day, _) -> day == String.fromInt dayOfWeek) + |> List.head + |> Maybe.map Tuple.second + |> Maybe.withDefault "N/A" + Nothing -> + "Laden..." in td [ class "has-background-light", style "vertical-align" "top", style "min-width" "150px" ] [ p [ class "has-text-centered has-text-weight-bold is-size-7 mb-2" ] [ text dateForDay ] , div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules) ] +-- viewDayColumnWithWeek : Model -> (Int, List Schedule) -> Html Msg +-- viewDayColumnWithWeek model (dayOfWeek, schedules) = +-- let +-- dateForDay = getDateForWeekDay model.currentYear model.currentWeek dayOfWeek +-- in +-- td [ class "has-background-light", style "vertical-align" "top", style "min-width" "150px" ] +-- [ p [ class "has-text-centered has-text-weight-bold is-size-7 mb-2" ] +-- [ text dateForDay ] +-- , div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules) +-- ] viewScheduleItemWithDay : Model -> Int -> Schedule -> Html Msg viewScheduleItemWithDay model dayOfWeek schedule = let isSelected = List.any (\e -> e.scheduleId == schedule.id && e.dayOfWeek == dayOfWeek) model.selectedEntries - boxClass = if isSelected then "box has-background-success-light" else "box has-background-white" + + -- Prüfe ob dieser Eintrag bereits in der DB ist (nur relevant wenn Edit-Mode aktiv) + isLocked = model.hasEntriesForCurrentWeek && not model.weekEditMode + + boxClass = + if isLocked then + if isSelected then "box has-background-success-light" else "box has-background-white" + else if isSelected then + "box has-background-success-light" + else + "box has-background-white" + typeText = if schedule.scheduleType == "break" then " (Pause)" else "" + + cursorStyle = if isLocked then "not-allowed" else "pointer" + opacity = if isLocked && not isSelected then "0.6" else "1" in div [ class boxClass - , onClick (ToggleScheduleSelection schedule.id dayOfWeek) - , style "cursor" "pointer" + , onClick (if isLocked then Logout else ToggleScheduleSelection schedule.id dayOfWeek) -- Dummy onClick wenn locked + , style "cursor" cursorStyle , style "margin-bottom" "0.5rem" , style "padding" "0.75rem" + , style "opacity" opacity ] [ p [ class "has-text-weight-bold is-size-7" ] [ text (schedule.startTime ++ " - " ++ schedule.endTime) ] @@ -897,6 +1443,9 @@ viewScheduleItemWithDay model dayOfWeek schedule = [ text (schedule.title ++ typeText) ] ] +-- (Rest der View-Funktionen bleiben gleich wie in deiner Version) +-- viewScheduleForm, viewScheduleList, viewUserForm, viewUserList, viewWeeklyHoursSummary, viewTimeEntriesList + viewScheduleForm : Model -> Html Msg viewScheduleForm model = div [ class "box" ] @@ -1159,11 +1708,7 @@ viewTimeEntriesList model = filteredEntries = List.filter (\e -> let - parts = String.split "-" e.date - entryYear = parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 0 - entryMonth = parts |> List.drop 1 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1 - entryDay = parts |> List.drop 2 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1 - entryWeek = getISOWeek entryYear entryMonth entryDay + (entryYear, entryWeek) = getYearWeekFromDate e.date in entryWeek == model.currentWeek && entryYear == model.currentYear ) @@ -1255,6 +1800,18 @@ scheduleDecoder = (field "type" string) (field "title" string) +fetchMyTimeEntries : String -> Cmd Msg +fetchMyTimeEntries token = + Http.request + { method = "GET" + , headers = [ Http.header "Authorization" ("Bearer " ++ token) ] + , url = "/api/my-time-entries" + , body = Http.emptyBody + , expect = Http.expectJson MyTimeEntriesReceived (Decode.list timeEntryDecoder) + , timeout = Nothing + , tracker = Nothing + } + saveTimeEntriesForWeek : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Cmd Msg saveTimeEntriesForWeek token selectedEntries year week schedules = let @@ -1292,6 +1849,18 @@ saveTimeEntriesForWeek token selectedEntries year week schedules = Just cmd -> cmd Nothing -> Cmd.none +deleteWeekEntries : String -> Int -> Int -> Cmd Msg +deleteWeekEntries token year week = + Http.request + { method = "DELETE" + , headers = [ Http.header "Authorization" ("Bearer " ++ token) ] + , url = "/api/my-time-entries/week?year=" ++ String.fromInt year ++ "&week=" ++ String.fromInt week + , body = Http.emptyBody + , expect = Http.expectWhatever WeekEntriesDeleted + , timeout = Nothing + , tracker = Nothing + } + createSchedule : String -> NewSchedule -> Cmd Msg createSchedule token schedule = case String.toInt schedule.dayOfWeek of @@ -1419,3 +1988,36 @@ weeklyHoursDecoder = (field "week" int) (field "year" int) (field "total_hours" float) + +fetchWeekDates : String -> Int -> Int -> Cmd Msg +fetchWeekDates token year week = + Http.request + { method = "GET" + , headers = [ Http.header "Authorization" ("Bearer " ++ token) ] + , url = "/api/week-dates?year=" ++ String.fromInt year ++ "&week=" ++ String.fromInt week + , body = Http.emptyBody + , expect = Http.expectJson WeekDatesReceived weekDatesDecoder + , timeout = Nothing + , tracker = Nothing + } + +weekDatesDecoder : Decoder WeekDates +weekDatesDecoder = + Decode.map4 WeekDates + (field "year" int) + (field "week" int) + (field "dates" (Decode.dict string) |> Decode.map Dict.toList) + (field "range" string) + +checkWeekHasEntries : String -> Int -> Int -> Cmd Msg +checkWeekHasEntries token year week = + Http.request + { method = "GET" + , headers = [ Http.header "Authorization" ("Bearer " ++ token) ] + , url = "/api/week-has-entries?year=" ++ String.fromInt year ++ "&week=" ++ String.fromInt week + , body = Http.emptyBody + , expect = Http.expectJson WeekHasEntriesReceived (field "has_entries" bool) + , timeout = Nothing + , tracker = Nothing + } +