school-timetracker/backend/handlers.go
Patryk Hegenberg 84def05c50 feat: change Manual time entry to work with hours instead of start and end time
according to add hours as well the logic was changed to accept ours for
manual entries instead of start and end time. This allows to add
negative numbers as well, which are added to working time.
2025-11-08 11:27:42 +01:00

496 lines
13 KiB
Go

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)
}