commit 708722865d7599c4ee487aeeda3eb5a4315dcdee Author: Daniel Eder Date: Fri Feb 14 16:08:48 2025 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91ed36f --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +#repo specific files +tms.exe +training.db diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..09943f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module tms + +go 1.22.0 + +require github.com/mattn/go-sqlite3 v1.14.24 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9dcdc9b --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/main.go b/main.go new file mode 100644 index 0000000..338ea75 --- /dev/null +++ b/main.go @@ -0,0 +1,316 @@ +package main + +import ( + "database/sql" + "embed" + "fmt" + "html/template" + "net/http" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +//go:embed templates/* +var templatesFS embed.FS + +//go:embed schema.sql +var schemaSQL string + +type Trainer struct { + ID int + Name string + Email string + Areas []TrainingArea +} + +type Training struct { + ID int + Title string + TrainingType string + Mode string + Start time.Time + End time.Time + Status string + Trainers []Trainer + Area TrainingArea +} + +type TrainingArea struct { + ID int + Name string + Trainers []Trainer +} + +func (ta TrainingArea) IsTrainerAssigned(trainerID int) bool { + if ta.Trainers == nil { + return false + } + + for _, trainer := range ta.Trainers { + if trainer.ID == trainerID { + return true + } + } + + return false +} + +var db *sql.DB + +func initDB() { + var err error + db, err = sql.Open("sqlite3", "training.db") + if err != nil { + panic(err) + } + + _, err = db.Exec(schemaSQL) + if err != nil { + panic(err) + } +} + +var tmpl *template.Template + +func initTemplates() { + var err error + tmpl, err = template.ParseFS(templatesFS, "templates/*.html") + if err != nil { + panic(fmt.Sprintf("Error parsing templates: %v", err)) + } +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + err := tmpl.ExecuteTemplate(w, "home.html", nil) + if err != nil { + http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) + } +} + +func trainingAreaListHandler(w http.ResponseWriter, r *http.Request) { + areas, err := fetchTrainingAreasWithTrainers() + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + trainers, err := fetchTrainers() + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + data := struct { + Areas []TrainingArea + Trainers []Trainer + }{ + Areas: areas, + Trainers: trainers, + } + + err = tmpl.ExecuteTemplate(w, "training_area_list.html", data) + if err != nil { + http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) + return + } +} + +func fetchTrainingAreasWithTrainers() ([]TrainingArea, error) { + rows, err := db.Query(` + SELECT ta.id, ta.name, t.id, t.name, t.email + FROM training_areas ta + LEFT JOIN trainer_training_areas tta ON ta.id = tta.training_area_id + LEFT JOIN trainers t ON tta.trainer_id = t.id + ORDER BY ta.id, t.name`) + if err != nil { + return nil, err + } + defer rows.Close() + + areaMap := make(map[int]*TrainingArea) + + for rows.Next() { + var areaID int + var areaName string + var trainerID sql.NullInt64 + var trainerName sql.NullString + var trainerEmail sql.NullString + + err := rows.Scan(&areaID, &areaName, &trainerID, &trainerName, &trainerEmail) + if err != nil { + return nil, err + } + + _, exists := areaMap[areaID] + if !exists { + area := &TrainingArea{ID: areaID, Name: areaName} + areaMap[areaID] = area + } + + if trainerID.Valid { + areaMap[areaID].Trainers = append(areaMap[areaID].Trainers, Trainer{ + ID: int(trainerID.Int64), + Name: trainerName.String, + Email: trainerEmail.String, + }) + } + } + + var areas []TrainingArea + for _, area := range areaMap { + areas = append(areas, *area) + } + + return areas, nil +} + +func assignTrainerToAreaHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + trainerID := r.FormValue("trainer_id") + areaID := r.FormValue("training_area_id") + + if trainerID == "" || areaID == "" { + http.Error(w, "Trainer and Training Area are required", http.StatusBadRequest) + return + } + + _, err := db.Exec("INSERT OR IGNORE INTO trainer_training_areas (trainer_id, training_area_id) VALUES (?, ?)", trainerID, areaID) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/training-areas", http.StatusSeeOther) + } +} + +func addTrainingAreaHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + name := r.FormValue("name") + if name == "" { + http.Error(w, "Name is required", http.StatusBadRequest) + return + } + + _, err := db.Exec("INSERT INTO training_areas (name) VALUES (?)", name) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/training-areas", http.StatusSeeOther) + return + } + + err := tmpl.ExecuteTemplate(w, "add_training_area.html", nil) + if err != nil { + http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) + } +} + +func fetchTrainings() ([]Training, error) { + rows, err := db.Query("SELECT id, title, training_type, mode, start, end, status FROM trainings") + if err != nil { + return nil, err + } + defer rows.Close() + + var trainings []Training + for rows.Next() { + var t Training + var start, end string + err := rows.Scan(&t.ID, &t.Title, &t.TrainingType, &t.Mode, &start, &end, &t.Status) + if err != nil { + return nil, err + } + t.Start, _ = time.Parse("2006-01-02 15:04:05", start) + t.End, _ = time.Parse("2006-01-02 15:04:05", end) + trainings = append(trainings, t) + } + return trainings, nil +} + +func trainingListHandler(w http.ResponseWriter, r *http.Request) { + trainings, err := fetchTrainings() + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + err = tmpl.ExecuteTemplate(w, "training_list.html", trainings) + if err != nil { + http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) + } +} + +func fetchTrainers() ([]Trainer, error) { + rows, err := db.Query("SELECT id, name, email FROM trainers") + if err != nil { + return nil, err + } + defer rows.Close() + + var trainers []Trainer + for rows.Next() { + var t Trainer + err := rows.Scan(&t.ID, &t.Name, &t.Email) + if err != nil { + return nil, err + } + trainers = append(trainers, t) + } + return trainers, nil +} + +func trainerListHandler(w http.ResponseWriter, r *http.Request) { + trainers, err := fetchTrainers() + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + + err = tmpl.ExecuteTemplate(w, "trainer_list.html", trainers) + if err != nil { + http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) + } +} + +func addTrainerHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + name := r.FormValue("name") + email := r.FormValue("email") + if name == "" || email == "" { + http.Error(w, "Name and Email are required", http.StatusBadRequest) + return + } + + _, err := db.Exec("INSERT INTO trainers (name, email) VALUES (?, ?)", name, email) + if err != nil { + http.Error(w, "Database error", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/trainers", http.StatusSeeOther) + return + } + + err := tmpl.ExecuteTemplate(w, "add_trainer.html", nil) + if err != nil { + http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) + } + +} + +func main() { + initDB() + initTemplates() + http.HandleFunc("/", homeHandler) + http.HandleFunc("/trainings", trainingListHandler) + http.HandleFunc("/trainers", trainerListHandler) + http.HandleFunc("/trainers/add", addTrainerHandler) + http.HandleFunc("/training-areas", trainingAreaListHandler) + http.HandleFunc("/training-areas/add", addTrainingAreaHandler) + http.HandleFunc("/training-areas/assign-trainer", assignTrainerToAreaHandler) + + fmt.Println("Server running on http://localhost:8080") + http.ListenAndServe(":8080", nil) +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..e567faf --- /dev/null +++ b/schema.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS trainings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + training_type TEXT NOT NULL, + mode TEXT NOT NULL, + start DATETIME NOT NULL, + end DATETIME NOT NULL, + status TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS trainers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS training_trainers ( + training_id INTEGER NOT NULL, + trainer_id INTEGER NOT NULL, + FOREIGN KEY (training_id) REFERENCES trainings(id), + FOREIGN KEY (trainer_id) REFERENCES trainers(id), + PRIMARY KEY (training_id, trainer_id) +); + +CREATE TABLE IF NOT EXISTS training_areas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL +); + +CREATE TABLE IF NOT EXISTS trainer_training_areas ( + trainer_id INTEGER, + training_area_id INTEGER, + PRIMARY KEY (trainer_id, training_area_id), + FOREIGN KEY (trainer_id) REFERENCES trainers(id), + FOREIGN KEY (training_area_id) REFERENCES training_areas(id) +); + +CREATE TABLE IF NOT EXISTS training_training_areas ( + training_id INTEGER, + training_area_id INTEGER, + PRIMARY KEY (training_id, training_area_id), + FOREIGN KEY (training_id) REFERENCES trainings(id), + FOREIGN KEY (training_area_id) REFERENCES training_areas(id) +); diff --git a/templates/add_trainer.html b/templates/add_trainer.html new file mode 100644 index 0000000..1d0072c --- /dev/null +++ b/templates/add_trainer.html @@ -0,0 +1,19 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} +

Add Trainer

+ Back to Home +
+ +

+ + +

+ + +
+ + diff --git a/templates/add_training_area.html b/templates/add_training_area.html new file mode 100644 index 0000000..9c6ba4f --- /dev/null +++ b/templates/add_training_area.html @@ -0,0 +1,18 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} +

Add New Training Area

+
+ + + +
+
+ Back to Training Areas +

+ Back to Home + + diff --git a/templates/css.html b/templates/css.html new file mode 100644 index 0000000..6361904 --- /dev/null +++ b/templates/css.html @@ -0,0 +1,101 @@ + + \ No newline at end of file diff --git a/templates/head.html b/templates/head.html new file mode 100644 index 0000000..86e3a13 --- /dev/null +++ b/templates/head.html @@ -0,0 +1,6 @@ + + + TMS - Nagarro Training Solutions + + {{template "css.html" .}} + diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..d18311b --- /dev/null +++ b/templates/header.html @@ -0,0 +1,4 @@ +
+ Training Management Logo +

Nagarro Training Solutions

+
\ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..f4356cb --- /dev/null +++ b/templates/home.html @@ -0,0 +1,33 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} +

Welcome to the Training Management System

+
+ +
+ + diff --git a/templates/trainer_list.html b/templates/trainer_list.html new file mode 100644 index 0000000..53f41c3 --- /dev/null +++ b/templates/trainer_list.html @@ -0,0 +1,24 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} + +

Trainers

+ Back to Home + Add Trainer + + + + + + {{ range . }} + + + + + {{ end }} +
NameEmail
{{ .Name }}{{ .Email }}
+ + diff --git a/templates/training_area_list.html b/templates/training_area_list.html new file mode 100644 index 0000000..d7abc63 --- /dev/null +++ b/templates/training_area_list.html @@ -0,0 +1,41 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} + +

Training Areas

+Add Training Area + + + + + + {{range $.Trainers}} + + {{end}} + + + + {{range $i, $currentArea := .Areas}} + + + {{range $.Trainers}} + + {{end}} + + {{end}} + +
Training Area{{.Name}}
{{.Name}} +
+ + + +
+
+ +Back to Home + + + diff --git a/templates/training_list.html b/templates/training_list.html new file mode 100644 index 0000000..e2503ce --- /dev/null +++ b/templates/training_list.html @@ -0,0 +1,30 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} +

Trainings

+ Back to Home + + + + + + + + + + {{ range . }} + + + + + + + + + {{ end }} +
TitleTypeModeStartEndStatus
{{ .Title }}{{ .TrainingType }}{{ .Mode }}{{ .Start }}{{ .End }}{{ .Status }}
+ +