feat: initial project commit
This commit is contained in:
commit
2c4fc7869a
10 changed files with 1632 additions and 0 deletions
178
backend/database.go
Normal file
178
backend/database.go
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
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
|
||||||
|
)`,
|
||||||
|
`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,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (schedule_id) REFERENCES schedules(id)
|
||||||
|
)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, query := range queries {
|
||||||
|
if _, err := db.Exec(query); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create default admin user (password: admin123)
|
||||||
|
// Hash generated with bcrypt
|
||||||
|
hash, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||||
|
fmt.Println(string(hash))
|
||||||
|
_, 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) {
|
||||||
|
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 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 CreateTimeEntry(db *sql.DB, entry *TimeEntry) error {
|
||||||
|
_, err := db.Exec("INSERT INTO time_entries (user_id, schedule_id, date, type) VALUES (?, ?, ?, ?)",
|
||||||
|
entry.UserID, entry.ScheduleID, entry.Date, entry.Type)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTimeEntriesByUser(db *sql.DB, userID int) ([]TimeEntry, error) {
|
||||||
|
rows, err := db.Query("SELECT id, user_id, schedule_id, date, type, created_at FROM time_entries WHERE user_id = ? ORDER BY date DESC, 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.CreatedAt); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllTimeEntries(db *sql.DB) ([]TimeEntry, error) {
|
||||||
|
rows, err := db.Query("SELECT id, user_id, schedule_id, date, type, created_at FROM time_entries ORDER BY date DESC, 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.CreatedAt); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
21
backend/go.mod
Normal file
21
backend/go.mod
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
module school-timetracker
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/crypto v0.43.0
|
||||||
|
modernc.org/sqlite v1.40.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
modernc.org/libc v1.66.10 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
51
backend/go.sum
Normal file
51
backend/go.sum
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||||
|
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||||
|
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||||
|
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||||
|
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||||
|
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ=
|
||||||
|
modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
172
backend/handlers.go
Normal file
172
backend/handlers.go
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := GetUserByUsername(app.DB, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||||
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createToken(user.ID, user.Username, user.IsAdmin)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error creating token", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
Username: user.Username,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetSchedulesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
schedules, err := GetAllSchedules(app.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(schedules)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) CreateScheduleHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var schedule Schedule
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&schedule); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CreateSchedule(app.DB, &schedule); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) DeleteScheduleHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := r.URL.Query().Get("id")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DeleteSchedule(app.DB, id); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) CreateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Error hashing password", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetUsersHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
users, err := GetAllUsers(app.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) CreateTimeEntryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userIDStr := r.Header.Get("X-User-ID")
|
||||||
|
userID, _ := strconv.Atoi(userIDStr)
|
||||||
|
|
||||||
|
var entry TimeEntry
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.UserID = userID
|
||||||
|
|
||||||
|
if err := CreateTimeEntry(app.DB, &entry); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetMyTimeEntriesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userIDStr := r.Header.Get("X-User-ID")
|
||||||
|
userID, _ := strconv.Atoi(userIDStr)
|
||||||
|
|
||||||
|
entries, err := GetTimeEntriesByUser(app.DB, userID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) GetAllTimeEntriesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
entries, err := GetAllTimeEntries(app.DB)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(entries)
|
||||||
|
}
|
||||||
46
backend/main.go
Normal file
46
backend/main.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dbPath := os.Getenv("DB_PATH")
|
||||||
|
if dbPath == "" {
|
||||||
|
dbPath = "./timetracking.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
db := InitDB(dbPath)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
app := &App{DB: db}
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
http.HandleFunc("/api/login", CORS(app.LoginHandler))
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
http.HandleFunc("/api/schedules", CORS(AuthMiddleware(app.GetSchedulesHandler)))
|
||||||
|
http.HandleFunc("/api/time-entries", CORS(AuthMiddleware(app.CreateTimeEntryHandler)))
|
||||||
|
http.HandleFunc("/api/my-time-entries", CORS(AuthMiddleware(app.GetMyTimeEntriesHandler)))
|
||||||
|
|
||||||
|
// Admin routes
|
||||||
|
http.HandleFunc("/api/admin/schedules", CORS(AdminMiddleware(app.CreateScheduleHandler)))
|
||||||
|
http.HandleFunc("/api/admin/schedules/delete", CORS(AdminMiddleware(app.DeleteScheduleHandler)))
|
||||||
|
http.HandleFunc("/api/admin/users", CORS(AdminMiddleware(app.CreateUserHandler)))
|
||||||
|
http.HandleFunc("/api/admin/users/list", CORS(AdminMiddleware(app.GetUsersHandler)))
|
||||||
|
http.HandleFunc("/api/admin/time-entries", CORS(AdminMiddleware(app.GetAllTimeEntriesHandler)))
|
||||||
|
|
||||||
|
// Serve frontend
|
||||||
|
fs := http.FileServer(http.Dir("./static"))
|
||||||
|
http.Handle("/", fs)
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Server starting on port %s", port)
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||||
|
}
|
||||||
125
backend/middleware.go
Normal file
125
backend/middleware.go
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jwtSecret = []byte("your-secret-key-change-in-production")
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createToken(userID int, username string, isAdmin bool) (string, error) {
|
||||||
|
claims := Claims{
|
||||||
|
UserID: userID,
|
||||||
|
Username: username,
|
||||||
|
IsAdmin: isAdmin,
|
||||||
|
Exp: time.Now().Add(24 * time.Hour).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(claims)
|
||||||
|
payloadEncoded := base64.RawURLEncoding.EncodeToString(payload)
|
||||||
|
|
||||||
|
message := header + "." + payloadEncoded
|
||||||
|
|
||||||
|
h := hmac.New(sha256.New, jwtSecret)
|
||||||
|
h.Write([]byte(message))
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
return message + "." + signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyToken(tokenString string) (*Claims, error) {
|
||||||
|
parts := strings.Split(tokenString, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid token format")
|
||||||
|
}
|
||||||
|
|
||||||
|
message := parts[0] + "." + parts[1]
|
||||||
|
h := hmac.New(sha256.New, jwtSecret)
|
||||||
|
h.Write([]byte(message))
|
||||||
|
expectedSignature := base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
|
||||||
|
if parts[2] != expectedSignature {
|
||||||
|
return nil, fmt.Errorf("invalid signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims Claims
|
||||||
|
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Unix() > claims.Exp {
|
||||||
|
return nil, fmt.Errorf("token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
claims, err := verifyToken(tokenString)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("X-User-ID", fmt.Sprintf("%d", claims.UserID))
|
||||||
|
r.Header.Set("X-Username", claims.Username)
|
||||||
|
r.Header.Set("X-Is-Admin", fmt.Sprintf("%t", claims.IsAdmin))
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
isAdmin := r.Header.Get("X-Is-Admin") == "true"
|
||||||
|
if !isAdmin {
|
||||||
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CORS(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
41
backend/models.go
Normal file
41
backend/models.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"-"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schedule struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
DayOfWeek int `json:"day_of_week"` // 0=Monday, 4=Friday
|
||||||
|
StartTime string `json:"start_time"`
|
||||||
|
EndTime string `json:"end_time"`
|
||||||
|
Type string `json:"type"` // "lesson" or "break"
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeEntry struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
ScheduleID int `json:"schedule_id"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Type string `json:"type"` // "lesson" or "break"
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
}
|
||||||
23
backend/static/index.html
Normal file
23
backend/static/index.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!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 app = Elm.Main.init({
|
||||||
|
node: document.getElementById('app')
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
25
frontend/elm.json
Normal file
25
frontend/elm.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"type": "application",
|
||||||
|
"source-directories": ["src"],
|
||||||
|
"elm-version": "0.19.1",
|
||||||
|
"dependencies": {
|
||||||
|
"direct": {
|
||||||
|
"elm/browser": "1.0.2",
|
||||||
|
"elm/core": "1.0.5",
|
||||||
|
"elm/html": "1.0.0",
|
||||||
|
"elm/http": "2.0.0",
|
||||||
|
"elm/json": "1.1.3",
|
||||||
|
"elm/time": "1.0.0"
|
||||||
|
},
|
||||||
|
"indirect": {
|
||||||
|
"elm/bytes": "1.0.8",
|
||||||
|
"elm/file": "1.0.5",
|
||||||
|
"elm/url": "1.0.0",
|
||||||
|
"elm/virtual-dom": "1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test-dependencies": {
|
||||||
|
"direct": {},
|
||||||
|
"indirect": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
950
frontend/src/Main.elm
Normal file
950
frontend/src/Main.elm
Normal file
|
|
@ -0,0 +1,950 @@
|
||||||
|
module Main exposing (..)
|
||||||
|
|
||||||
|
import Browser
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Http
|
||||||
|
import Json.Decode as Decode exposing (Decoder, field, int, string, bool, list)
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Time
|
||||||
|
|
||||||
|
|
||||||
|
-- MAIN
|
||||||
|
|
||||||
|
main : Program () Model Msg
|
||||||
|
main =
|
||||||
|
Browser.element
|
||||||
|
{ init = init
|
||||||
|
, update = update
|
||||||
|
, subscriptions = subscriptions
|
||||||
|
, view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
-- MODEL
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ page : Page
|
||||||
|
, username : String
|
||||||
|
, password : String
|
||||||
|
, token : Maybe String
|
||||||
|
, isAdmin : Bool
|
||||||
|
, schedules : List Schedule
|
||||||
|
, users : List User
|
||||||
|
, timeEntries : List TimeEntry
|
||||||
|
, selectedEntries : List Int
|
||||||
|
, currentDate : String
|
||||||
|
, newSchedule : NewSchedule
|
||||||
|
, newUser : NewUser
|
||||||
|
, error : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Page
|
||||||
|
= LoginPage
|
||||||
|
| UserDashboard
|
||||||
|
| AdminDashboard
|
||||||
|
|
||||||
|
type alias Schedule =
|
||||||
|
{ id : Int
|
||||||
|
, dayOfWeek : Int
|
||||||
|
, startTime : String
|
||||||
|
, endTime : String
|
||||||
|
, scheduleType : String
|
||||||
|
, title : String
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias User =
|
||||||
|
{ id : Int
|
||||||
|
, username : String
|
||||||
|
, isAdmin : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias TimeEntry =
|
||||||
|
{ id : Int
|
||||||
|
, userId : Int
|
||||||
|
, scheduleId : Int
|
||||||
|
, date : String
|
||||||
|
, entryType : String
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias NewSchedule =
|
||||||
|
{ dayOfWeek : String
|
||||||
|
, startTime : String
|
||||||
|
, endTime : String
|
||||||
|
, scheduleType : String
|
||||||
|
, title : String
|
||||||
|
}
|
||||||
|
|
||||||
|
type alias NewUser =
|
||||||
|
{ username : String
|
||||||
|
, password : String
|
||||||
|
, isAdmin : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
init : () -> (Model, Cmd Msg)
|
||||||
|
init _ =
|
||||||
|
( { page = LoginPage
|
||||||
|
, username = ""
|
||||||
|
, password = ""
|
||||||
|
, token = Nothing
|
||||||
|
, isAdmin = False
|
||||||
|
, schedules = []
|
||||||
|
, users = []
|
||||||
|
, timeEntries = []
|
||||||
|
, selectedEntries = []
|
||||||
|
, currentDate = ""
|
||||||
|
, newSchedule = NewSchedule "" "" "" "lesson" ""
|
||||||
|
, newUser = NewUser "" "" False
|
||||||
|
, error = Nothing
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
-- UPDATE
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= UpdateUsername String
|
||||||
|
| UpdatePassword String
|
||||||
|
| Login
|
||||||
|
| LoginResponse (Result Http.Error LoginResult)
|
||||||
|
| Logout
|
||||||
|
| FetchSchedules
|
||||||
|
| SchedulesReceived (Result Http.Error (List Schedule))
|
||||||
|
| ToggleScheduleSelection Int
|
||||||
|
| SaveTimeEntries
|
||||||
|
| TimeEntriesSaved (Result Http.Error ())
|
||||||
|
| UpdateNewScheduleDay String
|
||||||
|
| UpdateNewScheduleStart String
|
||||||
|
| UpdateNewScheduleEnd String
|
||||||
|
| UpdateNewScheduleType String
|
||||||
|
| UpdateNewScheduleTitle String
|
||||||
|
| CreateSchedule
|
||||||
|
| ScheduleCreated (Result Http.Error ())
|
||||||
|
| DeleteSchedule Int
|
||||||
|
| ScheduleDeleted (Result Http.Error ())
|
||||||
|
| UpdateNewUsername String
|
||||||
|
| UpdateNewPassword String
|
||||||
|
| UpdateNewUserAdmin Bool
|
||||||
|
| CreateUser
|
||||||
|
| UserCreated (Result Http.Error ())
|
||||||
|
| FetchUsers
|
||||||
|
| UsersReceived (Result Http.Error (List User))
|
||||||
|
| FetchAllTimeEntries
|
||||||
|
| AllTimeEntriesReceived (Result Http.Error (List TimeEntry))
|
||||||
|
| UpdateCurrentDate String
|
||||||
|
|
||||||
|
update : Msg -> Model -> (Model, Cmd Msg)
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
UpdateUsername username ->
|
||||||
|
({ model | username = username }, Cmd.none)
|
||||||
|
|
||||||
|
UpdatePassword password ->
|
||||||
|
({ model | password = password }, Cmd.none)
|
||||||
|
|
||||||
|
Login ->
|
||||||
|
(model, loginRequest model.username model.password)
|
||||||
|
|
||||||
|
LoginResponse (Ok result) ->
|
||||||
|
let
|
||||||
|
newPage = if result.isAdmin then AdminDashboard else UserDashboard
|
||||||
|
in
|
||||||
|
({ model
|
||||||
|
| token = Just result.token
|
||||||
|
, isAdmin = result.isAdmin
|
||||||
|
, page = newPage
|
||||||
|
, error = Nothing
|
||||||
|
}, fetchSchedules (Just result.token))
|
||||||
|
|
||||||
|
LoginResponse (Err _) ->
|
||||||
|
({ model | error = Just "Login fehlgeschlagen" }, Cmd.none)
|
||||||
|
|
||||||
|
Logout ->
|
||||||
|
init ()
|
||||||
|
|
||||||
|
FetchSchedules ->
|
||||||
|
(model, fetchSchedules model.token)
|
||||||
|
|
||||||
|
SchedulesReceived (Ok schedules) ->
|
||||||
|
({ model | schedules = schedules }, Cmd.none)
|
||||||
|
|
||||||
|
SchedulesReceived (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none)
|
||||||
|
|
||||||
|
ToggleScheduleSelection scheduleId ->
|
||||||
|
let
|
||||||
|
newSelected =
|
||||||
|
if List.member scheduleId model.selectedEntries then
|
||||||
|
List.filter (\id -> id /= scheduleId) model.selectedEntries
|
||||||
|
else
|
||||||
|
scheduleId :: model.selectedEntries
|
||||||
|
in
|
||||||
|
({ model | selectedEntries = newSelected }, Cmd.none)
|
||||||
|
|
||||||
|
SaveTimeEntries ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
(model, saveTimeEntries token model.selectedEntries model.currentDate)
|
||||||
|
Nothing ->
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
TimeEntriesSaved (Ok _) ->
|
||||||
|
({ model | selectedEntries = [], error = Nothing }, Cmd.none)
|
||||||
|
|
||||||
|
TimeEntriesSaved (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Speichern" }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewScheduleDay day ->
|
||||||
|
let
|
||||||
|
oldSchedule = model.newSchedule
|
||||||
|
newSchedule = { oldSchedule | dayOfWeek = day }
|
||||||
|
in
|
||||||
|
({ model | newSchedule = newSchedule }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewScheduleStart time ->
|
||||||
|
let
|
||||||
|
oldSchedule = model.newSchedule
|
||||||
|
newSchedule = { oldSchedule | startTime = time }
|
||||||
|
in
|
||||||
|
({ model | newSchedule = newSchedule }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewScheduleEnd time ->
|
||||||
|
let
|
||||||
|
oldSchedule = model.newSchedule
|
||||||
|
newSchedule = { oldSchedule | endTime = time }
|
||||||
|
in
|
||||||
|
({ model | newSchedule = newSchedule }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewScheduleType scheduleType ->
|
||||||
|
let
|
||||||
|
oldSchedule = model.newSchedule
|
||||||
|
newSchedule = { oldSchedule | scheduleType = scheduleType }
|
||||||
|
in
|
||||||
|
({ model | newSchedule = newSchedule }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewScheduleTitle title ->
|
||||||
|
let
|
||||||
|
oldSchedule = model.newSchedule
|
||||||
|
newSchedule = { oldSchedule | title = title }
|
||||||
|
in
|
||||||
|
({ model | newSchedule = newSchedule }, Cmd.none)
|
||||||
|
|
||||||
|
CreateSchedule ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
(model, createSchedule token model.newSchedule)
|
||||||
|
Nothing ->
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
ScheduleCreated (Ok _) ->
|
||||||
|
let
|
||||||
|
emptySchedule = NewSchedule "" "" "" "lesson" ""
|
||||||
|
in
|
||||||
|
({ model | newSchedule = emptySchedule }, fetchSchedules model.token)
|
||||||
|
|
||||||
|
ScheduleCreated (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Erstellen" }, Cmd.none)
|
||||||
|
|
||||||
|
DeleteSchedule scheduleId ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
(model, deleteSchedule token scheduleId)
|
||||||
|
Nothing ->
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
ScheduleDeleted (Ok _) ->
|
||||||
|
(model, fetchSchedules model.token)
|
||||||
|
|
||||||
|
ScheduleDeleted (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Löschen" }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewUsername username ->
|
||||||
|
let
|
||||||
|
oldUser = model.newUser
|
||||||
|
newUser = { oldUser | username = username }
|
||||||
|
in
|
||||||
|
({ model | newUser = newUser }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewPassword password ->
|
||||||
|
let
|
||||||
|
oldUser = model.newUser
|
||||||
|
newUser = { oldUser | password = password }
|
||||||
|
in
|
||||||
|
({ model | newUser = newUser }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateNewUserAdmin isAdmin ->
|
||||||
|
let
|
||||||
|
oldUser = model.newUser
|
||||||
|
newUser = { oldUser | isAdmin = isAdmin }
|
||||||
|
in
|
||||||
|
({ model | newUser = newUser }, Cmd.none)
|
||||||
|
|
||||||
|
CreateUser ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
(model, createUser token model.newUser)
|
||||||
|
Nothing ->
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
UserCreated (Ok _) ->
|
||||||
|
let
|
||||||
|
emptyUser = NewUser "" "" False
|
||||||
|
in
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
({ model | newUser = emptyUser }, fetchUsers token)
|
||||||
|
Nothing ->
|
||||||
|
({ model | error = Just "Kein Token vorhanden" }, Cmd.none)
|
||||||
|
-- ({ model | newUser = emptyUser }, fetchUsers model.token)
|
||||||
|
|
||||||
|
UserCreated (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Erstellen des Benutzers" }, Cmd.none)
|
||||||
|
|
||||||
|
FetchUsers ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
(model, fetchUsers token)
|
||||||
|
Nothing ->
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
UsersReceived (Ok users) ->
|
||||||
|
({ model | users = users }, Cmd.none)
|
||||||
|
|
||||||
|
UsersReceived (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Laden der Benutzer" }, Cmd.none)
|
||||||
|
|
||||||
|
FetchAllTimeEntries ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
(model, fetchAllTimeEntries token)
|
||||||
|
Nothing ->
|
||||||
|
(model, Cmd.none)
|
||||||
|
|
||||||
|
AllTimeEntriesReceived (Ok entries) ->
|
||||||
|
({ model | timeEntries = entries }, Cmd.none)
|
||||||
|
|
||||||
|
AllTimeEntriesReceived (Err _) ->
|
||||||
|
({ model | error = Just "Fehler beim Laden der Zeiteinträge" }, Cmd.none)
|
||||||
|
|
||||||
|
UpdateCurrentDate date ->
|
||||||
|
({ model | currentDate = date }, Cmd.none)
|
||||||
|
|
||||||
|
|
||||||
|
-- SUBSCRIPTIONS
|
||||||
|
|
||||||
|
subscriptions : Model -> Sub Msg
|
||||||
|
subscriptions model =
|
||||||
|
Sub.none
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEW
|
||||||
|
|
||||||
|
view : Model -> Html Msg
|
||||||
|
view model =
|
||||||
|
div [ class "container" ]
|
||||||
|
[ case model.page of
|
||||||
|
LoginPage ->
|
||||||
|
viewLogin model
|
||||||
|
|
||||||
|
UserDashboard ->
|
||||||
|
viewUserDashboard model
|
||||||
|
|
||||||
|
AdminDashboard ->
|
||||||
|
viewAdminDashboard model
|
||||||
|
]
|
||||||
|
|
||||||
|
viewLogin : Model -> Html Msg
|
||||||
|
viewLogin model =
|
||||||
|
section [ class "section" ]
|
||||||
|
[ div [ class "container" ]
|
||||||
|
[ div [ class "columns is-centered" ]
|
||||||
|
[ div [ class "column is-5-tablet is-4-desktop is-3-widescreen" ]
|
||||||
|
[ div [ class "box" ]
|
||||||
|
[ h1 [ class "title has-text-centered" ] [ text "Zeiterfassung Login" ]
|
||||||
|
, case model.error of
|
||||||
|
Just err ->
|
||||||
|
div [ class "notification is-danger" ] [ text err ]
|
||||||
|
Nothing ->
|
||||||
|
text ""
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Benutzername" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "text"
|
||||||
|
, placeholder "Benutzername"
|
||||||
|
, value model.username
|
||||||
|
, onInput UpdateUsername
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Passwort" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "password"
|
||||||
|
, placeholder "Passwort"
|
||||||
|
, value model.password
|
||||||
|
, onInput UpdatePassword
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ div [ class "control" ]
|
||||||
|
[ button
|
||||||
|
[ class "button is-primary is-fullwidth"
|
||||||
|
, onClick Login
|
||||||
|
] [ text "Anmelden" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewUserDashboard : Model -> Html Msg
|
||||||
|
viewUserDashboard model =
|
||||||
|
div []
|
||||||
|
[ nav [ class "navbar is-primary" ]
|
||||||
|
[ div [ class "navbar-brand" ]
|
||||||
|
[ div [ class "navbar-item" ]
|
||||||
|
[ h1 [ class "title is-4 has-text-white" ] [ text "Zeiterfassung" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "navbar-menu" ]
|
||||||
|
[ div [ class "navbar-end" ]
|
||||||
|
[ div [ class "navbar-item" ]
|
||||||
|
[ button [ class "button is-light", onClick Logout ] [ text "Abmelden" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, section [ class "section" ]
|
||||||
|
[ div [ class "container" ]
|
||||||
|
[ h2 [ class "title" ] [ text "Stundenplan" ]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Datum" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "date"
|
||||||
|
, value model.currentDate
|
||||||
|
, onInput UpdateCurrentDate
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, viewScheduleGrid model
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ div [ class "control" ]
|
||||||
|
[ button
|
||||||
|
[ class "button is-primary is-large is-fullwidth"
|
||||||
|
, onClick SaveTimeEntries
|
||||||
|
, disabled (List.isEmpty model.selectedEntries || String.isEmpty model.currentDate)
|
||||||
|
] [ text "Speichern" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, case model.error of
|
||||||
|
Just err ->
|
||||||
|
div [ class "notification is-danger" ] [ text err ]
|
||||||
|
Nothing ->
|
||||||
|
text ""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewAdminDashboard : Model -> Html Msg
|
||||||
|
viewAdminDashboard model =
|
||||||
|
div []
|
||||||
|
[ nav [ class "navbar is-danger" ]
|
||||||
|
[ div [ class "navbar-brand" ]
|
||||||
|
[ div [ class "navbar-item" ]
|
||||||
|
[ h1 [ class "title is-4 has-text-white" ] [ text "Admin Dashboard" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "navbar-menu" ]
|
||||||
|
[ div [ class "navbar-end" ]
|
||||||
|
[ div [ class "navbar-item" ]
|
||||||
|
[ button [ class "button is-light", onClick Logout ] [ text "Abmelden" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, section [ class "section" ]
|
||||||
|
[ div [ class "container" ]
|
||||||
|
[ div [ class "tabs is-boxed" ]
|
||||||
|
[ ul []
|
||||||
|
[ li [ class "is-active" ] [ a [] [ text "Stundenplan" ] ]
|
||||||
|
, li [] [ a [ onClick FetchUsers ] [ text "Benutzer" ] ]
|
||||||
|
, li [] [ a [ onClick FetchAllTimeEntries ] [ text "Zeiteinträge" ] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, h2 [ class "title" ] [ text "Stundenplan verwalten" ]
|
||||||
|
, viewScheduleForm model
|
||||||
|
, viewScheduleList model
|
||||||
|
, h2 [ class "title" ] [ text "Benutzer anlegen" ]
|
||||||
|
, viewUserForm model
|
||||||
|
, if not (List.isEmpty model.users) then
|
||||||
|
viewUserList model
|
||||||
|
else
|
||||||
|
text ""
|
||||||
|
, if not (List.isEmpty model.timeEntries) then
|
||||||
|
viewTimeEntriesList model
|
||||||
|
else
|
||||||
|
text ""
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewScheduleGrid : Model -> Html Msg
|
||||||
|
viewScheduleGrid model =
|
||||||
|
let
|
||||||
|
days = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"]
|
||||||
|
|
||||||
|
groupedSchedules = List.range 0 4
|
||||||
|
|> List.map (\day ->
|
||||||
|
List.filter (\s -> s.dayOfWeek == day) model.schedules
|
||||||
|
)
|
||||||
|
in
|
||||||
|
div [ class "table-container" ]
|
||||||
|
[ table [ class "table is-bordered is-fullwidth" ]
|
||||||
|
[ thead []
|
||||||
|
[ tr [] (List.map (\day -> th [] [ text day ]) days)
|
||||||
|
]
|
||||||
|
, tbody []
|
||||||
|
[ tr []
|
||||||
|
(List.map (viewDayColumn model) groupedSchedules)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewDayColumn : Model -> List Schedule -> Html Msg
|
||||||
|
viewDayColumn model schedules =
|
||||||
|
td [ class "has-background-light" ]
|
||||||
|
(List.map (viewScheduleItem model) schedules)
|
||||||
|
|
||||||
|
viewScheduleItem : Model -> Schedule -> Html Msg
|
||||||
|
viewScheduleItem model schedule =
|
||||||
|
let
|
||||||
|
isSelected = List.member schedule.id model.selectedEntries
|
||||||
|
boxClass = if isSelected then "box has-background-primary-light" else "box"
|
||||||
|
typeText = if schedule.scheduleType == "break" then " (Pause)" else ""
|
||||||
|
in
|
||||||
|
div
|
||||||
|
[ class boxClass
|
||||||
|
, onClick (ToggleScheduleSelection schedule.id)
|
||||||
|
, style "cursor" "pointer"
|
||||||
|
, style "margin-bottom" "0.5rem"
|
||||||
|
]
|
||||||
|
[ p [ class "has-text-weight-bold" ] [ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
|
||||||
|
, p [] [ text (schedule.title ++ typeText) ]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewScheduleForm : Model -> Html Msg
|
||||||
|
viewScheduleForm model =
|
||||||
|
div [ class "box" ]
|
||||||
|
[ div [ class "columns" ]
|
||||||
|
[ div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Wochentag" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ div [ class "select is-fullwidth" ]
|
||||||
|
[ select [ onInput UpdateNewScheduleDay ]
|
||||||
|
[ option [ value "" ] [ text "Wochentag wählen" ]
|
||||||
|
, option [ value "0" ] [ text "Montag" ]
|
||||||
|
, option [ value "1" ] [ text "Dienstag" ]
|
||||||
|
, option [ value "2" ] [ text "Mittwoch" ]
|
||||||
|
, option [ value "3" ] [ text "Donnerstag" ]
|
||||||
|
, option [ value "4" ] [ text "Freitag" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Startzeit" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "time"
|
||||||
|
, value model.newSchedule.startTime
|
||||||
|
, onInput UpdateNewScheduleStart
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Endzeit" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "time"
|
||||||
|
, value model.newSchedule.endTime
|
||||||
|
, onInput UpdateNewScheduleEnd
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "columns" ]
|
||||||
|
[ div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Typ" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ div [ class "select is-fullwidth" ]
|
||||||
|
[ select [ onInput UpdateNewScheduleType, value model.newSchedule.scheduleType ]
|
||||||
|
[ option [ value "lesson" ] [ text "Unterricht" ]
|
||||||
|
, option [ value "break" ] [ text "Pause" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Titel" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "text"
|
||||||
|
, placeholder "z.B. Mathematik"
|
||||||
|
, value model.newSchedule.title
|
||||||
|
, onInput UpdateNewScheduleTitle
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ div [ class "control" ]
|
||||||
|
[ button [ class "button is-primary", onClick CreateSchedule ] [ text "Hinzufügen" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewScheduleList : Model -> Html Msg
|
||||||
|
viewScheduleList model =
|
||||||
|
div [ class "box" ]
|
||||||
|
[ h3 [ class "subtitle" ] [ text "Aktueller Stundenplan" ]
|
||||||
|
, table [ class "table is-fullwidth is-striped" ]
|
||||||
|
[ thead []
|
||||||
|
[ tr []
|
||||||
|
[ th [] [ text "Tag" ]
|
||||||
|
, th [] [ text "Zeit" ]
|
||||||
|
, th [] [ text "Typ" ]
|
||||||
|
, th [] [ text "Titel" ]
|
||||||
|
, th [] [ text "Aktion" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, tbody []
|
||||||
|
(List.map viewScheduleRow model.schedules)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewScheduleRow : Schedule -> Html Msg
|
||||||
|
viewScheduleRow schedule =
|
||||||
|
let
|
||||||
|
dayName = case schedule.dayOfWeek of
|
||||||
|
0 -> "Montag"
|
||||||
|
1 -> "Dienstag"
|
||||||
|
2 -> "Mittwoch"
|
||||||
|
3 -> "Donnerstag"
|
||||||
|
4 -> "Freitag"
|
||||||
|
_ -> "Unbekannt"
|
||||||
|
|
||||||
|
typeName = if schedule.scheduleType == "break" then "Pause" else "Unterricht"
|
||||||
|
in
|
||||||
|
tr []
|
||||||
|
[ td [] [ text dayName ]
|
||||||
|
, td [] [ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
|
||||||
|
, td [] [ text typeName ]
|
||||||
|
, td [] [ text schedule.title ]
|
||||||
|
, td []
|
||||||
|
[ button
|
||||||
|
[ class "button is-small is-danger"
|
||||||
|
, onClick (DeleteSchedule schedule.id)
|
||||||
|
] [ text "Löschen" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewUserForm : Model -> Html Msg
|
||||||
|
viewUserForm model =
|
||||||
|
div [ class "box" ]
|
||||||
|
[ div [ class "columns" ]
|
||||||
|
[ div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Benutzername" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "text"
|
||||||
|
, placeholder "Benutzername"
|
||||||
|
, value model.newUser.username
|
||||||
|
, onInput UpdateNewUsername
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "column" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Passwort" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ input
|
||||||
|
[ class "input"
|
||||||
|
, type_ "password"
|
||||||
|
, placeholder "Passwort"
|
||||||
|
, value model.newUser.password
|
||||||
|
, onInput UpdateNewPassword
|
||||||
|
] []
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "column is-narrow" ]
|
||||||
|
[ div [ class "field" ]
|
||||||
|
[ label [ class "label" ] [ text "Admin" ]
|
||||||
|
, div [ class "control" ]
|
||||||
|
[ label [ class "checkbox" ]
|
||||||
|
[ input
|
||||||
|
[ type_ "checkbox"
|
||||||
|
, checked model.newUser.isAdmin
|
||||||
|
, onCheck UpdateNewUserAdmin
|
||||||
|
] []
|
||||||
|
, text " Admin-Rechte"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "field" ]
|
||||||
|
[ div [ class "control" ]
|
||||||
|
[ button [ class "button is-primary", onClick CreateUser ] [ text "Benutzer anlegen" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewUserList : Model -> Html Msg
|
||||||
|
viewUserList model =
|
||||||
|
div [ class "box" ]
|
||||||
|
[ h3 [ class "subtitle" ] [ text "Benutzer" ]
|
||||||
|
, table [ class "table is-fullwidth is-striped" ]
|
||||||
|
[ thead []
|
||||||
|
[ tr []
|
||||||
|
[ th [] [ text "ID" ]
|
||||||
|
, th [] [ text "Benutzername" ]
|
||||||
|
, th [] [ text "Rolle" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, tbody []
|
||||||
|
(List.map viewUserRow model.users)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewUserRow : User -> Html Msg
|
||||||
|
viewUserRow user =
|
||||||
|
tr []
|
||||||
|
[ td [] [ text (String.fromInt user.id) ]
|
||||||
|
, td [] [ text user.username ]
|
||||||
|
, td [] [ text (if user.isAdmin then "Admin" else "Benutzer") ]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewTimeEntriesList : Model -> Html Msg
|
||||||
|
viewTimeEntriesList model =
|
||||||
|
div [ class "box" ]
|
||||||
|
[ h3 [ class "subtitle" ] [ text "Alle Zeiteinträge" ]
|
||||||
|
, table [ class "table is-fullwidth is-striped" ]
|
||||||
|
[ thead []
|
||||||
|
[ tr []
|
||||||
|
[ th [] [ text "Benutzer ID" ]
|
||||||
|
, th [] [ text "Stundenplan ID" ]
|
||||||
|
, th [] [ text "Datum" ]
|
||||||
|
, th [] [ text "Typ" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, tbody []
|
||||||
|
(List.map viewTimeEntryRow model.timeEntries)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
viewTimeEntryRow : TimeEntry -> Html Msg
|
||||||
|
viewTimeEntryRow entry =
|
||||||
|
tr []
|
||||||
|
[ td [] [ text (String.fromInt entry.userId) ]
|
||||||
|
, td [] [ text (String.fromInt entry.scheduleId) ]
|
||||||
|
, td [] [ text entry.date ]
|
||||||
|
, td [] [ text entry.entryType ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
-- HTTP
|
||||||
|
|
||||||
|
type alias LoginResult =
|
||||||
|
{ token : String
|
||||||
|
, username : String
|
||||||
|
, isAdmin : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
loginRequest : String -> String -> Cmd Msg
|
||||||
|
loginRequest username password =
|
||||||
|
Http.post
|
||||||
|
{ url = "/api/login"
|
||||||
|
, body = Http.jsonBody <|
|
||||||
|
Encode.object
|
||||||
|
[ ("username", Encode.string username)
|
||||||
|
, ("password", Encode.string password)
|
||||||
|
]
|
||||||
|
, expect = Http.expectJson LoginResponse loginDecoder
|
||||||
|
}
|
||||||
|
|
||||||
|
loginDecoder : Decoder LoginResult
|
||||||
|
loginDecoder =
|
||||||
|
Decode.map3 LoginResult
|
||||||
|
(field "token" string)
|
||||||
|
(field "username" string)
|
||||||
|
(field "is_admin" bool)
|
||||||
|
|
||||||
|
fetchSchedules : Maybe String -> Cmd Msg
|
||||||
|
fetchSchedules maybeToken =
|
||||||
|
case maybeToken of
|
||||||
|
Just token ->
|
||||||
|
Http.request
|
||||||
|
{ method = "GET"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/schedules"
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect = Http.expectJson SchedulesReceived (Decode.list scheduleDecoder)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
scheduleDecoder : Decoder Schedule
|
||||||
|
scheduleDecoder =
|
||||||
|
Decode.map6 Schedule
|
||||||
|
(field "id" int)
|
||||||
|
(field "day_of_week" int)
|
||||||
|
(field "start_time" string)
|
||||||
|
(field "end_time" string)
|
||||||
|
(field "type" string)
|
||||||
|
(field "title" string)
|
||||||
|
|
||||||
|
saveTimeEntries : String -> List Int -> String -> Cmd Msg
|
||||||
|
saveTimeEntries token scheduleIds date =
|
||||||
|
let
|
||||||
|
requests = List.map (\scheduleId ->
|
||||||
|
Http.request
|
||||||
|
{ method = "POST"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/time-entries"
|
||||||
|
, body = Http.jsonBody <|
|
||||||
|
Encode.object
|
||||||
|
[ ("schedule_id", Encode.int scheduleId)
|
||||||
|
, ("date", Encode.string date)
|
||||||
|
, ("type", Encode.string "lesson")
|
||||||
|
]
|
||||||
|
, expect = Http.expectWhatever TimeEntriesSaved
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
) scheduleIds
|
||||||
|
in
|
||||||
|
case List.head requests of
|
||||||
|
Just cmd -> cmd
|
||||||
|
Nothing -> Cmd.none
|
||||||
|
|
||||||
|
createSchedule : String -> NewSchedule -> Cmd Msg
|
||||||
|
createSchedule token schedule =
|
||||||
|
case String.toInt schedule.dayOfWeek of
|
||||||
|
Just day ->
|
||||||
|
Http.request
|
||||||
|
{ method = "POST"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/schedules"
|
||||||
|
, body = Http.jsonBody <|
|
||||||
|
Encode.object
|
||||||
|
[ ("day_of_week", Encode.int day)
|
||||||
|
, ("start_time", Encode.string schedule.startTime)
|
||||||
|
, ("end_time", Encode.string schedule.endTime)
|
||||||
|
, ("type", Encode.string schedule.scheduleType)
|
||||||
|
, ("title", Encode.string schedule.title)
|
||||||
|
]
|
||||||
|
, expect = Http.expectWhatever ScheduleCreated
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
deleteSchedule : String -> Int -> Cmd Msg
|
||||||
|
deleteSchedule token scheduleId =
|
||||||
|
Http.request
|
||||||
|
{ method = "DELETE"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/schedules/delete?id=" ++ String.fromInt scheduleId
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect = Http.expectWhatever ScheduleDeleted
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
createUser : String -> NewUser -> Cmd Msg
|
||||||
|
createUser token user =
|
||||||
|
Http.request
|
||||||
|
{ method = "POST"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/users"
|
||||||
|
, body = Http.jsonBody <|
|
||||||
|
Encode.object
|
||||||
|
[ ("username", Encode.string user.username)
|
||||||
|
, ("password", Encode.string user.password)
|
||||||
|
, ("is_admin", Encode.bool user.isAdmin)
|
||||||
|
]
|
||||||
|
, expect = Http.expectWhatever UserCreated
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUsers : String -> Cmd Msg
|
||||||
|
fetchUsers token =
|
||||||
|
Http.request
|
||||||
|
{ method = "GET"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/users/list"
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect = Http.expectJson UsersReceived (Decode.list userDecoder)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
Decode.map3 User
|
||||||
|
(field "id" int)
|
||||||
|
(field "username" string)
|
||||||
|
(field "is_admin" bool)
|
||||||
|
|
||||||
|
fetchAllTimeEntries : String -> Cmd Msg
|
||||||
|
fetchAllTimeEntries token =
|
||||||
|
Http.request
|
||||||
|
{ method = "GET"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/time-entries"
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect = Http.expectJson AllTimeEntriesReceived (Decode.list timeEntryDecoder)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
timeEntryDecoder : Decoder TimeEntry
|
||||||
|
timeEntryDecoder =
|
||||||
|
Decode.map5 TimeEntry
|
||||||
|
(field "id" int)
|
||||||
|
(field "user_id" int)
|
||||||
|
(field "schedule_id" int)
|
||||||
|
(field "date" string)
|
||||||
|
(field "type" string)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue