From 20ba24001a039e34589dfd44dcec0caea684a022 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Wed, 5 Nov 2025 10:19:36 +0100 Subject: [PATCH] fix: fix reset of entries when switching between weeks --- backend/database.go | 86 +++++---- backend/handlers.go | 58 ++++-- backend/main.go | 16 +- backend/middleware.go | 13 +- frontend/src/Main.elm | 408 +++++++++--------------------------------- 5 files changed, 184 insertions(+), 397 deletions(-) diff --git a/backend/database.go b/backend/database.go index 5714fbc..8caacca 100644 --- a/backend/database.go +++ b/backend/database.go @@ -59,7 +59,6 @@ func createTables(db *sql.DB) { } } - // Create default admin user (password: admin123) hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) _, err := db.Exec(` INSERT OR IGNORE INTO users (id, username, password, is_admin) @@ -140,15 +139,14 @@ func CreateTimeEntry(db *sql.DB, entry *TimeEntry) error { return err } -// func CreateTimeEntry(db *sql.DB, entry *TimeEntry) error { -// _, err := db.Exec("INSERT INTO time_entries (user_id, schedule_id, date, type) VALUES (?, ?, ?, ?)", -// entry.UserID, entry.ScheduleID, entry.Date, entry.Type) -// return err -// } - func GetTimeEntriesByUser(db *sql.DB, userID int) ([]TimeEntry, error) { - rows, err := db.Query("SELECT id, user_id, schedule_id, date, type, created_at FROM time_entries WHERE user_id = ? ORDER BY date DESC, created_at DESC", - userID) + rows, err := db.Query(` + SELECT te.id, te.user_id, te.schedule_id, te.date, te.type, te.start_time, te.end_time, te.created_at, u.username + FROM time_entries te + JOIN users u ON te.user_id = u.id + WHERE te.user_id = ? + ORDER BY te.date DESC, te.created_at DESC + `, userID) if err != nil { return nil, err } @@ -157,7 +155,7 @@ func GetTimeEntriesByUser(db *sql.DB, userID int) ([]TimeEntry, error) { var entries []TimeEntry for rows.Next() { var e TimeEntry - if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.CreatedAt); err != nil { + if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.StartTime, &e.EndTime, &e.CreatedAt, &e.Username); err != nil { continue } entries = append(entries, e) @@ -188,30 +186,22 @@ func GetAllTimeEntries(db *sql.DB) ([]TimeEntry, error) { return entries, nil } -// func GetAllTimeEntries(db *sql.DB) ([]TimeEntry, error) { -// rows, err := db.Query("SELECT id, user_id, schedule_id, date, type, created_at FROM time_entries ORDER BY date DESC, created_at DESC") -// if err != nil { -// return nil, err -// } -// defer rows.Close() - -// var entries []TimeEntry -// for rows.Next() { -// var e TimeEntry -// if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.CreatedAt); err != nil { -// continue -// } -// entries = append(entries, e) -// } -// return entries, nil -// } func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) { rows, err := db.Query(` SELECT te.user_id, u.username, - CAST(strftime('%W', te.date) AS INTEGER) as week, - CAST(strftime('%Y', te.date) AS INTEGER) as year, + -- ISO 8601 Wochennummer berechnen + CASE + WHEN strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days')) <= '3' + THEN CAST(strftime('%Y', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days', '-4 days')) AS INTEGER) + ELSE CAST(strftime('%Y', te.date) AS INTEGER) + END as year, + CASE + WHEN strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days')) <= '3' + THEN CAST((strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days', '-4 days')) - 1) / 7 AS INTEGER) + 53 + ELSE CAST((strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days')) - 1) / 7 AS INTEGER) + 1 + END as week, SUM( (CAST(substr(te.end_time, 1, 2) AS REAL) + CAST(substr(te.end_time, 4, 2) AS REAL) / 60.0) - (CAST(substr(te.start_time, 1, 2) AS REAL) + CAST(substr(te.start_time, 4, 2) AS REAL) / 60.0) @@ -229,7 +219,7 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) { var hours []WeeklyHours for rows.Next() { var h WeeklyHours - if err := rows.Scan(&h.UserID, &h.Username, &h.Week, &h.Year, &h.TotalHours); err != nil { + if err := rows.Scan(&h.UserID, &h.Username, &h.Year, &h.Week, &h.TotalHours); err != nil { continue } hours = append(hours, h) @@ -246,39 +236,43 @@ func DeleteUser(db *sql.DB, id int) error { } func DeleteTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week int) error { + dates := calculateWeekDates(year, week) + + var dateList []string + for day := 0; day <= 4; day++ { + dateList = append(dateList, dates.Dates[fmt.Sprintf("%d", day)]) + } + 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) + DELETE FROM time_entries + WHERE user_id = ? + AND date IN (?, ?, ?, ?, ?) + ` + _, err := db.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]) 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) + for day := 0; day <= 4; day++ { + dateList = append(dateList, dates.Dates[fmt.Sprintf("%d", day)]) } - // Prüfe ob Einträge existieren query := ` - SELECT COUNT(*) - FROM time_entries - WHERE user_id = ? - AND date IN (?, ?, ?, ?, ?) - ` + 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 { + log.Printf("Error checking entries: %v", err) return false, err } diff --git a/backend/handlers.go b/backend/handlers.go index 5bb41c1..a5a9621 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -148,7 +148,6 @@ 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 { @@ -164,7 +163,6 @@ func (app *App) GetWeekDates(c echo.Context) error { 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) @@ -225,28 +223,24 @@ func (app *App) DeleteWeekEntries(c echo.Context) error { 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" + Dates map[string]string `json:"dates"` + Range string `json:"range"` } 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 + weekday = 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 + weekDays := []string{"0", "1", "2", "3", "4"} var firstDate, lastDate time.Time for i, day := range weekDays { @@ -270,3 +264,47 @@ func calculateWeekDates(year, week int) WeekDates { Range: rangeStr, } } + +type BatchTimeEntryRequest struct { + Entries []struct { + ScheduleID int `json:"schedule_id"` + Date string `json:"date"` + Type string `json:"type"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + } `json:"entries"` +} + +func (app *App) CreateBatchTimeEntriesHandler(c echo.Context) error { + userID := c.Get("user_id").(int) + + var req BatchTimeEntryRequest + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request") + } + + tx, err := app.DB.Begin() + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "transaction error") + } + defer tx.Rollback() + + stmt, err := tx.Prepare("INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time) VALUES (?, ?, ?, ?, ?, ?)") + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "prepare error") + } + defer stmt.Close() + + for _, entry := range req.Entries { + _, err := stmt.Exec(userID, entry.ScheduleID, entry.Date, entry.Type, entry.StartTime, entry.EndTime) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "insert error") + } + } + + if err := tx.Commit(); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "commit error") + } + + return c.JSON(http.StatusCreated, map[string]string{"message": "entries created"}) +} diff --git a/backend/main.go b/backend/main.go index 1a9a81a..68b90b3 100644 --- a/backend/main.go +++ b/backend/main.go @@ -10,7 +10,6 @@ import ( ) func main() { - // Database Setup dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./timetracking.db" @@ -21,10 +20,8 @@ func main() { app := &App{DB: db} - // Echo instance e := echo.New() - // Middleware e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ @@ -33,25 +30,22 @@ func main() { AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, })) - // Custom error handler e.HTTPErrorHandler = customHTTPErrorHandler - // Public routes e.POST("/api/login", app.LoginHandler) - // Protected routes group protected := e.Group("/api") protected.Use(JWTMiddleware()) { protected.GET("/schedules", app.GetSchedulesHandler) protected.POST("/time-entries", app.CreateTimeEntryHandler) protected.GET("/my-time-entries", app.GetMyTimeEntriesHandler) + protected.POST("/time-entries/batch", app.CreateBatchTimeEntriesHandler) protected.DELETE("/my-time-entries/week", app.DeleteWeekEntries) - protected.GET("/week-dates", app.GetWeekDates) // NEU - protected.GET("/week-has-entries", app.CheckWeekHasEntries) // NEU + protected.GET("/week-dates", app.GetWeekDates) + protected.GET("/week-has-entries", app.CheckWeekHasEntries) } - // Admin routes group admin := e.Group("/api/admin") admin.Use(JWTMiddleware()) admin.Use(AdminMiddleware()) @@ -65,10 +59,8 @@ func main() { admin.GET("/weekly-hours", app.GetWeeklyHoursHandler) } - // Static files e.Static("/", "./static") - // Start server port := os.Getenv("PORT") if port == "" { port = "8080" @@ -78,7 +70,6 @@ func main() { e.Logger.Fatal(e.Start(":" + port)) } -// Custom error handler for better error responses func customHTTPErrorHandler(err error, c echo.Context) { code := http.StatusInternalServerError message := "Internal Server Error" @@ -88,7 +79,6 @@ func customHTTPErrorHandler(err error, c echo.Context) { message = he.Message.(string) } - // Don't override response if already written if !c.Response().Committed { if c.Request().Method == http.MethodHead { c.NoContent(code) diff --git a/backend/middleware.go b/backend/middleware.go index 61bfa7f..1b4967d 100644 --- a/backend/middleware.go +++ b/backend/middleware.go @@ -16,7 +16,6 @@ import ( var jwtSecret = []byte("your-secret-key-change-in-production") -// JWT Token Funktionen (bleiben gleich) func createToken(userID int, username string, isAdmin bool) (string, error) { claims := Claims{ UserID: userID, @@ -26,8 +25,7 @@ func createToken(userID int, username string, isAdmin bool) (string, error) { header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) - // Füge Expiration hinzu - claimsWithExp := map[string]interface{}{ + claimsWithExp := map[string]any{ "user_id": claims.UserID, "username": claims.Username, "is_admin": claims.IsAdmin, @@ -66,12 +64,11 @@ func verifyToken(tokenString string) (*Claims, error) { return nil, err } - var claimsMap map[string]interface{} + var claimsMap map[string]any if err := json.Unmarshal(payload, &claimsMap); err != nil { return nil, err } - // Check expiration if exp, ok := claimsMap["exp"].(float64); ok { if time.Now().Unix() > int64(exp) { return nil, fmt.Errorf("token expired") @@ -87,7 +84,6 @@ func verifyToken(tokenString string) (*Claims, error) { return claims, nil } -// Echo JWT Middleware func JWTMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -102,17 +98,17 @@ func JWTMiddleware() echo.MiddlewareFunc { return echo.NewHTTPError(http.StatusUnauthorized, "invalid token") } - // Store claims in context c.Set("user_id", claims.UserID) c.Set("username", claims.Username) c.Set("is_admin", claims.IsAdmin) + c.Logger().Infof("Authenticated user: ID=%d, Username=%s", claims.UserID, claims.Username) + return next(c) } } } -// Admin Middleware func AdminMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { @@ -125,7 +121,6 @@ func AdminMiddleware() echo.MiddlewareFunc { } } -// Custom Logger Middleware (optional - Echo hat bereits einen) func CustomLogger() echo.MiddlewareFunc { return middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "${time_rfc3339} | ${status} | ${latency_human} | ${method} ${uri}\n", diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm index ea23eee..9b4bf87 100644 --- a/frontend/src/Main.elm +++ b/frontend/src/Main.elm @@ -55,28 +55,6 @@ type alias Model = , 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 @@ -144,7 +122,7 @@ type alias NewUser = type alias WeekDates = { year : Int , week : Int - , dates : List (String, String) -- [(dayOfWeek, date)] + , dates : List (String, String) , range : String } @@ -172,33 +150,20 @@ init storedToken = , error = Nothing , weekEditMode = False , hasEntriesForCurrentWeek = False - , weekDates = Nothing -- NEU + , weekDates = Nothing } cmd = case storedToken of Just token -> Cmd.batch - [ Task.perform SetTime Time.now -- Dies lädt dann automatisch Daten + [ Task.perform SetTime Time.now , 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 @@ -216,10 +181,10 @@ type Msg | TimeEntriesSaved (Result Http.Error ()) | PreviousWeek | NextWeek - | EnableEditMode -- NEU - | DisableEditMode -- NEU - | DeleteWeekEntries -- NEU - | WeekEntriesDeleted (Result Http.Error ()) -- NEU + | EnableEditMode + | DisableEditMode + | DeleteWeekEntries + | WeekEntriesDeleted (Result Http.Error ()) | SwitchTab AdminTab | UpdateNewScheduleDay String | UpdateNewScheduleStart String @@ -239,8 +204,8 @@ type Msg | UserDeleted (Result Http.Error ()) | FetchUsers | UsersReceived (Result Http.Error (List User)) - | FetchMyTimeEntries -- NEU - | MyTimeEntriesReceived (Result Http.Error (List TimeEntry)) -- NEU + | FetchMyTimeEntries + | MyTimeEntriesReceived (Result Http.Error (List TimeEntry)) | FetchAllTimeEntries | AllTimeEntriesReceived (Result Http.Error (List TimeEntry)) | FetchWeeklyHours @@ -265,6 +230,8 @@ update msg model = LoginResponse (Ok result) -> let newPage = if result.isAdmin then AdminDashboard else UserDashboard + + (year, week) = getISOWeekFromPosix model.currentTime in ({ model | token = Just result.token @@ -275,7 +242,14 @@ update msg model = }, Cmd.batch [ saveToken result.token , fetchSchedules (Just result.token) - , if not result.isAdmin then fetchMyTimeEntries result.token else Cmd.none + , if not result.isAdmin then + Cmd.batch + [ fetchMyTimeEntries result.token + , fetchWeekDates result.token year week + , checkWeekHasEntries result.token year week + ] + else + Cmd.none ]) LoginResponse (Err _) -> @@ -290,16 +264,6 @@ update msg model = , password = "" }, removeToken ()) - -- SetTime time -> - -- let - -- (year, week) = getISOWeekFromPosix time - -- in - -- ({ model - -- | currentTime = time - -- , currentWeek = week - -- , currentYear = year - -- }, Cmd.none) - FetchSchedules -> (model, fetchSchedules model.token) @@ -310,23 +274,21 @@ update msg model = ({ model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none) ToggleScheduleSelection scheduleId dayOfWeek -> - 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) + 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) SaveTimeEntries -> case model.token of Just token -> - ({ model | error = Nothing }, saveTimeEntriesForWeek token model.selectedEntries model.currentYear model.currentWeek model.schedules) + ({ model | error = Nothing }, + saveTimeEntriesForWeek token model.selectedEntries model.currentYear model.currentWeek model.schedules model.weekDates) Nothing -> (model, Cmd.none) @@ -412,81 +374,28 @@ update msg model = SetTime time -> let (year, week) = getISOWeekFromPosix time + + cmds = case model.token of + Just token -> + if model.page == UserDashboard then + Cmd.batch + [ checkWeekHasEntries token year week + , fetchWeekDates token year week + , fetchMyTimeEntries token + ] + else + Cmd.none + Nothing -> + Cmd.none 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 - -- ) + }, cmds) EnableEditMode -> let - -- Lade bestehende Einträge in selectedEntries currentWeekEntries = List.filter (\e -> let @@ -498,7 +407,6 @@ update msg model = 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 @@ -793,55 +701,25 @@ getISOWeek year month day = let dayOfYear = getDayOfYear year month day - -- 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 weekNum < 1 then - -- Gehört zur letzten Woche des Vorjahres - 52 -- Vereinfachung: könnte auch 53 sein + 52 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 = @@ -855,7 +733,6 @@ 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 @@ -867,28 +744,19 @@ 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 - -- 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 @@ -896,24 +764,6 @@ getDateForWeekDay year week dayOfWeek = 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 startYear startMonth startDay daysToAdd = @@ -938,7 +788,6 @@ addDaysToDate startYear startMonth startDay daysToAdd = if remaining == 0 then (y, m, d) else if remaining > 0 then - -- Vorwärts zählen let daysInCurrentMonth = daysInMonth m y daysLeftInMonth = daysInCurrentMonth - d @@ -950,7 +799,6 @@ addDaysToDate startYear startMonth startDay daysToAdd = else 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 @@ -965,41 +813,6 @@ addDaysToDate startYear startMonth startDay daysToAdd = helper y (m - 1) prevMonthDays (remaining + d) in 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 = @@ -1143,7 +956,6 @@ viewUserDashboard model = [ viewWeekNavigation model , h2 [ class "title" ] [ text "Stundenplan" ] - -- Status-Anzeige und Bearbeiten-Button , if model.hasEntriesForCurrentWeek && not model.weekEditMode then div [ class "notification is-success" ] [ div [ class "level" ] @@ -1191,10 +1003,9 @@ viewUserDashboard model = 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 + , if not model.hasEntriesForCurrentWeek || model.weekEditMode then div [ class "field mt-4" ] [ div [ class "control" ] [ button @@ -1322,39 +1133,6 @@ 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 = @@ -1397,41 +1175,28 @@ viewDayColumnWithWeek model (dayOfWeek, schedules) = [ 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 - -- Prüfe ob dieser Eintrag bereits in der DB ist (nur relevant wenn Edit-Mode aktiv) - isLocked = model.hasEntriesForCurrentWeek && not model.weekEditMode + isClickable = not model.hasEntriesForCurrentWeek || model.weekEditMode boxClass = - if isLocked then - if isSelected then "box has-background-success-light" else "box has-background-white" - else if isSelected then + 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" + cursorStyle = if isClickable then "pointer" else "not-allowed" + opacity = if isClickable || isSelected then "1" else "0.6" in div [ class boxClass - , onClick (if isLocked then Logout else ToggleScheduleSelection schedule.id dayOfWeek) -- Dummy onClick wenn locked + , onClick (if isClickable then ToggleScheduleSelection schedule.id dayOfWeek else CheckWeekHasEntries) -- Dummy-Event wenn nicht klickbar , style "cursor" cursorStyle , style "margin-bottom" "0.5rem" , style "padding" "0.75rem" @@ -1443,9 +1208,6 @@ 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" ] @@ -1812,42 +1574,50 @@ fetchMyTimeEntries token = , tracker = Nothing } -saveTimeEntriesForWeek : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Cmd Msg -saveTimeEntriesForWeek token selectedEntries year week schedules = - let - getScheduleById id = - List.filter (\s -> s.id == id) schedules |> List.head +saveTimeEntriesForWeek : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Maybe WeekDates -> Cmd Msg +saveTimeEntriesForWeek token selectedEntries year week schedules maybeWeekDates = + case maybeWeekDates of + Nothing -> + Cmd.none - createRequest entry = - case getScheduleById entry.scheduleId of - Just schedule -> - let - dateStr = getDateForWeekDay year week entry.dayOfWeek - in - Just <| Http.request - { method = "POST" - , headers = [ Http.header "Authorization" ("Bearer " ++ token) ] - , url = "/api/time-entries" - , body = Http.jsonBody <| - Encode.object + Just weekDates -> + let + getScheduleById id = + List.filter (\s -> s.id == id) schedules |> List.head + + getDateForDay dayOfWeek = + weekDates.dates + |> List.filter (\(day, _) -> day == String.fromInt dayOfWeek) + |> List.head + |> Maybe.map Tuple.second + + createEntryData entry = + case (getScheduleById entry.scheduleId, getDateForDay entry.dayOfWeek) of + (Just schedule, Just dateStr) -> + Just <| Encode.object [ ("schedule_id", Encode.int entry.scheduleId) , ("date", Encode.string dateStr) , ("type", Encode.string schedule.scheduleType) , ("start_time", Encode.string schedule.startTime) , ("end_time", Encode.string schedule.endTime) ] - , expect = Http.expectWhatever TimeEntriesSaved - , timeout = Nothing - , tracker = Nothing - } - Nothing -> - Nothing - - requests = List.filterMap createRequest selectedEntries - in - case List.head requests of - Just cmd -> cmd - Nothing -> Cmd.none + _ -> + Nothing + + entriesData = List.filterMap createEntryData selectedEntries + in + if List.isEmpty entriesData then + Cmd.none + else + Http.request + { method = "POST" + , headers = [ Http.header "Authorization" ("Bearer " ++ token) ] + , url = "/api/time-entries/batch" + , body = Http.jsonBody <| Encode.object [ ("entries", Encode.list identity entriesData) ] + , expect = Http.expectWhatever TimeEntriesSaved + , timeout = Nothing + , tracker = Nothing + } deleteWeekEntries : String -> Int -> Int -> Cmd Msg deleteWeekEntries token year week =