feat: update all and add all features for version 1.0
clean up in codebase needed
This commit is contained in:
parent
20ba24001a
commit
5001cc1147
8 changed files with 1497 additions and 129 deletions
56
Dockerfile
Normal file
56
Dockerfile
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Build stage for Elm frontend
|
||||||
|
FROM node:25-alpine AS elm-build
|
||||||
|
|
||||||
|
WORKDIR /frontend
|
||||||
|
|
||||||
|
# Install Elm
|
||||||
|
RUN npm install -g elm@latest-0.19.1
|
||||||
|
|
||||||
|
# Copy Elm files
|
||||||
|
COPY frontend/elm.json .
|
||||||
|
COPY frontend/src ./src
|
||||||
|
|
||||||
|
# Build Elm app
|
||||||
|
RUN elm make src/Main.elm --optimize --output=elm.js
|
||||||
|
|
||||||
|
# Build stage for Go backend
|
||||||
|
FROM golang:1.25.3-alpine AS go-build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY backend/go.mod backend/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy backend source
|
||||||
|
COPY backend/ ./
|
||||||
|
|
||||||
|
# Build Go binary
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /root/
|
||||||
|
|
||||||
|
# Copy Go binary from build stage
|
||||||
|
COPY --from=go-build /app/main .
|
||||||
|
|
||||||
|
# Create static directory
|
||||||
|
RUN mkdir -p /root/static
|
||||||
|
|
||||||
|
# Copy Elm build artifacts
|
||||||
|
COPY --from=elm-build /frontend/elm.js /root/static/
|
||||||
|
COPY frontend/public/index.html /root/static/
|
||||||
|
|
||||||
|
# Create volume for database
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
ENV PORT=8080
|
||||||
|
ENV DB_PATH=/data/timetracking.db
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["./main"]
|
||||||
|
|
@ -4,6 +4,10 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
|
|
@ -29,7 +33,8 @@ func createTables(db *sql.DB) {
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
is_admin BOOLEAN NOT NULL DEFAULT 0
|
is_admin BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
weekly_hours REAL NOT NULL DEFAULT 40.0
|
||||||
)`,
|
)`,
|
||||||
`CREATE TABLE IF NOT EXISTS schedules (
|
`CREATE TABLE IF NOT EXISTS schedules (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
@ -61,33 +66,90 @@ func createTables(db *sql.DB) {
|
||||||
|
|
||||||
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT OR IGNORE INTO users (id, username, password, is_admin)
|
INSERT OR IGNORE INTO users (id, username, password, is_admin, weekly_hours)
|
||||||
VALUES (?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?)`,
|
||||||
1, "admin", string(hash), true,
|
1, "admin", string(hash), true, 40.0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// )`,
|
||||||
|
// `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
|
||||||
|
// )`,
|
||||||
|
// `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)
|
||||||
|
// )`,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// VALUES (?, ?, ?, ?)`,
|
||||||
|
// 1, "admin", string(hash), true,
|
||||||
|
// )
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
func GetUserByUsername(db *sql.DB, username string) (*User, error) {
|
func GetUserByUsername(db *sql.DB, username string) (*User, error) {
|
||||||
user := &User{}
|
user := &User{}
|
||||||
err := db.QueryRow("SELECT id, username, password, is_admin FROM users WHERE username = ?", username).
|
err := db.QueryRow("SELECT id, username, password, is_admin, weekly_hours FROM users WHERE username = ?", username).
|
||||||
Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
|
Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin, &user.WeeklyHours)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUser(db *sql.DB, username, hashedPassword string, isAdmin bool) error {
|
func GetUserByID(db *sql.DB, userID int) (*User, error) {
|
||||||
_, err := db.Exec("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
user := &User{}
|
||||||
username, hashedPassword, isAdmin)
|
err := db.QueryRow("SELECT id, username, password, is_admin, weekly_hours FROM users WHERE id = ?", userID).
|
||||||
|
Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin, &user.WeeklyHours)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateUser(db *sql.DB, username, hashedPassword string, isAdmin bool, weeklyHours float64) error {
|
||||||
|
_, err := db.Exec("INSERT INTO users (username, password, is_admin, weekly_hours) VALUES (?, ?, ?, ?)",
|
||||||
|
username, hashedPassword, isAdmin, weeklyHours)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllUsers(db *sql.DB) ([]User, error) {
|
func GetAllUsers(db *sql.DB) ([]User, error) {
|
||||||
rows, err := db.Query("SELECT id, username, is_admin FROM users")
|
rows, err := db.Query("SELECT id, username, is_admin, weekly_hours FROM users")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +158,7 @@ func GetAllUsers(db *sql.DB) ([]User, error) {
|
||||||
var users []User
|
var users []User
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var u User
|
var u User
|
||||||
if err := rows.Scan(&u.ID, &u.Username, &u.IsAdmin); err != nil {
|
if err := rows.Scan(&u.ID, &u.Username, &u.IsAdmin, &u.WeeklyHours); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
users = append(users, u)
|
users = append(users, u)
|
||||||
|
|
@ -104,6 +166,59 @@ func GetAllUsers(db *sql.DB) ([]User, error) {
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// func GetUserByUsername(db *sql.DB, username string) (*User, error) {
|
||||||
|
// user := &User{}
|
||||||
|
// err := db.QueryRow("SELECT id, username, password, is_admin FROM users WHERE username = ?", username).
|
||||||
|
// Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return user, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func CreateUser(db *sql.DB, username, hashedPassword string, isAdmin bool) error {
|
||||||
|
// _, err := db.Exec("INSERT INTO users (username, password, is_admin) VALUES (?, ?, ?)",
|
||||||
|
// username, hashedPassword, isAdmin)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func GetAllUsers(db *sql.DB) ([]User, error) {
|
||||||
|
// rows, err := db.Query("SELECT id, username, is_admin FROM users")
|
||||||
|
// 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); err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// users = append(users, u)
|
||||||
|
// }
|
||||||
|
// return users, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func UpdateUser(db *sql.DB, userID int, weeklyHours float64) error {
|
||||||
|
_, err := db.Exec("UPDATE users SET weekly_hours = ? WHERE id = ?",
|
||||||
|
weeklyHours, 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 {
|
func CreateSchedule(db *sql.DB, schedule *Schedule) error {
|
||||||
_, err := db.Exec("INSERT INTO schedules (day_of_week, start_time, end_time, type, title) VALUES (?, ?, ?, ?, ?)",
|
_, 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)
|
schedule.DayOfWeek, schedule.StartTime, schedule.EndTime, schedule.Type, schedule.Title)
|
||||||
|
|
@ -133,6 +248,17 @@ func DeleteSchedule(db *sql.DB, id int) error {
|
||||||
return err
|
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 {
|
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 (?, ?, ?, ?, ?, ?)",
|
_, 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)
|
entry.UserID, entry.ScheduleID, entry.Date, entry.Type, entry.StartTime, entry.EndTime)
|
||||||
|
|
@ -191,49 +317,185 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
SELECT
|
SELECT
|
||||||
te.user_id,
|
te.user_id,
|
||||||
u.username,
|
u.username,
|
||||||
-- ISO 8601 Wochennummer berechnen
|
te.date,
|
||||||
CASE
|
te.start_time,
|
||||||
WHEN strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days')) <= '3'
|
te.end_time,
|
||||||
THEN CAST(strftime('%Y', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days', '-4 days')) AS INTEGER)
|
te.type,
|
||||||
ELSE CAST(strftime('%Y', te.date) AS INTEGER)
|
u.weekly_hours
|
||||||
END as year,
|
|
||||||
CASE
|
|
||||||
WHEN strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days')) <= '3'
|
|
||||||
THEN CAST((strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days', '-4 days')) - 1) / 7 AS INTEGER) + 53
|
|
||||||
ELSE CAST((strftime('%j', date(te.date, '-' || ((strftime('%w', te.date) + 6) % 7) || ' days')) - 1) / 7 AS INTEGER) + 1
|
|
||||||
END as week,
|
|
||||||
SUM(
|
|
||||||
(CAST(substr(te.end_time, 1, 2) AS REAL) + CAST(substr(te.end_time, 4, 2) AS REAL) / 60.0) -
|
|
||||||
(CAST(substr(te.start_time, 1, 2) AS REAL) + CAST(substr(te.start_time, 4, 2) AS REAL) / 60.0)
|
|
||||||
) as total_hours
|
|
||||||
FROM time_entries te
|
FROM time_entries te
|
||||||
JOIN users u ON te.user_id = u.id
|
JOIN users u ON te.user_id = u.id
|
||||||
GROUP BY te.user_id, u.username, week, year
|
ORDER BY te.date DESC
|
||||||
ORDER BY year DESC, week DESC, u.username
|
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
var hours []WeeklyHours
|
hoursMap := make(map[string]*WeeklyHours)
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var h WeeklyHours
|
var userID int
|
||||||
if err := rows.Scan(&h.UserID, &h.Username, &h.Year, &h.Week, &h.TotalHours); err != nil {
|
var username, dateStr, startTime, endTime, entryType string
|
||||||
|
var expectedWeeklyHours float64
|
||||||
|
|
||||||
|
if err := rows.Scan(&userID, &username, &dateStr, &startTime, &endTime, &entryType, &expectedWeeklyHours); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
hours = append(hours, h)
|
|
||||||
}
|
t, err := time.Parse("2006-01-02", dateStr)
|
||||||
return hours, nil
|
if err != nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteUser(db *sql.DB, id int) error {
|
year, week := t.ISOWeek()
|
||||||
if id == 1 {
|
|
||||||
return fmt.Errorf("cannot delete admin user")
|
var hours float64
|
||||||
|
if entryType == "lesson" {
|
||||||
|
hours = 1.0
|
||||||
|
} else {
|
||||||
|
hours = calculateHoursDiff(startTime, endTime)
|
||||||
}
|
}
|
||||||
_, err := db.Exec("DELETE FROM users WHERE id = ?", id)
|
|
||||||
return err
|
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,
|
||||||
|
ExpectedHours: expectedWeeklyHours,
|
||||||
|
RemainingHours: expectedWeeklyHours - hours,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range hoursMap {
|
||||||
|
h.RemainingHours = h.ExpectedHours - h.TotalHours
|
||||||
|
}
|
||||||
|
|
||||||
|
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 GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
|
// rows, err := db.Query(`
|
||||||
|
// SELECT
|
||||||
|
// te.user_id,
|
||||||
|
// u.username,
|
||||||
|
// te.date,
|
||||||
|
// te.start_time,
|
||||||
|
// te.end_time
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// for rows.Next() {
|
||||||
|
// var userID int
|
||||||
|
// var username, dateStr, startTime, endTime string
|
||||||
|
|
||||||
|
// if err := rows.Scan(&userID, &username, &dateStr, &startTime, &endTime); err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// t, err := time.Parse("2006-01-02", dateStr)
|
||||||
|
// if err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// year, week := t.ISOWeek()
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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 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 DeleteTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week int) error {
|
func DeleteTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week int) error {
|
||||||
dates := calculateWeekDates(year, week)
|
dates := calculateWeekDates(year, week)
|
||||||
|
|
|
||||||
|
|
@ -79,24 +79,24 @@ func (app *App) DeleteScheduleHandler(c echo.Context) error {
|
||||||
return c.NoContent(http.StatusOK)
|
return c.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// User Handlers
|
// // User Handlers
|
||||||
func (app *App) CreateUserHandler(c echo.Context) error {
|
// func (app *App) CreateUserHandler(c echo.Context) error {
|
||||||
var req CreateUserRequest
|
// var req CreateUserRequest
|
||||||
if err := c.Bind(&req); err != nil {
|
// if err := c.Bind(&req); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
// return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
||||||
}
|
// }
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
// hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "error hashing password")
|
// return echo.NewHTTPError(http.StatusInternalServerError, "error hashing password")
|
||||||
}
|
// }
|
||||||
|
|
||||||
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil {
|
// if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
// return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
// }
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, map[string]string{"message": "user created"})
|
// return c.JSON(http.StatusCreated, map[string]string{"message": "user created"})
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (app *App) GetUsersHandler(c echo.Context) error {
|
func (app *App) GetUsersHandler(c echo.Context) error {
|
||||||
users, err := GetAllUsers(app.DB)
|
users, err := GetAllUsers(app.DB)
|
||||||
|
|
@ -308,3 +308,137 @@ func (app *App) CreateBatchTimeEntriesHandler(c echo.Context) error {
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, map[string]string{"message": "entries created"})
|
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.WeeklyHours); 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.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetMyWeeklySummaryHandler(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")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := GetUserByID(app.DB, userID)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
allHours, err := GetWeeklyHours(app.DB)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, h := range allHours {
|
||||||
|
if h.UserID == userID && h.Year == year && h.Week == week {
|
||||||
|
return c.JSON(http.StatusOK, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, WeeklyHours{
|
||||||
|
UserID: userID,
|
||||||
|
Username: user.Username,
|
||||||
|
Year: year,
|
||||||
|
Week: week,
|
||||||
|
TotalHours: 0,
|
||||||
|
ExpectedHours: user.WeeklyHours,
|
||||||
|
RemainingHours: user.WeeklyHours,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WeeklyHours == 0 {
|
||||||
|
req.WeeklyHours = 40.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin, req.WeeklyHours); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ func main() {
|
||||||
protected.DELETE("/my-time-entries/week", app.DeleteWeekEntries)
|
protected.DELETE("/my-time-entries/week", app.DeleteWeekEntries)
|
||||||
protected.GET("/week-dates", app.GetWeekDates)
|
protected.GET("/week-dates", app.GetWeekDates)
|
||||||
protected.GET("/week-has-entries", app.CheckWeekHasEntries)
|
protected.GET("/week-has-entries", app.CheckWeekHasEntries)
|
||||||
|
protected.GET("/my-weekly-summary", app.GetMyWeeklySummaryHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
admin := e.Group("/api/admin")
|
admin := e.Group("/api/admin")
|
||||||
|
|
@ -57,6 +58,10 @@ func main() {
|
||||||
admin.DELETE("/users/delete", app.DeleteUserHandler)
|
admin.DELETE("/users/delete", app.DeleteUserHandler)
|
||||||
admin.GET("/time-entries", app.GetAllTimeEntriesHandler)
|
admin.GET("/time-entries", app.GetAllTimeEntriesHandler)
|
||||||
admin.GET("/weekly-hours", app.GetWeeklyHoursHandler)
|
admin.GET("/weekly-hours", app.GetWeeklyHoursHandler)
|
||||||
|
admin.PUT("/users/:id", app.UpdateUserHandler)
|
||||||
|
admin.POST("/users/:id/reset-password", app.ResetPasswordHandler)
|
||||||
|
admin.PUT("/time-entries/:id", app.UpdateTimeEntryHandler)
|
||||||
|
admin.DELETE("/time-entries/:id", app.DeleteTimeEntryHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Static("/", "./static")
|
e.Static("/", "./static")
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ type WeeklyHours struct {
|
||||||
Week int `json:"week"`
|
Week int `json:"week"`
|
||||||
Year int `json:"year"`
|
Year int `json:"year"`
|
||||||
TotalHours float64 `json:"total_hours"`
|
TotalHours float64 `json:"total_hours"`
|
||||||
|
ExpectedHours float64 `json:"expected_hours"`
|
||||||
|
RemainingHours float64 `json:"remaining_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
|
@ -27,6 +29,7 @@ type User struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
WeeklyHours float64 `json:"weekly_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Schedule struct {
|
type Schedule struct {
|
||||||
|
|
@ -53,9 +56,25 @@ type CreateUserRequest struct {
|
||||||
Username string `json:"username" validate:"required"`
|
Username string `json:"username" validate:"required"`
|
||||||
Password string `json:"password" validate:"required,min=6"`
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
WeeklyHours float64 `json:"weekly_hours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
WeeklyHours float64 `json:"weekly_hours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetPasswordRequest struct {
|
||||||
|
NewPassword string `json:"new_password" validate:"required,min=6"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTimeEntryRequest struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
StartTime string `json:"start_time"`
|
||||||
|
EndTime string `json:"end_time"`
|
||||||
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claims für JWT
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
|
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# version: '3.8'
|
||||||
|
|
||||||
|
# services:
|
||||||
|
# timetracking:
|
||||||
|
# build: .
|
||||||
|
# container_name: school-timetracking
|
||||||
|
# ports:
|
||||||
|
# - "8080:8080"
|
||||||
|
# volumes:
|
||||||
|
# - ./data:/data
|
||||||
|
# environment:
|
||||||
|
# - PORT=8080
|
||||||
|
# - DB_PATH=/data/timetracking.db
|
||||||
|
# restart: unless-stopped
|
||||||
|
services:
|
||||||
|
timetracking:
|
||||||
|
build: .
|
||||||
|
container_name: school-timetracking
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- DB_PATH=/data/timetracking.db
|
||||||
|
volumes:
|
||||||
|
- timetracking-data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- timetracking-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
timetracking-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
timetracking-net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
36
frontend/public/index.html
Normal file
36
frontend/public/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Schulzeit Erfassung</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="/elm.js"></script>
|
||||||
|
<script>
|
||||||
|
var storedToken = localStorage.getItem('authToken');
|
||||||
|
|
||||||
|
var app = Elm.Main.init({
|
||||||
|
node: document.getElementById('app'),
|
||||||
|
flags: storedToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save token to localStorage
|
||||||
|
app.ports.saveToken.subscribe(function(token) {
|
||||||
|
localStorage.setItem('authToken', token);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove token from localStorage
|
||||||
|
app.ports.removeToken.subscribe(function() {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue