fix: fix reset of entries when switching between weeks

This commit is contained in:
Patryk Hegenberg 2025-11-05 10:19:36 +01:00
parent c8b7666971
commit 20ba24001a
5 changed files with 184 additions and 397 deletions

View file

@ -59,7 +59,6 @@ func createTables(db *sql.DB) {
} }
} }
// Create default admin user (password: admin123)
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
_, err := db.Exec(` _, err := db.Exec(`
INSERT OR IGNORE INTO users (id, username, password, is_admin) INSERT OR IGNORE INTO users (id, username, password, is_admin)
@ -140,15 +139,14 @@ func CreateTimeEntry(db *sql.DB, entry *TimeEntry) error {
return err 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) { 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", rows, err := db.Query(`
userID) 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 { if err != nil {
return nil, err return nil, err
} }
@ -157,7 +155,7 @@ func GetTimeEntriesByUser(db *sql.DB, userID int) ([]TimeEntry, error) {
var entries []TimeEntry var entries []TimeEntry
for rows.Next() { for rows.Next() {
var e TimeEntry 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 continue
} }
entries = append(entries, e) entries = append(entries, e)
@ -188,30 +186,22 @@ func GetAllTimeEntries(db *sql.DB) ([]TimeEntry, error) {
return entries, nil 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) { func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
rows, err := db.Query(` rows, err := db.Query(`
SELECT SELECT
te.user_id, te.user_id,
u.username, u.username,
CAST(strftime('%W', te.date) AS INTEGER) as week, -- ISO 8601 Wochennummer berechnen
CAST(strftime('%Y', te.date) AS INTEGER) as year, 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( SUM(
(CAST(substr(te.end_time, 1, 2) AS REAL) + CAST(substr(te.end_time, 4, 2) AS REAL) / 60.0) - (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) (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 var hours []WeeklyHours
for rows.Next() { for rows.Next() {
var h WeeklyHours 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 continue
} }
hours = append(hours, h) hours = append(hours, h)
@ -246,27 +236,30 @@ func DeleteUser(db *sql.DB, id int) error {
} }
func DeleteTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week 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 := ` query := `
DELETE FROM time_entries DELETE FROM time_entries
WHERE user_id = ? WHERE user_id = ?
AND CAST(strftime('%W', date) AS INTEGER) = ? AND date IN (?, ?, ?, ?, ?)
AND CAST(strftime('%Y', date) AS INTEGER) = ?
` `
_, err := db.Exec(query, userID, week, year) _, err := db.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
return err return err
} }
func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (bool, error) { func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (bool, error) {
// Berechne die Daten der Woche
dates := calculateWeekDates(year, week) dates := calculateWeekDates(year, week)
// Hole alle Daten als Liste
var dateList []string var dateList []string
for _, date := range dates.Dates { for day := 0; day <= 4; day++ {
dateList = append(dateList, date) dateList = append(dateList, dates.Dates[fmt.Sprintf("%d", day)])
} }
// Prüfe ob Einträge existieren
query := ` query := `
SELECT COUNT(*) SELECT COUNT(*)
FROM time_entries FROM time_entries
@ -279,6 +272,7 @@ func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (boo
dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count) dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count)
if err != nil { if err != nil {
log.Printf("Error checking entries: %v", err)
return false, err return false, err
} }

View file

@ -148,7 +148,6 @@ 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 { func (app *App) GetWeekDates(c echo.Context) error {
year, err := strconv.Atoi(c.QueryParam("year")) year, err := strconv.Atoi(c.QueryParam("year"))
if err != nil { if err != nil {
@ -164,7 +163,6 @@ func (app *App) GetWeekDates(c echo.Context) error {
return c.JSON(http.StatusOK, dates) 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 { func (app *App) CheckWeekHasEntries(c echo.Context) error {
userID := c.Get("user_id").(int) userID := c.Get("user_id").(int)
@ -225,28 +223,24 @@ func (app *App) DeleteWeekEntries(c echo.Context) error {
type WeekDates struct { type WeekDates struct {
Year int `json:"year"` Year int `json:"year"`
Week int `json:"week"` Week int `json:"week"`
Dates map[string]string `json:"dates"` // dayOfWeek -> date Dates map[string]string `json:"dates"`
Range string `json:"range"` // "2025-11-03 bis 2025-11-07" Range string `json:"range"`
} }
func calculateWeekDates(year, week int) WeekDates { 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) jan4 := time.Date(year, time.January, 4, 0, 0, 0, 0, time.UTC)
// Finde Montag der Woche 1
weekday := int(jan4.Weekday()) weekday := int(jan4.Weekday())
if weekday == 0 { if weekday == 0 {
weekday = 7 // Sonntag -> 7 weekday = 7
} }
daysToMonday := weekday - 1 daysToMonday := weekday - 1
mondayWeek1 := jan4.AddDate(0, 0, -daysToMonday) mondayWeek1 := jan4.AddDate(0, 0, -daysToMonday)
// Berechne Montag der gewünschten Woche
targetMonday := mondayWeek1.AddDate(0, 0, (week-1)*7) targetMonday := mondayWeek1.AddDate(0, 0, (week-1)*7)
dates := make(map[string]string) 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 var firstDate, lastDate time.Time
for i, day := range weekDays { for i, day := range weekDays {
@ -270,3 +264,47 @@ func calculateWeekDates(year, week int) WeekDates {
Range: rangeStr, 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"})
}

View file

@ -10,7 +10,6 @@ import (
) )
func main() { func main() {
// Database Setup
dbPath := os.Getenv("DB_PATH") dbPath := os.Getenv("DB_PATH")
if dbPath == "" { if dbPath == "" {
dbPath = "./timetracking.db" dbPath = "./timetracking.db"
@ -21,10 +20,8 @@ func main() {
app := &App{DB: db} app := &App{DB: db}
// Echo instance
e := echo.New() e := echo.New()
// Middleware
e.Use(middleware.Logger()) e.Use(middleware.Logger())
e.Use(middleware.Recover()) e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
@ -33,25 +30,22 @@ func main() {
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
})) }))
// Custom error handler
e.HTTPErrorHandler = customHTTPErrorHandler e.HTTPErrorHandler = customHTTPErrorHandler
// Public routes
e.POST("/api/login", app.LoginHandler) e.POST("/api/login", app.LoginHandler)
// Protected routes group
protected := e.Group("/api") protected := e.Group("/api")
protected.Use(JWTMiddleware()) protected.Use(JWTMiddleware())
{ {
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.POST("/time-entries/batch", app.CreateBatchTimeEntriesHandler)
protected.DELETE("/my-time-entries/week", app.DeleteWeekEntries) protected.DELETE("/my-time-entries/week", app.DeleteWeekEntries)
protected.GET("/week-dates", app.GetWeekDates) // NEU protected.GET("/week-dates", app.GetWeekDates)
protected.GET("/week-has-entries", app.CheckWeekHasEntries) // NEU protected.GET("/week-has-entries", app.CheckWeekHasEntries)
} }
// Admin routes group
admin := e.Group("/api/admin") admin := e.Group("/api/admin")
admin.Use(JWTMiddleware()) admin.Use(JWTMiddleware())
admin.Use(AdminMiddleware()) admin.Use(AdminMiddleware())
@ -65,10 +59,8 @@ func main() {
admin.GET("/weekly-hours", app.GetWeeklyHoursHandler) admin.GET("/weekly-hours", app.GetWeeklyHoursHandler)
} }
// Static files
e.Static("/", "./static") e.Static("/", "./static")
// Start server
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8080" port = "8080"
@ -78,7 +70,6 @@ func main() {
e.Logger.Fatal(e.Start(":" + port)) e.Logger.Fatal(e.Start(":" + port))
} }
// Custom error handler for better error responses
func customHTTPErrorHandler(err error, c echo.Context) { func customHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError code := http.StatusInternalServerError
message := "Internal Server Error" message := "Internal Server Error"
@ -88,7 +79,6 @@ func customHTTPErrorHandler(err error, c echo.Context) {
message = he.Message.(string) message = he.Message.(string)
} }
// Don't override response if already written
if !c.Response().Committed { if !c.Response().Committed {
if c.Request().Method == http.MethodHead { if c.Request().Method == http.MethodHead {
c.NoContent(code) c.NoContent(code)

View file

@ -16,7 +16,6 @@ import (
var jwtSecret = []byte("your-secret-key-change-in-production") var jwtSecret = []byte("your-secret-key-change-in-production")
// JWT Token Funktionen (bleiben gleich)
func createToken(userID int, username string, isAdmin bool) (string, error) { func createToken(userID int, username string, isAdmin bool) (string, error) {
claims := Claims{ claims := Claims{
UserID: userID, 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"}`)) header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
// Füge Expiration hinzu claimsWithExp := map[string]any{
claimsWithExp := map[string]interface{}{
"user_id": claims.UserID, "user_id": claims.UserID,
"username": claims.Username, "username": claims.Username,
"is_admin": claims.IsAdmin, "is_admin": claims.IsAdmin,
@ -66,12 +64,11 @@ func verifyToken(tokenString string) (*Claims, error) {
return nil, err return nil, err
} }
var claimsMap map[string]interface{} var claimsMap map[string]any
if err := json.Unmarshal(payload, &claimsMap); err != nil { if err := json.Unmarshal(payload, &claimsMap); err != nil {
return nil, err return nil, err
} }
// Check expiration
if exp, ok := claimsMap["exp"].(float64); ok { if exp, ok := claimsMap["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) { if time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("token expired") return nil, fmt.Errorf("token expired")
@ -87,7 +84,6 @@ func verifyToken(tokenString string) (*Claims, error) {
return claims, nil return claims, nil
} }
// Echo JWT Middleware
func JWTMiddleware() echo.MiddlewareFunc { func JWTMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -102,17 +98,17 @@ func JWTMiddleware() echo.MiddlewareFunc {
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token") return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
} }
// Store claims in context
c.Set("user_id", claims.UserID) c.Set("user_id", claims.UserID)
c.Set("username", claims.Username) c.Set("username", claims.Username)
c.Set("is_admin", claims.IsAdmin) c.Set("is_admin", claims.IsAdmin)
c.Logger().Infof("Authenticated user: ID=%d, Username=%s", claims.UserID, claims.Username)
return next(c) return next(c)
} }
} }
} }
// Admin Middleware
func AdminMiddleware() echo.MiddlewareFunc { func AdminMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { 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 { func CustomLogger() echo.MiddlewareFunc {
return middleware.LoggerWithConfig(middleware.LoggerConfig{ return middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339} | ${status} | ${latency_human} | ${method} ${uri}\n", Format: "${time_rfc3339} | ${status} | ${latency_human} | ${method} ${uri}\n",

View file

@ -55,28 +55,6 @@ type alias Model =
, weekEditMode : Bool , weekEditMode : Bool
, hasEntriesForCurrentWeek : 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
@ -144,7 +122,7 @@ type alias NewUser =
type alias WeekDates = type alias WeekDates =
{ year : Int { year : Int
, week : Int , week : Int
, dates : List (String, String) -- [(dayOfWeek, date)] , dates : List (String, String)
, range : String , range : String
} }
@ -172,33 +150,20 @@ init storedToken =
, error = Nothing , error = Nothing
, weekEditMode = False , weekEditMode = False
, hasEntriesForCurrentWeek = False , hasEntriesForCurrentWeek = False
, weekDates = Nothing -- NEU , weekDates = Nothing
} }
cmd = cmd =
case storedToken of case storedToken of
Just token -> Just token ->
Cmd.batch Cmd.batch
[ Task.perform SetTime Time.now -- Dies lädt dann automatisch Daten [ Task.perform SetTime Time.now
, 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
@ -216,10 +181,10 @@ type Msg
| TimeEntriesSaved (Result Http.Error ()) | TimeEntriesSaved (Result Http.Error ())
| PreviousWeek | PreviousWeek
| NextWeek | NextWeek
| EnableEditMode -- NEU | EnableEditMode
| DisableEditMode -- NEU | DisableEditMode
| DeleteWeekEntries -- NEU | DeleteWeekEntries
| WeekEntriesDeleted (Result Http.Error ()) -- NEU | WeekEntriesDeleted (Result Http.Error ())
| SwitchTab AdminTab | SwitchTab AdminTab
| UpdateNewScheduleDay String | UpdateNewScheduleDay String
| UpdateNewScheduleStart String | UpdateNewScheduleStart String
@ -239,8 +204,8 @@ 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 | FetchMyTimeEntries
| MyTimeEntriesReceived (Result Http.Error (List TimeEntry)) -- NEU | MyTimeEntriesReceived (Result Http.Error (List TimeEntry))
| FetchAllTimeEntries | FetchAllTimeEntries
| AllTimeEntriesReceived (Result Http.Error (List TimeEntry)) | AllTimeEntriesReceived (Result Http.Error (List TimeEntry))
| FetchWeeklyHours | FetchWeeklyHours
@ -265,6 +230,8 @@ update msg model =
LoginResponse (Ok result) -> LoginResponse (Ok result) ->
let let
newPage = if result.isAdmin then AdminDashboard else UserDashboard newPage = if result.isAdmin then AdminDashboard else UserDashboard
(year, week) = getISOWeekFromPosix model.currentTime
in in
({ model ({ model
| token = Just result.token | token = Just result.token
@ -275,7 +242,14 @@ update msg model =
}, 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 , 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 _) -> LoginResponse (Err _) ->
@ -290,16 +264,6 @@ update msg model =
, password = "" , password = ""
}, removeToken ()) }, removeToken ())
-- SetTime time ->
-- let
-- (year, week) = getISOWeekFromPosix time
-- in
-- ({ model
-- | currentTime = time
-- , currentWeek = week
-- , currentYear = year
-- }, Cmd.none)
FetchSchedules -> FetchSchedules ->
(model, fetchSchedules model.token) (model, fetchSchedules model.token)
@ -310,7 +274,6 @@ 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 =
@ -320,13 +283,12 @@ 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
Just token -> 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 -> Nothing ->
(model, Cmd.none) (model, Cmd.none)
@ -412,81 +374,28 @@ update msg model =
SetTime time -> SetTime time ->
let let
(year, week) = getISOWeekFromPosix time (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 in
({ model ({ model
| currentTime = time | currentTime = time
, currentWeek = week , currentWeek = week
, currentYear = year , currentYear = year
}, case model.token of }, cmds)
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 -> EnableEditMode ->
let let
-- Lade bestehende Einträge in selectedEntries
currentWeekEntries = List.filter currentWeekEntries = List.filter
(\e -> (\e ->
let let
@ -498,7 +407,6 @@ update msg model =
preSelectedEntries = List.map preSelectedEntries = List.map
(\entry -> (\entry ->
-- Finde den dayOfWeek aus dem Datum
let let
parts = String.split "-" entry.date parts = String.split "-" entry.date
year = parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 2025 year = parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 2025
@ -793,55 +701,25 @@ getISOWeek year month day =
let let
dayOfYear = getDayOfYear year month day dayOfYear = getDayOfYear year month day
-- Wochentag des 4. Januar (definiert ISO Woche 1)
jan4DayOfWeek = getDayOfWeek year 1 4 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 mondayOfWeek1DayOfYear = 4 - jan4DayOfWeek
-- Berechne die Wochennummer
weekNum = ((dayOfYear - mondayOfWeek1DayOfYear) // 7) + 1 weekNum = ((dayOfYear - mondayOfWeek1DayOfYear) // 7) + 1
in in
if weekNum < 1 then if weekNum < 1 then
-- Gehört zur letzten Woche des Vorjahres 52
52 -- Vereinfachung: könnte auch 53 sein
else if weekNum > 52 then else if weekNum > 52 then
let let
-- Prüfe ob Jahr 53 Wochen hat
dec31DayOfWeek = getDayOfWeek year 12 31 dec31DayOfWeek = getDayOfWeek year 12 31
jan1DayOfWeek = getDayOfWeek year 1 1 jan1DayOfWeek = getDayOfWeek year 1 1
in 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 if jan1DayOfWeek == 3 || (isLeapYear year && jan1DayOfWeek == 2) then
weekNum weekNum
else else
1 1
else else
weekNum 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 =
@ -855,7 +733,6 @@ 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
@ -867,28 +744,19 @@ 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
-- 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 mondayOfWeek1Date = 4 - jan4DayOfWeek
-- Berechne den Tag: Montag Woche 1 + (Woche - 1) * 7 Tage + Wochentag
targetDayOfYear = mondayOfWeek1Date + ((week - 1) * 7) + dayOfWeek targetDayOfYear = mondayOfWeek1Date + ((week - 1) * 7) + dayOfWeek
(finalYear, finalMonth, finalDay) = (finalYear, finalMonth, finalDay) =
if targetDayOfYear < 1 then if targetDayOfYear < 1 then
-- Datum liegt im Vorjahr
addDaysToDate (year - 1) 12 31 (targetDayOfYear) addDaysToDate (year - 1) 12 31 (targetDayOfYear)
else else
addDaysToDate year 1 targetDayOfYear 0 addDaysToDate year 1 targetDayOfYear 0
@ -896,24 +764,6 @@ getDateForWeekDay year week dayOfWeek =
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 startYear startMonth startDay daysToAdd = addDaysToDate startYear startMonth startDay daysToAdd =
@ -938,7 +788,6 @@ addDaysToDate startYear startMonth startDay daysToAdd =
if remaining == 0 then if remaining == 0 then
(y, m, d) (y, m, d)
else if remaining > 0 then else if remaining > 0 then
-- Vorwärts zählen
let let
daysInCurrentMonth = daysInMonth m y daysInCurrentMonth = daysInMonth m y
daysLeftInMonth = daysInCurrentMonth - d daysLeftInMonth = daysInCurrentMonth - d
@ -950,7 +799,6 @@ addDaysToDate startYear startMonth startDay daysToAdd =
else else
helper y (m + 1) 1 (remaining - daysLeftInMonth - 1) helper y (m + 1) 1 (remaining - daysLeftInMonth - 1)
else else
-- Rückwärts zählen
if d + remaining >= 1 then if d + remaining >= 1 then
(y, m, d + remaining) (y, m, d + remaining)
else if m == 1 then else if m == 1 then
@ -965,41 +813,6 @@ addDaysToDate startYear startMonth startDay daysToAdd =
helper y (m - 1) prevMonthDays (remaining + d) helper y (m - 1) prevMonthDays (remaining + d)
in in
helper startYear startMonth startDay 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 : Int -> Int -> (Int, Int)
previousWeek year week = previousWeek year week =
@ -1143,7 +956,6 @@ viewUserDashboard model =
[ 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 , if model.hasEntriesForCurrentWeek && not model.weekEditMode then
div [ class "notification is-success" ] div [ class "notification is-success" ]
[ div [ class "level" ] [ div [ class "level" ]
@ -1193,8 +1005,7 @@ viewUserDashboard model =
[ text "Wählen Sie die Zeiten aus, die Sie in dieser Woche gearbeitet haben." ] [ text "Wählen Sie die Zeiten aus, die Sie in dieser Woche gearbeitet haben." ]
, viewScheduleGridWithWeek model , viewScheduleGridWithWeek model
, if not model.hasEntriesForCurrentWeek || model.weekEditMode then
, if model.weekEditMode || not model.hasEntriesForCurrentWeek then
div [ class "field mt-4" ] div [ class "field mt-4" ]
[ div [ class "control" ] [ div [ class "control" ]
[ button [ 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 -> Html Msg
viewScheduleGridWithWeek model = viewScheduleGridWithWeek model =
@ -1397,41 +1175,28 @@ viewDayColumnWithWeek model (dayOfWeek, schedules) =
[ 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
-- Prüfe ob dieser Eintrag bereits in der DB ist (nur relevant wenn Edit-Mode aktiv) isClickable = not model.hasEntriesForCurrentWeek || model.weekEditMode
isLocked = model.hasEntriesForCurrentWeek && not model.weekEditMode
boxClass = boxClass =
if isLocked then if isSelected then
if isSelected then "box has-background-success-light" else "box has-background-white"
else if isSelected then
"box has-background-success-light" "box has-background-success-light"
else else
"box has-background-white" "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" cursorStyle = if isClickable then "pointer" else "not-allowed"
opacity = if isLocked && not isSelected then "0.6" else "1" opacity = if isClickable || isSelected then "1" else "0.6"
in in
div div
[ class boxClass [ 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 "cursor" cursorStyle
, style "margin-bottom" "0.5rem" , style "margin-bottom" "0.5rem"
, style "padding" "0.75rem" , style "padding" "0.75rem"
@ -1443,9 +1208,6 @@ 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" ]
@ -1812,42 +1574,50 @@ fetchMyTimeEntries token =
, tracker = Nothing , tracker = Nothing
} }
saveTimeEntriesForWeek : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Cmd Msg saveTimeEntriesForWeek : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Maybe WeekDates -> Cmd Msg
saveTimeEntriesForWeek token selectedEntries year week schedules = saveTimeEntriesForWeek token selectedEntries year week schedules maybeWeekDates =
case maybeWeekDates of
Nothing ->
Cmd.none
Just weekDates ->
let let
getScheduleById id = getScheduleById id =
List.filter (\s -> s.id == id) schedules |> List.head List.filter (\s -> s.id == id) schedules |> List.head
createRequest entry = getDateForDay dayOfWeek =
case getScheduleById entry.scheduleId of weekDates.dates
Just schedule -> |> List.filter (\(day, _) -> day == String.fromInt dayOfWeek)
let |> List.head
dateStr = getDateForWeekDay year week entry.dayOfWeek |> Maybe.map Tuple.second
in
Just <| Http.request createEntryData entry =
{ method = "POST" case (getScheduleById entry.scheduleId, getDateForDay entry.dayOfWeek) of
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ] (Just schedule, Just dateStr) ->
, url = "/api/time-entries" Just <| Encode.object
, body = Http.jsonBody <|
Encode.object
[ ("schedule_id", Encode.int entry.scheduleId) [ ("schedule_id", Encode.int entry.scheduleId)
, ("date", Encode.string dateStr) , ("date", Encode.string dateStr)
, ("type", Encode.string schedule.scheduleType) , ("type", Encode.string schedule.scheduleType)
, ("start_time", Encode.string schedule.startTime) , ("start_time", Encode.string schedule.startTime)
, ("end_time", Encode.string schedule.endTime) , ("end_time", Encode.string schedule.endTime)
] ]
_ ->
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 , expect = Http.expectWhatever TimeEntriesSaved
, timeout = Nothing , timeout = Nothing
, tracker = Nothing , tracker = Nothing
} }
Nothing ->
Nothing
requests = List.filterMap createRequest selectedEntries
in
case List.head requests of
Just cmd -> cmd
Nothing -> Cmd.none
deleteWeekEntries : String -> Int -> Int -> Cmd Msg deleteWeekEntries : String -> Int -> Int -> Cmd Msg
deleteWeekEntries token year week = deleteWeekEntries token year week =