fix: fix wrong date calculation

This commit is contained in:
Patryk Hegenberg 2025-11-05 08:58:05 +01:00
parent 4514ce44a2
commit c8b7666971
4 changed files with 815 additions and 62 deletions

View file

@ -244,3 +244,43 @@ func DeleteUser(db *sql.DB, id int) error {
_, err := db.Exec("DELETE FROM users WHERE id = ?", id) _, err := db.Exec("DELETE FROM users WHERE id = ?", id)
return err 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
}

View file

@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -147,6 +148,44 @@ func (app *App) GetMyTimeEntriesHandler(c echo.Context) error {
return c.JSON(http.StatusOK, entries) 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 { func (app *App) GetAllTimeEntriesHandler(c echo.Context) error {
entries, err := GetAllTimeEntries(app.DB) entries, err := GetAllTimeEntries(app.DB)
if err != nil { if err != nil {
@ -162,3 +201,72 @@ func (app *App) GetWeeklyHoursHandler(c echo.Context) error {
} }
return c.JSON(http.StatusOK, hours) 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,
}
}

View file

@ -46,6 +46,9 @@ func main() {
protected.GET("/schedules", app.GetSchedulesHandler) protected.GET("/schedules", app.GetSchedulesHandler)
protected.POST("/time-entries", app.CreateTimeEntryHandler) protected.POST("/time-entries", app.CreateTimeEntryHandler)
protected.GET("/my-time-entries", app.GetMyTimeEntriesHandler) 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 // Admin routes group

View file

@ -9,6 +9,7 @@ import Json.Decode as Decode exposing (Decoder, field, int, string, bool, list,
import Json.Encode as Encode import Json.Encode as Encode
import Task import Task
import Time import Time
import Dict exposing (Dict)
-- PORTS -- PORTS
@ -45,12 +46,37 @@ type alias Model =
, selectedEntries : List SelectedEntry , selectedEntries : List SelectedEntry
, currentWeek : Int , currentWeek : Int
, currentYear : Int , currentYear : Int
, weekDates : Maybe WeekDates -- NEU: Backend liefert Daten
, currentTime : Time.Posix , currentTime : Time.Posix
, zone : Time.Zone , zone : Time.Zone
, newSchedule : NewSchedule , newSchedule : NewSchedule
, newUser : NewUser , newUser : NewUser
, error : Maybe String , 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 type Page
= LoginPage = LoginPage
@ -115,6 +141,13 @@ type alias NewUser =
, isAdmin : Bool , isAdmin : Bool
} }
type alias WeekDates =
{ year : Int
, week : Int
, dates : List (String, String) -- [(dayOfWeek, date)]
, range : String
}
init : Maybe String -> (Model, Cmd Msg) init : Maybe String -> (Model, Cmd Msg)
init storedToken = init storedToken =
let let
@ -137,19 +170,34 @@ init storedToken =
, newSchedule = NewSchedule "" "" "" "lesson" "" , newSchedule = NewSchedule "" "" "" "lesson" ""
, newUser = NewUser "" "" False , newUser = NewUser "" "" False
, error = Nothing , error = Nothing
, weekEditMode = False
, hasEntriesForCurrentWeek = False
, weekDates = Nothing -- NEU
} }
cmd = cmd =
case storedToken of case storedToken of
Just token -> Just token ->
Cmd.batch Cmd.batch
[ Task.perform SetTime Time.now [ Task.perform SetTime Time.now -- Dies lädt dann automatisch Daten
, fetchSchedules (Just token) , fetchSchedules (Just token)
] ]
Nothing -> Nothing ->
Task.perform SetTime Time.now Task.perform SetTime Time.now
in in
(model, cmd) (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 -- UPDATE
@ -168,6 +216,10 @@ type Msg
| TimeEntriesSaved (Result Http.Error ()) | TimeEntriesSaved (Result Http.Error ())
| PreviousWeek | PreviousWeek
| NextWeek | NextWeek
| EnableEditMode -- NEU
| DisableEditMode -- NEU
| DeleteWeekEntries -- NEU
| WeekEntriesDeleted (Result Http.Error ()) -- NEU
| SwitchTab AdminTab | SwitchTab AdminTab
| UpdateNewScheduleDay String | UpdateNewScheduleDay String
| UpdateNewScheduleStart String | UpdateNewScheduleStart String
@ -187,10 +239,16 @@ type Msg
| UserDeleted (Result Http.Error ()) | UserDeleted (Result Http.Error ())
| FetchUsers | FetchUsers
| UsersReceived (Result Http.Error (List User)) | UsersReceived (Result Http.Error (List User))
| FetchMyTimeEntries -- NEU
| MyTimeEntriesReceived (Result Http.Error (List TimeEntry)) -- NEU
| FetchAllTimeEntries | FetchAllTimeEntries
| AllTimeEntriesReceived (Result Http.Error (List TimeEntry)) | AllTimeEntriesReceived (Result Http.Error (List TimeEntry))
| FetchWeeklyHours | FetchWeeklyHours
| WeeklyHoursReceived (Result Http.Error (List WeeklyHours)) | 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 -> (Model, Cmd Msg)
update msg model = update msg model =
@ -210,12 +268,14 @@ update msg model =
in in
({ model ({ model
| token = Just result.token | token = Just result.token
, username = result.username
, isAdmin = result.isAdmin , isAdmin = result.isAdmin
, page = newPage , page = newPage
, error = Nothing , error = Nothing
}, Cmd.batch }, Cmd.batch
[ saveToken result.token [ saveToken result.token
, fetchSchedules (Just result.token) , fetchSchedules (Just result.token)
, if not result.isAdmin then fetchMyTimeEntries result.token else Cmd.none
]) ])
LoginResponse (Err _) -> LoginResponse (Err _) ->
@ -230,15 +290,15 @@ update msg model =
, password = "" , password = ""
}, removeToken ()) }, removeToken ())
SetTime time -> -- SetTime time ->
let -- let
(year, week) = getISOWeekFromPosix time -- (year, week) = getISOWeekFromPosix time
in -- in
({ model -- ({ model
| currentTime = time -- | currentTime = time
, currentWeek = week -- , currentWeek = week
, currentYear = year -- , currentYear = year
}, Cmd.none) -- }, Cmd.none)
FetchSchedules -> FetchSchedules ->
(model, fetchSchedules model.token) (model, fetchSchedules model.token)
@ -250,6 +310,7 @@ update msg model =
({ model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none) ({ model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none)
ToggleScheduleSelection scheduleId dayOfWeek -> ToggleScheduleSelection scheduleId dayOfWeek ->
if model.weekEditMode then
let let
entry = { scheduleId = scheduleId, dayOfWeek = dayOfWeek } entry = { scheduleId = scheduleId, dayOfWeek = dayOfWeek }
newSelected = newSelected =
@ -259,6 +320,8 @@ update msg model =
entry :: model.selectedEntries entry :: model.selectedEntries
in in
({ model | selectedEntries = newSelected }, Cmd.none) ({ model | selectedEntries = newSelected }, Cmd.none)
else
(model, Cmd.none)
SaveTimeEntries -> SaveTimeEntries ->
case model.token of case model.token of
@ -268,7 +331,16 @@ update msg model =
(model, Cmd.none) (model, Cmd.none)
TimeEntriesSaved (Ok _) -> 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 _) -> TimeEntriesSaved (Err _) ->
({ model | error = Just "Fehler beim Speichern" }, Cmd.none) ({ model | error = Just "Fehler beim Speichern" }, Cmd.none)
@ -277,13 +349,198 @@ update msg model =
let let
(newYear, newWeek) = previousWeek model.currentYear model.currentWeek (newYear, newWeek) = previousWeek model.currentYear model.currentWeek
in 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 -> NextWeek ->
let let
(newYear, newWeek) = nextWeek model.currentYear model.currentWeek (newYear, newWeek) = nextWeek model.currentYear model.currentWeek
in 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 -> SwitchTab tab ->
let let
@ -294,7 +551,6 @@ update msg model =
fetchUsers token fetchUsers token
Nothing -> Nothing ->
Cmd.none Cmd.none
-- fetchUsers model.token
TimeEntriesTab -> TimeEntriesTab ->
case model.token of case model.token of
Just token -> Just token ->
@ -410,7 +666,6 @@ update msg model =
({ model | newUser = emptyUser }, fetchUsers token) ({ model | newUser = emptyUser }, fetchUsers token)
Nothing -> Nothing ->
(model, Cmd.none) (model, Cmd.none)
-- ({ model | newUser = emptyUser }, fetchUsers model.token)
UserCreated (Err _) -> UserCreated (Err _) ->
({ model | error = Just "Fehler beim Erstellen des Benutzers" }, Cmd.none) ({ model | error = Just "Fehler beim Erstellen des Benutzers" }, Cmd.none)
@ -428,7 +683,6 @@ update msg model =
(model, fetchUsers token) (model, fetchUsers token)
Nothing -> Nothing ->
(model, Cmd.none) (model, Cmd.none)
-- (model, fetchUsers model.token)
UserDeleted (Err _) -> UserDeleted (Err _) ->
({ model | error = Just "Fehler beim Löschen des Benutzers" }, Cmd.none) ({ model | error = Just "Fehler beim Löschen des Benutzers" }, Cmd.none)
@ -446,6 +700,33 @@ update msg model =
UsersReceived (Err _) -> UsersReceived (Err _) ->
({ model | error = Just "Fehler beim Laden der Benutzer" }, Cmd.none) ({ 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 -> FetchAllTimeEntries ->
case model.token of case model.token of
Just token -> Just token ->
@ -511,11 +792,56 @@ getISOWeek : Int -> Int -> Int -> Int
getISOWeek year month day = getISOWeek year month day =
let let
dayOfYear = getDayOfYear year month day dayOfYear = getDayOfYear year month day
jan1DayOfWeek = getDayOfWeek year 1 1
weekDay = modBy 7 (jan1DayOfWeek + dayOfYear - 1) -- Wochentag des 4. Januar (definiert ISO Woche 1)
weekNumber = ((dayOfYear + jan1DayOfWeek - 1) // 7) + 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 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 : Int -> Int -> Int -> Int
getDayOfYear year month day = getDayOfYear year month day =
@ -529,6 +855,7 @@ isLeapYear : Int -> Bool
isLeapYear year = isLeapYear year =
(modBy 4 year == 0) && ((modBy 100 year /= 0) || (modBy 400 year == 0)) (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 : Int -> Int -> Int -> Int
getDayOfWeek year month day = getDayOfWeek year month day =
let let
@ -540,25 +867,56 @@ getDayOfWeek year month day =
j = adjustedYear // 100 j = adjustedYear // 100
h = (q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) |> modBy 7 h = (q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) |> modBy 7
in in
-- Konvertiere: Zeller gibt Samstag=0, Sonntag=1, ... Freitag=6
-- ISO 8601 will: Montag=0, ..., Sonntag=6
(h + 5) |> modBy 7 (h + 5) |> modBy 7
-- Korrigiertes getDateForWeekDay
getDateForWeekDay : Int -> Int -> Int -> String getDateForWeekDay : Int -> Int -> Int -> String
getDateForWeekDay year week dayOfWeek = getDateForWeekDay year week dayOfWeek =
let let
-- Finde den 4. Januar (immer in Woche 1 nach ISO 8601)
jan4DayOfWeek = getDayOfWeek year 1 4 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 in
String.fromInt finalYear ++ "-" ++ String.fromInt finalYear ++ "-" ++
String.padLeft 2 '0' (String.fromInt finalMonth) ++ "-" ++ String.padLeft 2 '0' (String.fromInt finalMonth) ++ "-" ++
String.padLeft 2 '0' (String.fromInt finalDay) 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 : Int -> Int -> Int -> Int -> (Int, Int, Int)
addDaysToDate year month day daysToAdd = addDaysToDate startYear startMonth startDay daysToAdd =
let let
daysInMonth m y = daysInMonth m y =
case m of case m of
@ -577,21 +935,71 @@ addDaysToDate year month day daysToAdd =
_ -> 0 _ -> 0
helper y m d remaining = helper y m d remaining =
if remaining <= 0 then if remaining == 0 then
(y, m, d) (y, m, d)
else else if remaining > 0 then
-- Vorwärts zählen
let let
daysInCurrentMonth = daysInMonth m y daysInCurrentMonth = daysInMonth m y
daysLeftInMonth = daysInCurrentMonth - d + 1 daysLeftInMonth = daysInCurrentMonth - d
in in
if remaining < daysLeftInMonth then if remaining <= daysLeftInMonth then
(y, m, d + remaining) (y, m, d + remaining)
else if m == 12 then else if m == 12 then
helper (y + 1) 1 1 (remaining - daysLeftInMonth) helper (y + 1) 1 1 (remaining - daysLeftInMonth - 1)
else 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 in
helper year month day daysToAdd helper (y - 1) 12 prevMonthDays (remaining + d)
else
let
prevMonthDays = daysInMonth (m - 1) y
in
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 : Int -> Int -> (Int, Int)
previousWeek year week = previousWeek year week =
@ -602,7 +1010,7 @@ previousWeek year week =
nextWeek : Int -> Int -> (Int, Int) nextWeek : Int -> Int -> (Int, Int)
nextWeek year week = nextWeek year week =
if week == 52 then if week >= 52 then
(year + 1, 1) (year + 1, 1)
else else
(year, week + 1) (year, week + 1)
@ -615,6 +1023,16 @@ getWeekDateRange year week =
in in
mondayDate ++ " bis " ++ fridayDate 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 : String -> String -> Float
calculateHours startTime endTime = calculateHours startTime endTime =
let let
@ -724,16 +1142,71 @@ viewUserDashboard model =
[ div [ class "container" ] [ div [ class "container" ]
[ viewWeekNavigation model [ viewWeekNavigation model
, h2 [ class "title" ] [ text "Stundenplan" ] , 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" ]
[ 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 , viewScheduleGridWithWeek model
, div [ class "field mt-4" ]
, if model.weekEditMode || not model.hasEntriesForCurrentWeek then
div [ class "field mt-4" ]
[ div [ class "control" ] [ div [ class "control" ]
[ button [ button
[ class "button is-primary is-large is-fullwidth" [ class "button is-primary is-large is-fullwidth"
, onClick SaveTimeEntries , onClick SaveTimeEntries
, disabled (List.isEmpty model.selectedEntries) , disabled (List.isEmpty model.selectedEntries)
] [ text "Speichern" ] ] [ text (if model.weekEditMode then "Änderungen speichern" else "Speichern") ]
] ]
] ]
else
text ""
, case model.error of , case model.error of
Just err -> Just err ->
div [ class "notification is-danger mt-4" ] [ text err ] div [ class "notification is-danger mt-4" ] [ text err ]
@ -812,6 +1285,12 @@ viewTimeEntriesTab model =
viewWeekNavigation : Model -> Html Msg viewWeekNavigation : Model -> Html Msg
viewWeekNavigation model = viewWeekNavigation model =
let
dateRange =
case model.weekDates of
Just wd -> wd.range
Nothing -> "Laden..."
in
div [ class "box" ] div [ class "box" ]
[ nav [ class "level" ] [ nav [ class "level" ]
[ div [ class "level-left" ] [ div [ class "level-left" ]
@ -829,7 +1308,7 @@ viewWeekNavigation model =
, p [ class "title" ] , p [ class "title" ]
[ text ("KW " ++ String.fromInt model.currentWeek ++ " / " ++ String.fromInt model.currentYear) ] [ text ("KW " ++ String.fromInt model.currentWeek ++ " / " ++ String.fromInt model.currentYear) ]
, p [ class "subtitle is-6" ] , p [ class "subtitle is-6" ]
[ text (getWeekDateRange model.currentYear model.currentWeek) ] [ text dateRange ]
] ]
] ]
, div [ class "level-right" ] , 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 -> Html Msg
viewScheduleGridWithWeek model = viewScheduleGridWithWeek model =
@ -869,27 +1381,61 @@ viewScheduleGridWithWeek model =
viewDayColumnWithWeek : Model -> (Int, List Schedule) -> Html Msg viewDayColumnWithWeek : Model -> (Int, List Schedule) -> Html Msg
viewDayColumnWithWeek model (dayOfWeek, schedules) = viewDayColumnWithWeek model (dayOfWeek, schedules) =
let 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 in
td [ class "has-background-light", style "vertical-align" "top", style "min-width" "150px" ] 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" ] [ p [ class "has-text-centered has-text-weight-bold is-size-7 mb-2" ]
[ text dateForDay ] [ text dateForDay ]
, div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules) , 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 -> Int -> Schedule -> Html Msg
viewScheduleItemWithDay model dayOfWeek schedule = viewScheduleItemWithDay model dayOfWeek schedule =
let let
isSelected = List.any (\e -> e.scheduleId == schedule.id && e.dayOfWeek == dayOfWeek) model.selectedEntries 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 "" 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 in
div div
[ class boxClass [ class boxClass
, onClick (ToggleScheduleSelection schedule.id dayOfWeek) , onClick (if isLocked then Logout else ToggleScheduleSelection schedule.id dayOfWeek) -- Dummy onClick wenn locked
, style "cursor" "pointer" , style "cursor" cursorStyle
, style "margin-bottom" "0.5rem" , style "margin-bottom" "0.5rem"
, style "padding" "0.75rem" , style "padding" "0.75rem"
, style "opacity" opacity
] ]
[ p [ class "has-text-weight-bold is-size-7" ] [ p [ class "has-text-weight-bold is-size-7" ]
[ text (schedule.startTime ++ " - " ++ schedule.endTime) ] [ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
@ -897,6 +1443,9 @@ viewScheduleItemWithDay model dayOfWeek schedule =
[ text (schedule.title ++ typeText) ] [ 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 -> Html Msg
viewScheduleForm model = viewScheduleForm model =
div [ class "box" ] div [ class "box" ]
@ -1159,11 +1708,7 @@ viewTimeEntriesList model =
filteredEntries = List.filter filteredEntries = List.filter
(\e -> (\e ->
let let
parts = String.split "-" e.date (entryYear, entryWeek) = getYearWeekFromDate 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
in in
entryWeek == model.currentWeek && entryYear == model.currentYear entryWeek == model.currentWeek && entryYear == model.currentYear
) )
@ -1255,6 +1800,18 @@ scheduleDecoder =
(field "type" string) (field "type" string)
(field "title" 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 : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Cmd Msg
saveTimeEntriesForWeek token selectedEntries year week schedules = saveTimeEntriesForWeek token selectedEntries year week schedules =
let let
@ -1292,6 +1849,18 @@ saveTimeEntriesForWeek token selectedEntries year week schedules =
Just cmd -> cmd Just cmd -> cmd
Nothing -> Cmd.none 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 : String -> NewSchedule -> Cmd Msg
createSchedule token schedule = createSchedule token schedule =
case String.toInt schedule.dayOfWeek of case String.toInt schedule.dayOfWeek of
@ -1419,3 +1988,36 @@ weeklyHoursDecoder =
(field "week" int) (field "week" int)
(field "year" int) (field "year" int)
(field "total_hours" float) (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
}