fix: fix reset of entries when switching between weeks
This commit is contained in:
parent
c8b7666971
commit
20ba24001a
5 changed files with 184 additions and 397 deletions
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue