school-timetracker/backend/handlers.go

525 lines
14 KiB
Go

package main
import (
"database/sql"
"fmt"
"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 = 60.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)
}
func (app *App) GenerateYearlySummaryPDFHandler(c echo.Context) error {
isAdmin, _ := c.Get("is_admin").(bool)
if !isAdmin {
return echo.NewHTTPError(http.StatusForbidden, "Only admins can generate PDFs")
}
schoolYear, err := GetActiveSchoolYear(app.DB)
if err != nil || schoolYear == nil {
return echo.NewHTTPError(http.StatusNotFound, "No active school year found")
}
summary, err := GetYearlyHoursSummary(app.DB)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
pdfBytes, err := GenerateYearlySummaryPDF(schoolYear, summary)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate PDF: "+err.Error())
}
filename := fmt.Sprintf("Jahresuebersicht_%s.pdf", schoolYear.Name)
c.Response().Header().Set("Content-Type", "application/pdf")
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
return c.Blob(http.StatusOK, "application/pdf", pdfBytes)
}