package main import ( "database/sql" "fmt" "log" "sort" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" _ "modernc.org/sqlite" ) func InitDB(filepath string) *sql.DB { db, err := sql.Open("sqlite", filepath) if err != nil { log.Fatal(err) } if err = db.Ping(); err != nil { log.Fatal(err) } createTables(db) createIndexes(db) return db } func createTables(db *sql.DB) { queries := []string{ `CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, is_admin BOOLEAN NOT NULL DEFAULT 0, yearly_hours REAL NOT NULL DEFAULT 1800.0, -- 40 Stunden/Woche * 45 Schulwochen created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, day_of_week INTEGER NOT NULL, start_time TEXT NOT NULL, end_time TEXT NOT NULL, type TEXT NOT NULL, title TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, `CREATE TABLE IF NOT EXISTS time_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, schedule_id INTEGER NOT NULL, date TEXT NOT NULL, type TEXT NOT NULL, start_time TEXT NOT NULL, end_time TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (schedule_id) REFERENCES schedules(id) )`, `CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, action TEXT NOT NULL, details TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, } for _, query := range queries { if _, err := db.Exec(query); err != nil { log.Fatal(err) } } hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost) _, err := db.Exec(` INSERT OR IGNORE INTO users (id, username, password, is_admin, yearly_hours) VALUES (?, ?, ?, ?, ?)`, 1, "admin", string(hash), true, 40.0, ) if err != nil { log.Fatal(err) } } func createIndexes(db *sql.DB) { indexes := []string{ `CREATE INDEX IF NOT EXISTS idx_time_entries_user_date ON time_entries(user_id, date)`, `CREATE INDEX IF NOT EXISTS idx_time_entries_date ON time_entries(date)`, `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)`, } for _, idx := range indexes { if _, err := db.Exec(idx); err != nil { log.Printf("Warning: Failed to create index: %v", err) } } } func GetUserByUsername(db *sql.DB, username string) (*User, error) { user := &User{} err := db.QueryRow("SELECT id, username, password, is_admin, yearly_hours FROM users WHERE username = ?", username). Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin, &user.YearlyHours) if err != nil { return nil, err } return user, nil } func GetUserByID(db *sql.DB, userID int) (*User, error) { user := &User{} err := db.QueryRow("SELECT id, username, password, is_admin, yearly_hours FROM users WHERE id = ?", userID). Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin, &user.YearlyHours) if err != nil { return nil, err } return user, nil } func CreateUser(db *sql.DB, username, hashedPassword string, isAdmin bool, yearlyHours float64) error { _, err := db.Exec("INSERT INTO users (username, password, is_admin, yearly_hours) VALUES (?, ?, ?, ?)", username, hashedPassword, isAdmin, yearlyHours) return err } func GetAllUsers(db *sql.DB) ([]User, error) { rows, err := db.Query("SELECT id, username, is_admin, yearly_hours FROM users ORDER BY username") if err != nil { return nil, err } defer rows.Close() var users []User for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Username, &u.IsAdmin, &u.YearlyHours); err != nil { continue } users = append(users, u) } return users, nil } func UpdateUser(db *sql.DB, userID int, yearlyHours float64) error { _, err := db.Exec("UPDATE users SET yearly_hours = ? WHERE id = ?", yearlyHours, userID) return err } func ResetUserPassword(db *sql.DB, userID int, hashedPassword string) error { _, err := db.Exec("UPDATE users SET password = ? WHERE id = ?", hashedPassword, userID) return err } func DeleteUser(db *sql.DB, id int) error { if id == 1 { return fmt.Errorf("cannot delete admin user") } _, err := db.Exec("DELETE FROM users WHERE id = ?", id) return err } func CreateSchedule(db *sql.DB, schedule *Schedule) error { _, err := db.Exec("INSERT INTO schedules (day_of_week, start_time, end_time, type, title) VALUES (?, ?, ?, ?, ?)", schedule.DayOfWeek, schedule.StartTime, schedule.EndTime, schedule.Type, schedule.Title) return err } func GetAllSchedules(db *sql.DB) ([]Schedule, error) { rows, err := db.Query("SELECT id, day_of_week, start_time, end_time, type, title FROM schedules ORDER BY day_of_week, start_time") if err != nil { return nil, err } defer rows.Close() var schedules []Schedule for rows.Next() { var s Schedule if err := rows.Scan(&s.ID, &s.DayOfWeek, &s.StartTime, &s.EndTime, &s.Type, &s.Title); err != nil { continue } schedules = append(schedules, s) } return schedules, nil } func DeleteSchedule(db *sql.DB, id int) error { _, err := db.Exec("DELETE FROM schedules WHERE id = ?", id) return err } func UpdateTimeEntry(db *sql.DB, entryID int, date, startTime, endTime, entryType string) error { _, err := db.Exec("UPDATE time_entries SET date = ?, start_time = ?, end_time = ?, type = ? WHERE id = ?", date, startTime, endTime, entryType, entryID) return err } func DeleteTimeEntry(db *sql.DB, entryID int) error { _, err := db.Exec("DELETE FROM time_entries WHERE id = ?", entryID) return err } func CreateTimeEntry(db *sql.DB, entry *TimeEntry) error { _, err := db.Exec("INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time) VALUES (?, ?, ?, ?, ?, ?)", entry.UserID, entry.ScheduleID, entry.Date, entry.Type, entry.StartTime, entry.EndTime) return err } func GetTimeEntriesByUser(db *sql.DB, userID int) ([]TimeEntry, error) { rows, err := db.Query(` SELECT te.id, te.user_id, te.schedule_id, te.date, te.type, te.start_time, te.end_time, te.created_at, u.username FROM time_entries te JOIN users u ON te.user_id = u.id WHERE te.user_id = ? ORDER BY te.date DESC, te.created_at DESC `, userID) if err != nil { return nil, err } defer rows.Close() var entries []TimeEntry for rows.Next() { var e TimeEntry if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.StartTime, &e.EndTime, &e.CreatedAt, &e.Username); err != nil { continue } entries = append(entries, e) } return entries, nil } func GetAllTimeEntries(db *sql.DB) ([]TimeEntry, error) { rows, err := db.Query(` SELECT te.id, te.user_id, te.schedule_id, te.date, te.type, te.start_time, te.end_time, te.created_at, u.username FROM time_entries te JOIN users u ON te.user_id = u.id ORDER BY te.date DESC, te.created_at DESC `) if err != nil { return nil, err } defer rows.Close() var entries []TimeEntry for rows.Next() { var e TimeEntry if err := rows.Scan(&e.ID, &e.UserID, &e.ScheduleID, &e.Date, &e.Type, &e.StartTime, &e.EndTime, &e.CreatedAt, &e.Username); err != nil { continue } entries = append(entries, e) } return entries, nil } func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) { rows, err := db.Query(` SELECT te.user_id, u.username, te.date, te.start_time, te.end_time, te.type, u.yearly_hours FROM time_entries te JOIN users u ON te.user_id = u.id ORDER BY te.date DESC `) if err != nil { return nil, err } defer rows.Close() hoursMap := make(map[string]*WeeklyHours) userYearlyHours := make(map[int]float64) for rows.Next() { var userID int var username, dateStr, startTime, endTime, entryType string var yearlyHours float64 if err := rows.Scan(&userID, &username, &dateStr, &startTime, &endTime, &entryType, &yearlyHours); err != nil { continue } userYearlyHours[userID] = yearlyHours t, err := time.Parse("2006-01-02", dateStr) if err != nil { continue } year, week := t.ISOWeek() var hours float64 if entryType == "lesson" { hours = 1.0 } else { hours = calculateHoursDiff(startTime, endTime) } key := fmt.Sprintf("%d_%d_%d", userID, year, week) if existing, exists := hoursMap[key]; exists { existing.TotalHours += hours } else { hoursMap[key] = &WeeklyHours{ UserID: userID, Username: username, Year: year, Week: week, TotalHours: hours, } } } yearlyTotals := make(map[int]float64) for _, h := range hoursMap { yearlyTotals[h.UserID] += h.TotalHours } for _, h := range hoursMap { h.YearlyTarget = userYearlyHours[h.UserID] h.YearlyActual = yearlyTotals[h.UserID] h.WeeklyTarget = h.YearlyTarget / 45.0 h.RemainingYearly = h.YearlyTarget - h.YearlyActual } var result []WeeklyHours for _, h := range hoursMap { result = append(result, *h) } sort.Slice(result, func(i, j int) bool { if result[i].Year != result[j].Year { return result[i].Year > result[j].Year } if result[i].Week != result[j].Week { return result[i].Week > result[j].Week } return result[i].Username < result[j].Username }) return result, nil } 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 } 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 } 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, }) } } sort.Slice(result, func(i, j int) bool { return result[i].Username < result[j].Username }) return result, nil } func calculateHoursDiff(startTime, endTime string) float64 { parseTime := func(timeStr string) float64 { parts := strings.Split(timeStr, ":") if len(parts) != 2 { return 0 } hours, err1 := strconv.ParseFloat(parts[0], 64) minutes, err2 := strconv.ParseFloat(parts[1], 64) if err1 != nil || err2 != nil { return 0 } return hours + (minutes / 60.0) } start := parseTime(startTime) end := parseTime(endTime) if end > start { return end - start } return 0 } func DeleteTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week int) error { dates := calculateWeekDates(year, week) var dateList []string for day := 0; day <= 4; day++ { dateList = append(dateList, dates.Dates[fmt.Sprintf("%d", day)]) } query := ` DELETE FROM time_entries WHERE user_id = ? AND date IN (?, ?, ?, ?, ?) ` _, err := db.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]) return err } func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (bool, error) { dates := calculateWeekDates(year, week) var dateList []string for day := 0; day <= 4; day++ { dateList = append(dateList, dates.Dates[fmt.Sprintf("%d", day)]) } query := ` SELECT COUNT(*) FROM time_entries WHERE user_id = ? AND date IN (?, ?, ?, ?, ?) ` var count int err := db.QueryRow(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count) if err != nil { log.Printf("Error checking entries: %v", err) return false, err } return count > 0, nil }