package main import ( "database/sql" "net/http" "strconv" "time" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" ) type App struct { DB *sql.DB } func (app *App) LoginHandler(c echo.Context) error { var req LoginRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } user, err := GetUserByUsername(app.DB, req.Username) if err != nil { return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") } token, err := createToken(user.ID, user.Username, user.IsAdmin) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "error creating token") } response := LoginResponse{ Token: token, Username: user.Username, IsAdmin: user.IsAdmin, } return c.JSON(http.StatusOK, response) } func (app *App) GetSchedulesHandler(c echo.Context) error { schedules, err := GetAllSchedules(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, schedules) } func (app *App) CreateScheduleHandler(c echo.Context) error { var schedule Schedule if err := c.Bind(&schedule); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } if err := CreateSchedule(app.DB, &schedule); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusCreated, map[string]string{"message": "schedule created"}) } func (app *App) DeleteScheduleHandler(c echo.Context) error { id, err := strconv.Atoi(c.QueryParam("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid id") } if err := DeleteSchedule(app.DB, id); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusNoContent) } func (app *App) GetYearlyHoursSummaryHandler(c echo.Context) error { hours, err := GetYearlyHoursSummary(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if hours == nil { hours = []WeeklyHours{} } return c.JSON(http.StatusOK, hours) } func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error { isAdmin, _ := c.Get("is_admin").(bool) if !isAdmin { return echo.NewHTTPError(http.StatusForbidden, "Only admins can create entries for others") } var req struct { UserID int `json:"user_id"` Date string `json:"date"` Hours float64 `json:"hours"` Type string `json:"type"` } if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } entry := TimeEntry{ UserID: req.UserID, Date: req.Date, StartTime: "00:00", EndTime: "00:00", Type: "manual", } if err := CreateManualTimeEntry(app.DB, &entry, req.Hours); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusCreated) } func (app *App) GetUsersHandler(c echo.Context) error { users, err := GetAllUsers(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if users == nil { users = []User{} } return c.JSON(http.StatusOK, users) } func (app *App) DeleteUserHandler(c echo.Context) error { id, err := strconv.Atoi(c.QueryParam("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid id") } if err := DeleteUser(app.DB, id); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusNoContent) } func (app *App) CreateTimeEntryHandler(c echo.Context) error { userID := c.Get("user_id").(int) var entry TimeEntry if err := c.Bind(&entry); err != nil { return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } entry.UserID = userID if err := CreateTimeEntry(app.DB, &entry); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusCreated, map[string]string{"message": "time entry created"}) } func (app *App) GetMyTimeEntriesHandler(c echo.Context) error { userID := c.Get("user_id").(int) entries, err := GetTimeEntriesByUser(app.DB, userID) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if entries == nil { entries = []TimeEntry{} } return c.JSON(http.StatusOK, entries) } 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) } func (app *App) CheckWeekHasEntries(c echo.Context) error { userID := c.Get("user_id").(int) year, err := strconv.Atoi(c.QueryParam("year")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid year") } week, err := strconv.Atoi(c.QueryParam("week")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid week") } hasEntries, err := CheckUserHasEntriesForWeek(app.DB, userID, year, week) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, map[string]bool{"has_entries": hasEntries}) } func (app *App) GetAllTimeEntriesHandler(c echo.Context) error { entries, err := GetAllTimeEntries(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if entries == nil { entries = []TimeEntry{} } return c.JSON(http.StatusOK, entries) } func (app *App) GetWeeklyHoursHandler(c echo.Context) error { hours, err := GetWeeklyHours(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if hours == nil { hours = []WeeklyHours{} } 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.StatusNoContent) } type WeekDates struct { Year int `json:"year"` Week int `json:"week"` Dates map[string]string `json:"dates"` Range string `json:"range"` } func calculateWeekDates(year, week int) WeekDates { jan4 := time.Date(year, time.January, 4, 0, 0, 0, 0, time.UTC) weekday := int(jan4.Weekday()) if weekday == 0 { weekday = 7 } daysToMonday := weekday - 1 mondayWeek1 := jan4.AddDate(0, 0, -daysToMonday) targetMonday := mondayWeek1.AddDate(0, 0, (week-1)*7) dates := make(map[string]string) weekDays := []string{"0", "1", "2", "3", "4"} 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, } } 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"}) } func (app *App) UpdateUserHandler(c echo.Context) error { userID, err := strconv.Atoi(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID") } var req UpdateUserRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } if err := UpdateUser(app.DB, userID, req.YearlyHours); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusOK) } func (app *App) ResetPasswordHandler(c echo.Context) error { userID, err := strconv.Atoi(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID") } var req ResetPasswordRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Error hashing password") } if err := ResetUserPassword(app.DB, userID, string(hashedPassword)); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusOK) } func (app *App) UpdateTimeEntryHandler(c echo.Context) error { entryID, err := strconv.Atoi(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid entry ID") } var req UpdateTimeEntryRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } if err := UpdateTimeEntry(app.DB, entryID, req.Date, req.StartTime, req.EndTime, req.Type); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusOK) } func (app *App) DeleteTimeEntryHandler(c echo.Context) error { entryID, err := strconv.Atoi(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid entry ID") } if err := DeleteTimeEntry(app.DB, entryID); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusNoContent) } func (app *App) GetMyInfoHandler(c echo.Context) error { userID := c.Get("user_id").(int) user, err := GetUserByID(app.DB, userID) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.JSON(http.StatusOK, user) } func (app *App) CreateUserHandler(c echo.Context) error { var req CreateUserRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Error hashing password") } if req.YearlyHours == 0 { req.YearlyHours = 1800.0 } if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin, req.YearlyHours); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusCreated) } func (app *App) GetSchoolYearsHandler(c echo.Context) error { years, err := GetAllSchoolYears(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if years == nil { years = []SchoolYear{} } return c.JSON(http.StatusOK, years) } func (app *App) CreateSchoolYearHandler(c echo.Context) error { var req CreateSchoolYearRequest if err := c.Bind(&req); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } if err := CreateSchoolYear(app.DB, req.Name, req.StartDate, req.EndDate); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusCreated) } func (app *App) SetActiveSchoolYearHandler(c echo.Context) error { id, err := strconv.Atoi(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID") } if err := SetActiveSchoolYear(app.DB, id); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } return c.NoContent(http.StatusNoContent) } func (app *App) GetActiveSchoolYearHandler(c echo.Context) error { year, err := GetActiveSchoolYear(app.DB) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } if year == nil { return c.JSON(http.StatusOK, map[string]any{"active": false}) } return c.JSON(http.StatusOK, year) }