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.
496 lines
13 KiB
Go
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)
|
|
}
|