feat: add schoolyear based calculation

This commit is contained in:
Patryk Hegenberg 2025-11-06 07:18:23 +01:00
parent e65ba85c43
commit c07019e3eb
5 changed files with 675 additions and 44 deletions

View file

@ -66,6 +66,14 @@ func createTables(db *sql.DB) {
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS school_years (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
}
for _, query := range queries {
@ -92,6 +100,8 @@ func createIndexes(db *sql.DB) {
`CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id)`,
`CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)`,
`CREATE INDEX IF NOT EXISTS idx_schedules_day ON schedules(day_of_week)`,
`CREATE INDEX IF NOT EXISTS idx_school_years_active ON school_years(is_active)`,
`CREATE INDEX IF NOT EXISTS idx_school_years_dates ON school_years(start_date, end_date)`,
}
for _, idx := range indexes {
@ -349,56 +359,56 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
return result, nil
}
func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
users, err := GetAllUsers(db)
if err != nil {
return nil, err
}
// func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
// users, err := GetAllUsers(db)
// if err != nil {
// return nil, err
// }
entries, err := GetAllTimeEntries(db)
if err != nil {
return nil, err
}
// entries, err := GetAllTimeEntries(db)
// if err != nil {
// return nil, err
// }
userTotals := make(map[int]float64)
usernames := make(map[int]string)
// userTotals := make(map[int]float64)
// usernames := make(map[int]string)
for _, entry := range entries {
var hours float64
if entry.Type == "lesson" {
hours = 1.0
} else {
hours = calculateHoursDiff(entry.StartTime, entry.EndTime)
}
userTotals[entry.UserID] += hours
usernames[entry.UserID] = entry.Username
}
// for _, entry := range entries {
// var hours float64
// if entry.Type == "lesson" {
// hours = 1.0
// } else {
// hours = calculateHoursDiff(entry.StartTime, entry.EndTime)
// }
// userTotals[entry.UserID] += hours
// usernames[entry.UserID] = entry.Username
// }
var result []WeeklyHours
for _, user := range users {
if !user.IsAdmin {
total := userTotals[user.ID]
remaining := user.YearlyHours - total
// var result []WeeklyHours
// for _, user := range users {
// if !user.IsAdmin {
// total := userTotals[user.ID]
// remaining := user.YearlyHours - total
result = append(result, WeeklyHours{
UserID: user.ID,
Username: user.Username,
Year: time.Now().Year(),
Week: 0,
TotalHours: total,
YearlyTarget: user.YearlyHours,
YearlyActual: total,
RemainingYearly: remaining,
})
}
}
// result = append(result, WeeklyHours{
// UserID: user.ID,
// Username: user.Username,
// Year: time.Now().Year(),
// Week: 0,
// TotalHours: total,
// YearlyTarget: user.YearlyHours,
// YearlyActual: total,
// RemainingYearly: remaining,
// })
// }
// }
sort.Slice(result, func(i, j int) bool {
return result[i].Username < result[j].Username
})
// sort.Slice(result, func(i, j int) bool {
// return result[i].Username < result[j].Username
// })
return result, nil
}
// return result, nil
// }
func calculateHoursDiff(startTime, endTime string) float64 {
parseTime := func(timeStr string) float64 {
@ -469,3 +479,130 @@ func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (boo
return count > 0, nil
}
func GetActiveSchoolYear(db *sql.DB) (*SchoolYear, error) {
var sy SchoolYear
err := db.QueryRow(`
SELECT id, name, start_date, end_date, is_active, created_at
FROM school_years
WHERE is_active = 1
`).Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt)
if err == sql.ErrNoRows {
return nil, nil // Kein aktives Schuljahr
}
return &sy, err
}
func GetAllSchoolYears(db *sql.DB) ([]SchoolYear, error) {
rows, err := db.Query(`
SELECT id, name, start_date, end_date, is_active, created_at
FROM school_years
ORDER BY start_date DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
years := []SchoolYear{}
for rows.Next() {
var sy SchoolYear
if err := rows.Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt); err != nil {
continue
}
years = append(years, sy)
}
return years, rows.Err()
}
func CreateSchoolYear(db *sql.DB, name, startDate, endDate string) error {
_, err := db.Exec(`
INSERT INTO school_years (name, start_date, end_date, is_active)
VALUES (?, ?, ?, 0)
`, name, startDate, endDate)
return err
}
func SetActiveSchoolYear(db *sql.DB, id int) error {
tx, err := db.Begin()
if err != nil {
return err
}
if _, err := tx.Exec("UPDATE school_years SET is_active = 0"); err != nil {
tx.Rollback()
return err
}
if _, err := tx.Exec("UPDATE school_years SET is_active = 1 WHERE id = ?", id); err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
schoolYear, err := GetActiveSchoolYear(db)
if err != nil || schoolYear == nil {
return []WeeklyHours{}, err
}
users, err := GetAllUsers(db)
if err != nil {
return []WeeklyHours{}, err
}
rows, err := db.Query(`
SELECT user_id, date, start_time, end_time, type
FROM time_entries
WHERE date >= ? AND date <= ?
ORDER BY date DESC
`, schoolYear.StartDate, schoolYear.EndDate)
if err != nil {
return []WeeklyHours{}, err
}
defer rows.Close()
userTotals := make(map[int]float64)
for rows.Next() {
var userID int
var date, startTime, endTime, entryType string
if err := rows.Scan(&userID, &date, &startTime, &endTime, &entryType); err != nil {
continue
}
var hours float64
if entryType == "lesson" {
hours = 1.0
} else {
hours = calculateHoursDiff(startTime, endTime)
}
userTotals[userID] += hours
}
var result []WeeklyHours
for _, user := range users {
if !user.IsAdmin {
total := userTotals[user.ID]
remaining := user.YearlyHours - total
result = append(result, WeeklyHours{
UserID: user.ID,
Username: user.Username,
Year: 0,
Week: 0,
TotalHours: total,
YearlyTarget: user.YearlyHours,
YearlyActual: total,
RemainingYearly: remaining,
})
}
}
return result, nil
}

View file

@ -448,3 +448,51 @@ func (app *App) CreateUserHandler(c echo.Context) 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)
}

View file

@ -46,6 +46,7 @@ func main() {
protected.GET("/week-has-entries", app.CheckWeekHasEntries)
protected.GET("/yearly-hours-summary", app.GetYearlyHoursSummaryHandler)
protected.GET("/my-info", app.GetMyInfoHandler)
protected.GET("/school-year/active", app.GetActiveSchoolYearHandler)
}
admin := e.Group("/api/admin")
@ -64,6 +65,9 @@ func main() {
admin.PUT("/time-entries/:id", app.UpdateTimeEntryHandler)
admin.DELETE("/time-entries/:id", app.DeleteTimeEntryHandler)
admin.POST("/time-entry", app.AdminCreateTimeEntryHandler)
admin.GET("/school-years", app.GetSchoolYearsHandler)
admin.POST("/school-years", app.CreateSchoolYearHandler)
admin.PUT("/school-years/:id/activate", app.SetActiveSchoolYearHandler)
}
e.Static("/", "./static")

View file

@ -61,6 +61,21 @@ type CreateUserRequest struct {
YearlyHours float64 `json:"yearly_hours"`
}
type SchoolYear struct {
ID int `json:"id"`
Name string `json:"name"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
}
type CreateSchoolYearRequest struct {
Name string `json:"name" validate:"required"`
StartDate string `json:"start_date" validate:"required"`
EndDate string `json:"end_date" validate:"required"`
}
type UpdateUserRequest struct {
Username string `json:"username"`
YearlyHours float64 `json:"yearly_hours"`