school-timetracker/backend/database.go

842 lines
20 KiB
Go

package main
import (
"database/sql"
"fmt"
"log"
"os"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
_ "modernc.org/sqlite"
)
func InitDB(filepath string) *sql.DB {
dsn := filepath + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=synchronous(NORMAL)"
db, err := sql.Open("sqlite", dsn)
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(time.Hour)
if err = db.Ping(); err != nil {
log.Fatal(err)
}
createTables(db)
createIndexes(db)
ensureAdminExists(db)
return db
}
func ensureAdminExists(db *sql.DB) {
var count int
db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
if count == 0 {
var pw []byte
if os.Getenv("INITIAL_ADMIN_PASSWORD") == "" {
log.Println("Keine Benutzer gefunden. Erstelle Standard-Admin...")
pw, _ = bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
} else {
initialPassword := os.Getenv("INITIAL_ADMIN_PASSWORD")
pw, _ = bcrypt.GenerateFromPassword([]byte(initialPassword), bcrypt.DefaultCost)
}
_, err := db.Exec("INSERT INTO users (username, password, is_admin, yearly_hours) VALUES (?, ?, ?, ?)",
"admin", string(pw), true, 0)
if err != nil {
log.Printf("Fehler beim Erstellen des Admins: %v", err)
} else {
log.Println("Admin erstellt. User: 'admin', Pass: 'admin123'")
}
}
}
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 60.0,
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),
FOREIGN KEY(schedule_id) REFERENCES schedules(id)
)`,
`CREATE TABLE IF NOT EXISTS school_years (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE TABLE IF NOT EXISTS substitutions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
title TEXT NOT NULL,
notes TEXT,
taken_by_user_id INTEGER,
schedule_id INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(taken_by_user_id) REFERENCES users(id),
FOREIGN KEY(schedule_id) REFERENCES schedules(id)
)`,
}
for _, query := range queries {
_, err := db.Exec(query)
if err != nil {
log.Fatalf("Error creating table: %s\nQuery: %s", err, query)
}
}
}
func createIndexes(db *sql.DB) {
indexes := []string{
"CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username)",
"CREATE INDEX IF NOT EXISTS idx_time_entries_user_date ON time_entries(user_id, date)",
"CREATE INDEX IF NOT EXISTS idx_schedules_day ON schedules(day_of_week)",
`CREATE INDEX IF NOT EXISTS idx_substitutions_date ON substitutions(date)`,
}
for _, idx := range indexes {
db.Exec(idx)
}
}
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()
entry := TimeEntry{
Type: entryType,
StartTime: startTime,
EndTime: endTime,
}
hours := calculateHours(entry)
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 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
}
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
}
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
}
entry := TimeEntry{
Type: entryType,
StartTime: startTime,
EndTime: endTime,
}
hours := calculateHours(entry)
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
}
func CreateManualTimeEntry(db *sql.DB, entry *TimeEntry, hours float64) error {
entry.StartTime = fmt.Sprintf("%.2f", hours)
entry.EndTime = "manual"
entry.Type = "manual"
_, err := db.Exec(`
INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time)
VALUES (?, 0, ?, ?, ?, ?)
`, entry.UserID, entry.Date, entry.Type, entry.StartTime, entry.EndTime)
return err
}
func calculateHours(entry TimeEntry) float64 {
switch entry.Type {
case "lesson":
return 1.0
case "manual":
hours, err := strconv.ParseFloat(entry.StartTime, 64)
if err != nil {
return 0
}
return hours
default:
return calculateHoursDiff(entry.StartTime, entry.EndTime)
}
}
func DeleteSchoolYear(db *sql.DB, id int) error {
var isActive bool
err := db.QueryRow("SELECT is_active FROM school_years WHERE id = ?", id).Scan(&isActive)
if err != nil {
return err
}
if isActive {
return fmt.Errorf("cannot delete active school year")
}
result, err := db.Exec("DELETE FROM school_years WHERE id = ? AND is_active = 0", id)
if err != nil {
return err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}
func DeleteNonManualTimeEntriesByUserAndWeek(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.Sprint(day)])
}
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec(`
UPDATE substitutions
SET taken_by_user_id = NULL
WHERE taken_by_user_id = ?
AND date IN (?, ?, ?, ?, ?)
`, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
if err != nil {
return err
}
query := `DELETE FROM time_entries
WHERE user_id = ?
AND type != 'manual'
AND date IN (?, ?, ?, ?, ?)`
_, err = tx.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
if err != nil {
return err
}
return tx.Commit()
}
func CreateSubstitution(db *sql.DB, date, start, end, title, notes string, scheduleID int) error {
_, err := db.Exec(`
INSERT INTO substitutions (date, start_time, end_time, title, notes, schedule_id)
VALUES (?, ?, ?, ?, ?, ?)
`, date, start, end, title, notes, scheduleID)
return err
}
func GetOpenSubstitutions(db *sql.DB) ([]Substitution, error) {
today := time.Now().Format("2006-01-02")
rows, err := db.Query(`
SELECT id, date, start_time, end_time, title, notes, schedule_id, created_at
FROM substitutions
WHERE taken_by_user_id IS NULL
AND date >= ?
ORDER BY date ASC, start_time ASC
`, today)
if err != nil {
return nil, err
}
defer rows.Close()
var subs []Substitution
for rows.Next() {
var s Substitution
if err := rows.Scan(&s.ID, &s.Date, &s.StartTime, &s.EndTime, &s.Title, &s.Notes, &s.ScheduleID, &s.CreatedAt); err != nil {
continue
}
subs = append(subs, s)
}
return subs, nil
}
func GetAllSubstitutions(db *sql.DB) ([]Substitution, error) {
rows, err := db.Query(`
SELECT
s.id,
s.date,
s.start_time,
s.end_time,
s.title,
s.notes,
s.schedule_id,
s.created_at,
s.taken_by_user_id,
u.username
FROM substitutions s
LEFT JOIN users u ON s.taken_by_user_id = u.id
ORDER BY s.date DESC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var subs []Substitution
for rows.Next() {
var s Substitution
var takenID sql.NullInt64
var takenName sql.NullString
if err := rows.Scan(
&s.ID,
&s.Date,
&s.StartTime,
&s.EndTime,
&s.Title,
&s.Notes,
&s.ScheduleID,
&s.CreatedAt,
&takenID,
&takenName,
); err != nil {
continue
}
if takenID.Valid {
id := int(takenID.Int64)
s.TakenByUserID = &id
s.TakenByUsername = takenName.String
}
subs = append(subs, s)
}
return subs, nil
}
func DeleteSubstitution(db *sql.DB, id int) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var takenByUserID sql.NullInt64
var scheduleID int
var date string
err = tx.QueryRow(`
SELECT taken_by_user_id, schedule_id, date
FROM substitutions
WHERE id = ?
`, id).Scan(&takenByUserID, &scheduleID, &date)
if err != nil {
if err == sql.ErrNoRows {
return nil
}
return err
}
if takenByUserID.Valid {
userID := int(takenByUserID.Int64)
_, err = tx.Exec(`
DELETE FROM time_entries
WHERE user_id = ? AND schedule_id = ? AND date = ?
`, userID, scheduleID, date)
if err != nil {
return err
}
}
_, err = tx.Exec("DELETE FROM substitutions WHERE id = ?", id)
if err != nil {
return err
}
return tx.Commit()
}
func AcceptSubstitution(db *sql.DB, substitutionID int, userID int) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
var currentDate, start, end string
var scheduleID int
var scheduleType string
err = tx.QueryRow(`
SELECT s.date, s.start_time, s.end_time, s.schedule_id, sch.type
FROM substitutions s
JOIN schedules sch ON s.schedule_id = sch.id
WHERE s.id = ? AND s.taken_by_user_id IS NULL
`, substitutionID).Scan(&currentDate, &start, &end, &scheduleID, &scheduleType)
if err == sql.ErrNoRows {
return fmt.Errorf("Vertretung wurde bereits vergeben oder existiert nicht")
}
if err != nil {
return err
}
_, err = tx.Exec(`UPDATE substitutions SET taken_by_user_id = ? WHERE id = ?`, userID, substitutionID)
if err != nil {
return err
}
_, err = tx.Exec(`
INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time)
VALUES (?, ?, ?, ?, ?, ?)
`, userID, scheduleID, currentDate, scheduleType, start, end)
if err != nil {
return err
}
return tx.Commit()
}