diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 12b0ee1..b5170b3 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,12 +2,11 @@ package database import ( "embed" - "encoding/json" "errors" + "fmt" "path/filepath" "ttime/internal/types" - "github.com/gofiber/fiber/v2/log" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" ) @@ -23,6 +22,8 @@ type Database interface { GetUserId(username string) (int, error) AddProject(name string, description string, username string) error DeleteProject(name string, username string) error + Migrate() error + MigrateSampleData() error GetProjectId(projectname string) (int, error) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddUserToProject(username string, projectname string, role string) error @@ -40,21 +41,18 @@ type Database interface { SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) IsProjectManager(username string, projectname string) (bool, error) - ReportStatistics(username string, projectName string) (*types.Statistics, error) GetProjectTimes(projectName string) (map[string]int, error) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error RemoveProject(projectname string) error GetUserName(id int) (string, error) UnsignWeeklyReport(reportId int, projectManagerId int) error DeleteReport(reportID int) error - ChangeProjectName(projectName string, newProjectName string) error - ChangeUserPassword(username string, password string) error } // This struct is a wrapper type that holds the database connection // Internally DB holds a connection pool, so it's safe for concurrent use type Db struct { - *sqlx.Tx + *sqlx.DB } type UserProjectMember struct { @@ -96,19 +94,8 @@ const removeUserFromProjectQuery = `DELETE FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)` -const reportStatistics = `SELECT SUM(development_time) AS total_development_time, - SUM(meeting_time) AS total_meeting_time, - SUM(admin_time) AS total_admin_time, - SUM(own_work_time) AS total_own_work_time, - SUM(study_time) AS total_study_time, - SUM(testing_time) AS total_testing_time - FROM weekly_reports - WHERE user_id = (SELECT id FROM users WHERE username = ?) - AND project_id = (SELECT id FROM projects WHERE name = ?) - GROUP BY user_id, project_id` - // DbConnect connects to the database -func DbConnect(dbpath string) sqlx.DB { +func DbConnect(dbpath string) Database { // Open the database db, err := sqlx.Connect("sqlite", dbpath) if err != nil { @@ -121,25 +108,7 @@ func DbConnect(dbpath string) sqlx.DB { panic(err) } - return *db -} - -func (d *Db) ReportStatistics(username string, projectName string) (*types.Statistics, error) { - var result types.Statistics - - err := d.Get(&result, reportStatistics, username, projectName) - if err != nil { - return nil, err - } - - serialized, err := json.Marshal(result) - if err != nil { - return nil, err - } - - log.Info(string(serialized)) - - return &result, nil + return &Db{db} } func (d *Db) CheckUser(username string, password string) bool { @@ -243,15 +212,25 @@ func (d *Db) GetProjectId(projectname string) (int, error) { // Creates a new project in the database, associated with a user func (d *Db) AddProject(name string, description string, username string) error { + tx := d.MustBegin() // Insert the project into the database - _, err := d.Exec(projectInsert, name, description, username) + _, err := tx.Exec(projectInsert, name, description, username) if err != nil { + if err := tx.Rollback(); err != nil { + return err + } return err } // Add creator to project as project manager - _, err = d.Exec(addUserToProject, username, name, "project_manager") + _, err = tx.Exec(addUserToProject, username, name, "project_manager") if err != nil { + if err := tx.Rollback(); err != nil { + return err + } + return err + } + if err := tx.Commit(); err != nil { return err } @@ -259,7 +238,16 @@ func (d *Db) AddProject(name string, description string, username string) error } func (d *Db) DeleteProject(projectID string, username string) error { - _, err := d.Exec(deleteProject, projectID, username) + tx := d.MustBegin() + + _, err := tx.Exec(deleteProject, projectID, username) + + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + return fmt.Errorf("error rolling back transaction: %v, delete error: %v", rollbackErr, err) + } + panic(err) + } return err } @@ -412,7 +400,7 @@ func (d *Db) UnsignWeeklyReport(reportId int, projectManagerId int) error { } // Update the signed_by field of the specified report - _, err = d.Exec("UPDATE weekly_reports SET signed_by = NULL WHERE report_id = ?;", reportId) + _, err = d.Exec("UPDATE weekly_reports SET signed_by = NULL WHERE report_id = ?;", projectManagerId, reportId) return err } @@ -483,7 +471,7 @@ func (d *Db) IsSiteAdmin(username string) (bool, error) { // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory -func Migrate(db sqlx.DB) error { +func (d *Db) Migrate() error { // Read the embedded scripts directory files, err := scripts.ReadDir("migrations") if err != nil { @@ -495,7 +483,7 @@ func Migrate(db sqlx.DB) error { return nil } - tr := db.MustBegin() + tr := d.MustBegin() // Iterate over each SQL file and execute it for _, file := range files { @@ -581,7 +569,7 @@ func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, d } // MigrateSampleData applies sample data to the database. -func MigrateSampleData(db sqlx.DB) error { +func (d *Db) MigrateSampleData() error { // Insert sample data files, err := sampleData.ReadDir("sample_data") if err != nil { @@ -591,7 +579,7 @@ func MigrateSampleData(db sqlx.DB) error { if len(files) == 0 { println("No sample data files found") } - tr := db.MustBegin() + tr := d.MustBegin() // Iterate over each SQL file and execute it for _, file := range files { @@ -628,7 +616,7 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { WHERE projects.name = ? ` - rows, err := d.Query(query, projectName) + rows, err := d.DB.Query(query, projectName) if err != nil { return nil, err } @@ -672,14 +660,3 @@ func (d *Db) DeleteReport(reportID int) error { _, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID) return err } - -// ChangeProjectName is a handler that changes the name of a project -func (d *Db) ChangeProjectName(projectName string, newProjectName string) error { - _, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName) - return err -} - -func (d *Db) ChangeUserPassword(username string, password string) error { - _, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username) - return err -} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 7b599f2..b5a598c 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -9,13 +9,11 @@ import ( // setupState initializes a database instance with necessary setup for testing func setupState() (Database, error) { db := DbConnect(":memory:") - err := Migrate(db) + err := db.Migrate() if err != nil { return nil, err } - - db_iface := Db{db.MustBegin()} - return &db_iface, nil + return db, nil } // This is a more advanced setup that includes more data in the database. @@ -1080,7 +1078,7 @@ func TestDeleteReport(t *testing.T) { } // Remove report - err = db.DeleteReport(report.ReportId) + err = db.DeleteReport(report.ReportId,) if err != nil { t.Error("RemoveReport failed:", err) } @@ -1090,55 +1088,5 @@ func TestDeleteReport(t *testing.T) { if err == nil { t.Error("RemoveReport failed: report not removed") } - -} - -func TestChangeProjectName(t *testing.T) { - db, err := setupAdvancedState() - if err != nil { - t.Error("setupState failed:", err) - } - - // Promote user to Admin - err = db.PromoteToAdmin("demouser") - if err != nil { - t.Error("PromoteToAdmin failed:", err) - } - - // Change project name - err = db.ChangeProjectName("projecttest", "newprojectname") - if err != nil { - t.Error("ChangeProjectName failed:", err) - } - - // Check if the project name was changed - projects, err := db.GetAllProjects() - if err != nil { - t.Error("GetAllProjects failed:", err) - } - if projects[0].Name != "newprojectname" { - t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name) - } -} - -func TestChangeUserPassword(t *testing.T) { - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - } - - // Add a user - _ = db.AddUser("testuser", "password") - - // Change user password - err = db.ChangeUserPassword("testuser", "newpassword") - if err != nil { - t.Error("ChangeUserPassword failed:", err) - } - - // Check if the password was changed - if !db.CheckUser("testuser", "newpassword") { - t.Error("ChangeUserPassword failed: password not changed") - } - + } diff --git a/backend/internal/database/middleware.go b/backend/internal/database/middleware.go index b73a42f..69fa3a2 100644 --- a/backend/internal/database/middleware.go +++ b/backend/internal/database/middleware.go @@ -1,28 +1,11 @@ package database -import ( - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/jmoiron/sqlx" -) +import "github.com/gofiber/fiber/v2" -// Simple middleware that provides a transaction as a local key "db" -func DbMiddleware(db *sqlx.DB) func(c *fiber.Ctx) error { +// Simple middleware that provides a shared database pool as a local key "db" +func DbMiddleware(db *Database) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { - tx := db.MustBegin() - - defer func() { - if err := tx.Commit(); err != nil { - if err = tx.Rollback(); err != nil { - log.Error("Failed to rollback transaction: ", err) - } - return - } - }() - - var db_iface Database = &Db{tx} - - c.Locals("db", &db_iface) + c.Locals("db", db) return c.Next() } } diff --git a/backend/internal/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index f519608..70499b0 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -1,220 +1,58 @@ INSERT OR IGNORE INTO users(username, password) -VALUES ("admin", "123"), - ("user", "123"), - ("user2", "123"), - ("John", "123"), - ("Emma", "123"), - ("Michael", "123"), - ("Liam", "123"), - ("Oliver", "123"), - ("Amelia", "123"), - ("Benjamin", "123"), - ("Mia", "123"), - ("Elijah", "123"), - ("Charlotte", "123"), - ("Henry", "123"), - ("Harper", "123"), - ("Lucas", "123"), - ("Emily", "123"), - ("Alexander", "123"), - ("Daniel", "123"), - ("Ella", "123"), - ("Matthew", "123"), - ("Madison", "123"), - ("Samuel", "123"), - ("Avery", "123"), - ("Sofia", "123"), - ("David", "123"), - ("Victoria", "123"), - ("Jackson", "123"), - ("Abigail", "123"), - ("Gabriel", "123"), - ("Luna", "123"), - ("Wyatt", "123"), - ("Chloe", "123"), - ("Nora", "123"), - ("Joshua", "123"), - ("Hazel", "123"), - ("Riley", "123"), - ("Scarlett", "123"), - ("Aria", "123"), - ("Carter", "123"), - ("Grace", "123"), - ("Jayden", "123"), - ("Hannah", "123"), - ("Zoe", "123"), - ("Luke", "123"), - ("Sophia", "123"), - ("Jack", "123"), - ("Isabella", "123"), - ("William", "123"), - ("Mason", "123"), - ("Evelyn", "123"), - ("James", "123"), - ("Cynthia", "123"), - ("Abraham", "123"), - ("Ava", "123"), - ("Aiden", "123"), - ("Natalie", "123"), - ("Lily", "123"), - ("Olivia", "123"), - ("Alexander", "123"), - ("Ethan", "123"), - ("Mila", "123"), - ("Evelyn", "123"), - ("Logan", "123"), - ("Riley", "123"), - ("Grace", "123"), - ("Arnold", "123"), - ("Connor", "123"), - ("Samantha", "123"), - ("Emma", "123"), - ("Sarah", "123"), - ("Nathan", "123"), - ("Layla", "123"), - ("Ryan", "123"), - ("Zoey", "123"), - ("Megan", "123"), - ("Christian", "123"), - ("Eva", "123"), - ("Isaac", "123"), - ("Michaela", "123"), - ("Caroline", "123"), - ("Elijah", "123"), - ("Elena", "123"), - ("Julian", "123"), - ("Sophie", "123"), - ("Gabriella", "123"), - ("Cole", "123"), - ("Hannah", "123"), - ("Lucy", "123"), - ("Katherine", "123"), - ("Benjamin", "123"), - ("Ella", "123"), - ("Evan", "123"); +VALUES ("admin", "123"); -INSERT OR IGNORE INTO projects(name, description, owner_user_id) -VALUES ("projecttest1", "Description for projecttest1", 1), - ("projecttest2", "Description for projecttest2", 1), - ("projecttest3", "Description for projecttest3", 1), - ("projecttest4", "Description for projecttest4", 1), - ("projecttest5", "Description for projecttest5", 1), - ("projecttest6", "Description for projecttest6", 1), - ("projecttest7", "Description for projecttest7", 1), - ("projecttest8", "Description for projecttest8", 1), - ("projecttest9", "Description for projecttest9", 1), - ("projecttest10", "Description for projecttest10", 1), - ("projecttest11", "Description for projecttest11", 1), - ("projecttest12", "Description for projecttest12", 1), - ("projecttest13", "Description for projecttest13", 1), - ("projecttest14", "Description for projecttest14", 1), - ("projecttest15", "Description for projecttest15", 1); +INSERT OR IGNORE INTO users(username, password) +VALUES ("user", "123"); + +INSERT OR IGNORE INTO users(username, password) +VALUES ("user2", "123"); + +INSERT OR IGNORE INTO site_admin VALUES (1); + +INSERT OR IGNORE INTO projects(name,description,owner_user_id) +VALUES ("projecttest","test project", 1); + +INSERT OR IGNORE INTO projects(name,description,owner_user_id) +VALUES ("projecttest2","test project2", 1); + +INSERT OR IGNORE INTO projects(name,description,owner_user_id) +VALUES ("projecttest3","test project3", 1); INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (1,1,"project_manager"), - (1,2,"project_manager"), - (1,3,"project_manager"), - (1,4,"project_manager"), - (1,5,"project_manager"), - (1,6,"project_manager"), - (1,7,"project_manager"), - (1,8,"project_manager"), - (1,9,"project_manager"), - (1,10,"project_manager"), - (1,11,"project_manager"), - (1,12,"project_manager"), - (1,13,"project_manager"), - (1,14,"project_manager"), - (1,15,"project_manager"), - (2,1,"project_manager"), - (2,2,"member"), - (2,3,"member"), - (2,4,"member"), - (2,5,"member"), - (2,6,"member"), - (2,7,"member"), - (2,8,"member"), - (2,9,"member"), - (2,10,"member"), - (2,11,"member"), - (2,12,"member"), - (2,13,"member"), - (2,14,"member"), - (2,15,"member"), - (3,1,"member"), - (3,2,"member"), - (3,3,"member"), - (3,4,"member"), - (3,5,"member"), - (3,6,"member"), - (3,7,"member"), - (3,8,"member"), - (3,9,"member"), - (3,10,"member"), - (3,11,"member"), - (3,12,"member"), - (3,13,"member"), - (3,14,"member"), - (3,15,"member"), - (4,1,"member"), - (4,2,"member"), - (4,3,"member"), - (4,4,"member"), - (4,5,"member"), - (4,6,"member"), - (4,7,"member"), - (4,8,"member"), - (4,9,"member"), - (4,10,"member"), - (4,11,"member"), - (4,12,"member"), - (4,13,"member"), - (4,14,"member"), - (4,15,"member"), - (5,1,"member"), - (5,2,"member"), - (5,3,"member"), - (5,4,"member"), - (5,5,"member"), - (5,6,"member"), - (5,7,"member"), - (5,8,"member"), - (5,9,"member"), - (5,10,"member"), - (5,11,"member"), - (5,12,"member"), - (5,13,"member"), - (5,14,"member"), - (5,15,"member"); +VALUES (1,1,"project_manager"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (1,2,"project_manager"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (1,3,"project_manager"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (2,1,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (3,1,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (3,2,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (3,3,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (2,1,"project_manager"); INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) -VALUES (2, 1, 12, 100, 50, 30, 150, 80, 20, NULL), - (3, 1, 12, 200, 80, 20, 200, 100, 30, NULL), - (3, 1, 14, 150, 70, 40, 180, 90, 25, NULL), - (3, 2, 12, 120, 60, 35, 160, 85, 15, NULL), - (3, 3, 12, 180, 90, 25, 190, 110, 40, NULL), - (2, 1, 13, 130, 70, 40, 170, 95, 35, NULL), - (3, 1, 15, 140, 60, 50, 200, 120, 30, NULL), - (2, 2, 11, 110, 50, 45, 140, 70, 25, NULL), - (3, 3, 14, 170, 80, 30, 180, 100, 35, NULL), - (3, 3, 15, 200, 100, 20, 220, 130, 45, NULL), - (2, 4, 12, 120, 60, 40, 160, 80, 30, NULL), - (3, 5, 14, 150, 70, 30, 180, 90, 25, NULL), - (3, 5, 15, 180, 90, 20, 190, 110, 35, NULL), - (2, 6, 11, 100, 50, 35, 130, 60, 20, NULL), - (3, 7, 14, 170, 80, 25, 180, 100, 30, NULL), - (2, 8, 12, 130, 70, 30, 170, 90, 25, NULL), - (2, 8, 13, 150, 80, 20, 180, 110, 35, NULL), - (3, 9, 12, 140, 60, 40, 180, 100, 30, NULL), - (3, 10, 11, 120, 50, 45, 150, 70, 25, NULL), - (2, 11, 13, 110, 60, 35, 140, 80, 30, NULL), - (3, 12, 12, 160, 70, 30, 180, 100, 35, NULL), - (3, 12, 13, 180, 90, 25, 190, 110, 40, NULL), - (3, 12, 14, 200, 100, 20, 220, 130, 45, NULL), - (2, 13, 11, 100, 50, 45, 130, 60, 20, NULL), - (2, 13, 12, 120, 60, 40, 160, 80, 30, NULL), - (3, 14, 13, 140, 70, 30, 160, 90, 35, NULL), - (3, 15, 12, 150, 80, 25, 180, 100, 30, NULL), - (3, 15, 13, 170, 90, 20, 190, 110, 35, NULL); +VALUES (2, 1, 12, 20, 10, 5, 30, 15, 10, NULL); -INSERT OR IGNORE INTO site_admin VALUES (1); \ No newline at end of file +INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) +VALUES (3, 1, 12, 20, 10, 5, 30, 15, 10, NULL); + +INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) +VALUES (3, 1, 14, 20, 10, 5, 30, 15, 10, NULL); + +INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) +VALUES (3, 2, 12, 20, 10, 5, 30, 15, 10, NULL); + +INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) +VALUES (3, 3, 12, 20, 10, 5, 30, 15, 10, NULL); diff --git a/backend/internal/handlers/projects/ChangeProjectName.go b/backend/internal/handlers/projects/ChangeProjectName.go deleted file mode 100644 index f6831db..0000000 --- a/backend/internal/handlers/projects/ChangeProjectName.go +++ /dev/null @@ -1,43 +0,0 @@ -package projects - -import ( - db "ttime/internal/database" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -// ChangeProjectName is a handler that changes the name of a project -func ChangeProjectName(c *fiber.Ctx) error { - - //check token and get username of current user - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract the necessary parameters from the request - projectName := c.Params("projectName") - newProjectName := c.Query("newProjectName") - - // Check if user is site admin - issiteadmin, err := db.GetDb(c).IsSiteAdmin(username) - if err != nil { - log.Warn("Error checking if siteadmin:", err) - return c.Status(500).SendString(err.Error()) - } else if !issiteadmin { - log.Warn("User is not siteadmin") - return c.Status(401).SendString("User is not siteadmin") - } - - - // Perform the project name change - err = db.GetDb(c).ChangeProjectName(projectName, newProjectName) - if err != nil { - log.Warn("Error changing project name:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.Status(200).SendString("Project name changed successfully") -} diff --git a/backend/internal/handlers/reports/Statistics.go b/backend/internal/handlers/reports/Statistics.go deleted file mode 100644 index dac017d..0000000 --- a/backend/internal/handlers/reports/Statistics.go +++ /dev/null @@ -1,56 +0,0 @@ -package reports - -import ( - db "ttime/internal/database" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -func GetStatistics(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract project name from query parameters - projectName := c.Query("projectName") - userNameParam := c.Query("userName") - - log.Info(username, " trying to get statistics for project: ", projectName) - - if projectName == "" { - log.Info("Missing project name") - return c.Status(400).SendString("Missing project name") - } - - // Check if the user is a project manager - pm, err := db.GetDb(c).IsProjectManager(username, projectName) - if err != nil { - log.Info("Error checking if user is project manager:", err) - return c.Status(500).SendString(err.Error()) - } - - // Bail if the user is not a PM or checking its own statistics - if !pm && userNameParam != "" && userNameParam != username { - log.Info("Unauthorized access for user: ", username, "trying to access project: ", projectName, "statistics for user: ", userNameParam) - return c.Status(403).SendString("Unauthorized access") - } - - if pm && userNameParam != "" { - username = userNameParam - } - - // Retrieve statistics for the project from the database - statistics, err := db.GetDb(c).ReportStatistics(username, projectName) - if err != nil { - log.Error("Error getting statistics for project:", projectName, ":", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning statistics") - // Return the retrieved statistics - return c.JSON(statistics) - -} diff --git a/backend/internal/handlers/reports/UnsignReport.go b/backend/internal/handlers/reports/UnsignReport.go index 45943de..ea0f480 100644 --- a/backend/internal/handlers/reports/UnsignReport.go +++ b/backend/internal/handlers/reports/UnsignReport.go @@ -36,6 +36,6 @@ func UnsignReport(c *fiber.Ctx) error { return c.Status(500).SendString(err.Error()) } - log.Info("Project manager ID: ", projectManagerID, " unsigned report ID: ", reportId) + log.Info("Project manager ID: ", projectManagerID, " signed report ID: ", reportId) return c.Status(200).SendString("Weekly report unsigned successfully") } diff --git a/backend/internal/handlers/users/ChangeUserPassword.go b/backend/internal/handlers/users/ChangeUserPassword.go deleted file mode 100644 index 1596247..0000000 --- a/backend/internal/handlers/users/ChangeUserPassword.go +++ /dev/null @@ -1,42 +0,0 @@ -package users - -import ( - db "ttime/internal/database" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -// ChangeUserPassword is a handler that changes the password of a user -func ChangeUserPassword(c *fiber.Ctx) error { - - //Check token and get username of current user - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - admin := claims["name"].(string) - - // Extract the necessary parameters from the request - username := c.Params("username") - newPassword := c.Query("newPassword") - - // Check if user is site admin - issiteadmin, err := db.GetDb(c).IsSiteAdmin(admin) - if err != nil { - log.Warn("Error checking if siteadmin:", err) - return c.Status(500).SendString(err.Error()) - } else if !issiteadmin { - log.Warn("User is not siteadmin") - return c.Status(401).SendString("User is not siteadmin") - } - - // Perform the password change - err = db.GetDb(c).ChangeUserPassword(username, newPassword) - if err != nil { - log.Warn("Error changing password:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.Status(200).SendString("Password changed successfully") -} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 5550b3f..234781b 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -66,15 +66,6 @@ type WeeklyReport struct { SignedBy *int `json:"signedBy" db:"signed_by"` } -type Statistics struct { - TotalDevelopmentTime int `json:"totalDevelopmentTime" db:"total_development_time"` - TotalMeetingTime int `json:"totalMeetingTime" db:"total_meeting_time"` - TotalAdminTime int `json:"totalAdminTime" db:"total_admin_time"` - TotalOwnWorkTime int `json:"totalOwnWorkTime" db:"total_own_work_time"` - TotalStudyTime int `json:"totalStudyTime" db:"total_study_time"` - TotalTestingTime int `json:"totalTestingTime" db:"total_testing_time"` -} - type UpdateWeeklyReport struct { // The name of the project, as it appears in the database ProjectName string `json:"projectName"` diff --git a/backend/main.go b/backend/main.go index 0c66369..dbf5151 100644 --- a/backend/main.go +++ b/backend/main.go @@ -59,13 +59,13 @@ func main() { db := database.DbConnect(conf.DbPath) // Migrate the database - if err = database.Migrate(db); err != nil { + if err = db.Migrate(); err != nil { fmt.Println("Error migrating database: ", err) os.Exit(1) } // Migrate sample data, should not be used in production - if err = database.MigrateSampleData(db); err != nil { + if err = db.MigrateSampleData(); err != nil { fmt.Println("Error migrating sample data: ", err) os.Exit(1) } @@ -110,7 +110,6 @@ func main() { api.Post("/promoteToAdmin", users.PromoteToAdmin) api.Put("/changeUserName", users.ChangeUserName) api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches - api.Put("/changeUserPassword/:username", users.ChangeUserPassword) // All project related routes // projectGroup := api.Group("/project") // Not currently in use @@ -126,14 +125,13 @@ func main() { api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) - api.Put("/changeProjectName/:projectName", projects.ChangeProjectName) + // All report related routes // reportGroup := api.Group("/report") // Not currently in use api.Get("/getWeeklyReport", reports.GetWeeklyReport) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports) - api.Get("/getStatistics", reports.GetStatistics) api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Put("/signReport/:reportId", reports.SignReport) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 67768c2..de5f19d 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -11,7 +11,6 @@ import { NewProject, WeeklyReport, StrNameChange, - Statistics, } from "../Types/goTypes"; /** @@ -259,42 +258,6 @@ interface API { reportId: number, token: string, ): Promise>; - - /** - * Retrieves the total time spent on a project for a particular user (the user is determined by the token) - * - * @param {string} projectName The name of the project - * @param {string} token The authentication token - */ - getStatistics( - projectName: string, - token: string, - userName?: string, - ): Promise>; - - /** - * Changes the name of a project - * @param {string} projectName The name of the project - * @param {string} newProjectName The new name of the project - * @param {string} token The authentication token - */ - changeProjectName( - projectName: string, - newProjectName: string, - token: string, - ): Promise>; - - /** - * Changes the password of a user - * @param {string} username The username of the user - * @param {string} newPassword The new password - * @param {string} token The authentication token - */ - changeUserPassword( - username: string, - newPassword: string, - token: string, - ): Promise>; } /** An instance of the API */ @@ -701,11 +664,7 @@ export const api: API = { }); if (!response.ok) { - return { - success: false, - data: `${response.status}`, - message: "Failed to login", - }; + return { success: false, message: "Failed to login" }; } else { const data = (await response.json()) as { token: string }; // Update the type of 'data' return { success: true, data: data.token }; @@ -1003,85 +962,4 @@ export const api: API = { return { success: false, message: "Failed to delete report" }; } }, - async getStatistics( - projectName: string, - token: string, - userName?: string, - ): Promise> { - try { - const response = await fetch( - `/api/getStatistics/?projectName=${projectName}&userName=${userName ?? ""}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }, - ); - - if (!response.ok) { - return { success: false, message: "Failed to get statistics" }; - } else { - const data = (await response.json()) as Statistics; - return { success: true, data }; - } - } catch (e) { - return { success: false, message: "Failed to get statistics" }; - } - }, - - async changeProjectName( - projectName: string, - newProjectName: string, - token: string, - ): Promise> { - try { - const response = await fetch( - `/api/changeProjectName/${projectName}?newProjectName=${newProjectName}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }, - ); - - if (!response.ok) { - return { success: false, message: "Failed to change project name" }; - } else { - return { success: true, message: "Project name changed" }; - } - } catch (e) { - return { success: false, message: "Failed to change project name" }; - } - }, - - async changeUserPassword( - username: string, - newPassword: string, - token: string, - ): Promise> { - try { - const response = await fetch( - `/api/changeUserPassword/${username}?newPassword=${newPassword}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }, - ); - - if (!response.ok) { - return { success: false, message: "Failed to change password" }; - } else { - return { success: true, message: "Password changed" }; - } - } catch (e) { - return { success: false, message: "Failed to change password" }; - } - }, }; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index 8c9c566..c8a1c66 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -1,13 +1,9 @@ import { useState } from "react"; import { api } from "../API/API"; import { NewProject } from "../Types/goTypes"; +import InputField from "./InputField"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -import { useNavigate } from "react-router-dom"; -import ProjectNameInput from "./Inputs/ProjectNameInput"; -import DescriptionInput from "./Inputs/DescriptionInput"; -import { alphanumeric } from "../Data/regex"; -import { projNameHighLimit, projNameLowLimit } from "../Data/constants"; /** * Provides UI for adding a project to the system. @@ -16,26 +12,11 @@ import { projNameHighLimit, projNameLowLimit } from "../Data/constants"; function AddProject(): JSX.Element { const [name, setName] = useState(""); const [description, setDescription] = useState(""); - const navigate = useNavigate(); /** * Tries to add a project to the system */ const handleCreateProject = async (): Promise => { - if ( - !alphanumeric.test(name) || - name.length > projNameHighLimit || - name.length < projNameLowLimit - ) { - alert( - "Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)", - ); - return; - } - if (description.length > projNameHighLimit) { - alert("Please provide valid description: \n-Max 100 characters"); - return; - } const project: NewProject = { name: name.replace(/ /g, ""), description: description.trim(), @@ -49,7 +30,6 @@ function AddProject(): JSX.Element { alert(`${project.name} added!`); setDescription(""); setName(""); - navigate("/admin"); } else { alert("Project not added, name could be taken"); console.error(response.message); @@ -64,7 +44,7 @@ function AddProject(): JSX.Element {
{ e.preventDefault(); void handleCreateProject(); @@ -72,29 +52,33 @@ function AddProject(): JSX.Element { > TTIME Logo

Create a new project

- { - e.preventDefault(); - setName(e.target.value); - }} - /> -
- { - e.preventDefault(); - setDescription(e.target.value); - }} - placeholder={"Description (Optional)"} - /> -
+
+ { + e.preventDefault(); + setName(e.target.value); + }} + /> + { + e.preventDefault(); + setDescription(e.target.value); + }} + /> +
+
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 5037a76..d56ee42 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -18,13 +18,12 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const { projectName, fetchedWeek, signedOrUnsigned } = useParams<{ + const { projectName, fetchedWeek } = useParams<{ projectName: string; fetchedWeek: string; - signedOrUnsigned: string; }>(); const username = localStorage.getItem("userName") ?? ""; - console.log(projectName, fetchedWeek, signedOrUnsigned); + console.log(projectName, fetchedWeek); useEffect(() => { const fetchWeeklyReport = async (): Promise => { @@ -60,7 +59,7 @@ export default function GetWeeklyReport(): JSX.Element { }; void fetchWeeklyReport(); - }, [projectName, fetchedWeek, signedOrUnsigned, token]); + }, [projectName, fetchedWeek, token]); const handleUpdateWeeklyReport = async (): Promise => { const updateWeeklyReport: UpdateWeeklyReport = { @@ -140,12 +139,6 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} - onClick={() => { - if (signedOrUnsigned === "signed") { - alert("You cannot edit a signed report."); - } - }} - readOnly={signedOrUnsigned === "signed"} /> @@ -175,12 +168,6 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} - onClick={() => { - if (signedOrUnsigned === "signed") { - alert("You cannot edit a signed report."); - } - }} - readOnly={signedOrUnsigned === "signed"} /> @@ -210,12 +197,6 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} - onClick={() => { - if (signedOrUnsigned === "signed") { - alert("You cannot edit a signed report."); - } - }} - readOnly={signedOrUnsigned === "signed"} /> @@ -245,12 +226,6 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} - onClick={() => { - if (signedOrUnsigned === "signed") { - alert("You cannot edit a signed report."); - } - }} - readOnly={signedOrUnsigned === "signed"} /> @@ -280,12 +255,6 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} - onClick={() => { - if (signedOrUnsigned === "signed") { - alert("You cannot edit a signed report."); - } - }} - readOnly={signedOrUnsigned === "signed"} /> @@ -315,26 +284,18 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} - onClick={() => { - if (signedOrUnsigned === "signed") { - alert("You cannot edit a signed report."); - } - }} - readOnly={signedOrUnsigned === "signed"} /> - {signedOrUnsigned !== "signed" && ( -
diff --git a/frontend/src/Components/InputField.tsx b/frontend/src/Components/InputField.tsx index 5a5cdaf..699d8fa 100644 --- a/frontend/src/Components/InputField.tsx +++ b/frontend/src/Components/InputField.tsx @@ -4,21 +4,19 @@ * @returns {JSX.Element} The input field * @example * { * setExample(e.target.value); * }} + * value={example} * /> */ function InputField(props: { - label?: string; - placeholder?: string; - type?: string; - value?: string; - onChange?: (e: React.ChangeEvent) => void; + label: string; + type: string; + value: string; + onChange: (e: React.ChangeEvent) => void; }): JSX.Element { return (
@@ -32,7 +30,7 @@ function InputField(props: { className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id={props.label} type={props.type} - placeholder={props.placeholder} + placeholder={props.label} value={props.value} onChange={props.onChange} /> diff --git a/frontend/src/Components/Inputs/DescriptionInput.tsx b/frontend/src/Components/Inputs/DescriptionInput.tsx deleted file mode 100644 index 43e046c..0000000 --- a/frontend/src/Components/Inputs/DescriptionInput.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { projDescHighLimit, projDescLowLimit } from "../../Data/constants"; - -export default function DescriptionInput(props: { - desc: string; - placeholder: string; - onChange: (e: React.ChangeEvent) => void; -}): JSX.Element { - return ( - <> - -
- {props.desc.length > projDescHighLimit && ( -

- Description must be under 100 characters -

- )} - {props.desc.length <= projDescHighLimit && - props.desc.length > projDescLowLimit && ( -

- Valid project description! -

- )} -
- - ); -} diff --git a/frontend/src/Components/Inputs/PasswordInput.tsx b/frontend/src/Components/Inputs/PasswordInput.tsx deleted file mode 100644 index 9f67e98..0000000 --- a/frontend/src/Components/Inputs/PasswordInput.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { passwordLength } from "../../Data/constants"; -import { lowercase } from "../../Data/regex"; - -export default function PasswordInput(props: { - password: string; - onChange: (e: React.ChangeEvent) => void; -}): JSX.Element { - const password = props.password; - return ( - <> - -
- {password.length === passwordLength && - lowercase.test(props.password) && ( -

- Valid password! -

- )} - {password.length !== passwordLength && ( -

- Password must be 6 characters -

- )} - {!lowercase.test(password) && password !== "" && ( -

- No number, uppercase or special
characters allowed -

- )} -
- - ); -} diff --git a/frontend/src/Components/Inputs/ProjectNameInput.tsx b/frontend/src/Components/Inputs/ProjectNameInput.tsx deleted file mode 100644 index de28c12..0000000 --- a/frontend/src/Components/Inputs/ProjectNameInput.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { projNameHighLimit, projNameLowLimit } from "../../Data/constants"; -import { alphanumeric } from "../../Data/regex"; - -export default function ProjectNameInput(props: { - name: string; - onChange: (e: React.ChangeEvent) => void; -}): JSX.Element { - const name = props.name; - return ( - <> - = projNameLowLimit && - name.length <= projNameHighLimit && - alphanumeric.test(name) - ? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight" - : "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight" - } - spellCheck="false" - id="New name" - type="text" - placeholder="Project name" - value={name} - onChange={props.onChange} - /> -
- {!alphanumeric.test(name) && name !== "" && ( -

- No special characters allowed -

- )} - {(name.length < projNameLowLimit || - name.length > projNameHighLimit) && ( -

- Project name must be 10-99 characters -

- )} - {alphanumeric.test(props.name) && - name.length >= projNameLowLimit && - name.length <= projNameHighLimit && ( -

- Valid project name! -

- )} -
- - ); -} diff --git a/frontend/src/Components/Inputs/UsernameInput.tsx b/frontend/src/Components/Inputs/UsernameInput.tsx deleted file mode 100644 index 8f653ba..0000000 --- a/frontend/src/Components/Inputs/UsernameInput.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { usernameLowLimit, usernameUpLimit } from "../../Data/constants"; -import { alphanumeric } from "../../Data/regex"; - -export default function UsernameInput(props: { - username: string; - onChange: (e: React.ChangeEvent) => void; -}): JSX.Element { - const username = props.username; - return ( - <> - = usernameLowLimit && - username.length <= usernameUpLimit && - alphanumeric.test(props.username) - ? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight" - : "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight" - } - spellCheck="false" - id="New username" - type="text" - placeholder="Username" - value={username} - onChange={props.onChange} - /> -
- {alphanumeric.test(username) && - username.length >= usernameLowLimit && - username.length <= usernameUpLimit && ( -

- Valid username! -

- )} - {!alphanumeric.test(username) && username !== "" && ( -

- No special characters allowed -

- )} - {!( - username.length >= usernameLowLimit && - username.length <= usernameUpLimit - ) && ( -

- Username must be 5-10 characters -

- )} -
- - ); -} diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx index 50ffb98..f44d7f3 100644 --- a/frontend/src/Components/LoginCheck.tsx +++ b/frontend/src/Components/LoginCheck.tsx @@ -11,10 +11,6 @@ function LoginCheck(props: { password: string; setAuthority: Dispatch>; }): void { - if (props.username === "" || props.password === "") { - alert("Please enter username and password to login"); - return; - } const user: NewUser = { username: props.username, password: props.password, @@ -46,15 +42,7 @@ function LoginCheck(props: { console.error("Token was undefined"); } } else { - if (response.data === "500") { - console.error(response.message); - alert("No connection/Error"); - } else { - console.error( - "Token could not be fetched/No such user" + response.message, - ); - alert("Incorrect login information"); - } + console.error("Token could not be fetched/No such user"); } }) .catch((error) => { diff --git a/frontend/src/Components/LoginField.tsx b/frontend/src/Components/LoginField.tsx index 8d0aa62..dda1714 100644 --- a/frontend/src/Components/LoginField.tsx +++ b/frontend/src/Components/LoginField.tsx @@ -33,7 +33,6 @@ function Login(props: { props.setUsername(e.target.value); }} value={props.username} - placeholder={"Username"} />
- ); -} diff --git a/frontend/src/Components/OtherUsersTR.tsx b/frontend/src/Components/OtherUsersTR.tsx index 40e0b94..ce7761c 100644 --- a/frontend/src/Components/OtherUsersTR.tsx +++ b/frontend/src/Components/OtherUsersTR.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from "react"; import { WeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; -import { useParams, useNavigate } from "react-router-dom"; -import Button from "./Button"; +import { useParams } from "react-router-dom"; /** * Renders the component for editing a weekly report. @@ -18,14 +17,11 @@ export default function OtherUsersTR(): JSX.Element { const [ownWorkTime, setOwnWorkTime] = useState(0); const [studyTime, setStudyTime] = useState(0); const [testingTime, setTestingTime] = useState(0); - const [reportId, setReportId] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; const { projectName } = useParams(); const { username } = useParams(); const { fetchedWeek } = useParams(); - const { signedOrUnsigned } = useParams(); - console.log(projectName, username, fetchedWeek, signedOrUnsigned); useEffect(() => { const fetchUsersWeeklyReport = async (): Promise => { @@ -49,7 +45,6 @@ export default function OtherUsersTR(): JSX.Element { studyTime: 0, testingTime: 0, }; - setReportId(report.reportId); setWeek(report.week); setDevelopmentTime(report.developmentTime); setMeetingTime(report.meetingTime); @@ -65,27 +60,6 @@ export default function OtherUsersTR(): JSX.Element { void fetchUsersWeeklyReport(); }); - const handleUnsignWeeklyReport = async (): Promise => { - const response = await api.unsignReport(reportId, token); - console.log(response); - console.log(reportId); - if (response.success) { - return true; - } else { - return false; - } - }; - - const handleDeleteWeeklyReport = async (): Promise => { - const response = await api.deleteWeeklyReport(reportId, token); - console.log(response); - if (response.success) { - return true; - } - return false; - }; - const navigate = useNavigate(); - return ( <>

{username}'s Report

@@ -179,48 +153,6 @@ export default function OtherUsersTR(): JSX.Element { -
- {signedOrUnsigned === "signed" && ( -
diff --git a/frontend/src/Components/PMProjectMenu.tsx b/frontend/src/Components/PMProjectMenu.tsx index f0cb492..ce7c5c5 100644 --- a/frontend/src/Components/PMProjectMenu.tsx +++ b/frontend/src/Components/PMProjectMenu.tsx @@ -8,22 +8,22 @@ function PMProjectMenu(): JSX.Element {

{projectName}

-

+

Your Time Reports

-

+

New Time Report

-

+

Statistics

-

+

Unsigned Time Reports

diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index 4be3397..1f98d79 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -4,62 +4,19 @@ import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; import { Link } from "react-router-dom"; import GetProjectTimes, { projectTimes } from "./GetProjectTimes"; import DeleteProject from "./DeleteProject"; -import InputField from "./InputField"; -import ProjectNameInput from "./Inputs/ProjectNameInput"; -import { alphanumeric } from "../Data/regex"; -import { projNameHighLimit, projNameLowLimit } from "../Data/constants"; -import ChangeProjectName from "./ChangeProjectName"; function ProjectInfoModal(props: { projectname: string; onClose: () => void; - onClick: (username: string, userRole: string) => void; + onClick: (username: string) => void; }): JSX.Element { - const [showInput, setShowInput] = useState(false); const [users, setUsers] = useState([]); const [times, setTimes] = useState(); - const [search, setSearch] = useState(""); - const [newProjName, setNewProjName] = useState(""); const totalTime = useRef(0); GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); GetProjectTimes({ setTimesProp: setTimes, projectName: props.projectname }); - const handleChangeNameView = (): void => { - if (showInput) { - setNewProjName(""); - setShowInput(false); - } else { - setShowInput(true); - } - }; - - const handleClickChangeName = (): void => { - if ( - newProjName.length > projNameHighLimit || - newProjName.length < projNameLowLimit || - !alphanumeric.test(newProjName) - ) { - alert( - "Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)", - ); - return; - } - - if ( - confirm( - `Are you sure you want to change name of ${props.projectname} to ${newProjName}?`, - ) - ) { - ChangeProjectName({ - projectName: props.projectname, - newProjectName: newProjName, - }); - } else { - alert("Name was not changed!"); - } - }; - useEffect(() => { if (times?.totalTime !== undefined) { totalTime.current = times.totalTime; @@ -71,88 +28,44 @@ function ProjectInfoModal(props: { className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm flex justify-center items-center" > -
+
-

{props.projectname}

-

- (Change project name) -

- {showInput && ( - <> -

Change name:

-
- -
-
-
- - )} -

Statistics:

-
-

Number of members: {users.length}

-

+

+ {props.projectname} +

+
+

Statistics:

+
+
+

Number of members: {users.length}

+

Total time reported:{" "} {Math.floor(totalTime.current / 60 / 24) + " d "} {Math.floor((totalTime.current / 60) % 24) + " h "} {(totalTime.current % 60) + " m "}

-

Project members:

-
- { - setSearch(e.target.value); - }} - /> -
    - {users - .filter((user) => { - return search.toLowerCase() === "" - ? user.Username - : user.Username.toLowerCase().includes( - search.toLowerCase(), - ); - }) - .map((user) => ( -
  • { - props.onClick(user.Username, user.UserRole); - }} - > - - Name: {user.Username} -
    - Role: {user.UserRole} -
    -
  • - ))} +
    +

    Project members:

    +
    +
    +
      +
      + {users.map((user) => ( +
    • { + props.onClick(user.Username); + }} + > + + Name: {user.Username} +
      + Role: {user.UserRole} +
      +
    • + ))}
    diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index 6461dae..294a131 100644 --- a/frontend/src/Components/ProjectListAdmin.tsx +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -2,7 +2,6 @@ import { useState } from "react"; import { NewProject } from "../Types/goTypes"; import ProjectInfoModal from "./ProjectInfoModal"; import MemberInfoModal from "./MemberInfoModal"; -import InputField from "./InputField"; /** * A list of projects for admin manage projects page, that sets an onClick @@ -22,12 +21,9 @@ export function ProjectListAdmin(props: { const [projectName, setProjectName] = useState(""); const [userModalVisible, setUserModalVisible] = useState(false); const [username, setUsername] = useState(""); - const [userRole, setUserRole] = useState(""); - const [search, setSearch] = useState(""); - const handleClickUser = (username: string, userRole: string): void => { + const handleClickUser = (username: string): void => { setUsername(username); - setUserRole(userRole); setUserModalVisible(true); }; @@ -43,13 +39,11 @@ export function ProjectListAdmin(props: { const handleCloseUser = (): void => { setUsername(""); - setUserRole(""); setUserModalVisible(false); }; return ( <> -

    Manage Projects

    {projectModalVisible && ( )}
    - { - setSearch(e.target.value); - }} - /> -
      - {props.projects - .filter((project) => { - return search.toLowerCase() === "" - ? project.name - : project.name.toLowerCase().includes(search.toLowerCase()); - }) - .map((project) => ( -
    • { - handleClickProject(project.name); - }} - > - {project.name} -
    • - ))} +
        + {props.projects.map((project) => ( +
      • { + handleClickProject(project.name); + }} + > + {project.name} +
      • + ))}
    diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index e06ed75..52e8559 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import { Link, useParams } from "react-router-dom"; import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; -import { api } from "../API/API"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); @@ -12,68 +11,34 @@ function ProjectMembers(): JSX.Element { setUsersProp: setProjectMembers, }); - const handleUserDeleteClick = async (username: string): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.removeUserFromProject( - username, - projectName ?? "", - token, - ); - console.log(response.data); - - // Remove the deleted user from the state - setProjectMembers((prevMembers) => - prevMembers.filter((member) => member.Username !== username), - ); - }; - return ( <>

    All Members In: {projectName}{" "}

    - {projectMembers.map((projectMember: ProjectMember, index: number) => { - if (projectMember.Username === "admin") { - return null; // Skip rendering for admin user - } - return ( -

    -
    -
    -

    {projectMember.Username}

    - Role: -

    {projectMember.UserRole}

    -
    -
    -
    - {projectMember.Username !== - localStorage.getItem("username") && ( -

    { - confirm( - "Are you sure you want to delete this user? This action cannot be undone.", - ) && - void handleUserDeleteClick(projectMember.Username); - }} - > - Delete User -

    - )} - -

    - View Reports -

    - -
    + {projectMembers.map((projectMember: ProjectMember, index: number) => ( +

    +
    +
    +

    {projectMember.Username}

    + Role: +

    {projectMember.UserRole}

    +
    +
    +
    + +

    + View Reports +

    +
    -

    - ); - })} +
    +

    + ))}
    ); diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 7310e4f..be35a74 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -3,44 +3,25 @@ import { NewUser } from "../Types/goTypes"; import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -import UsernameInput from "./Inputs/UsernameInput"; -import PasswordInput from "./Inputs/PasswordInput"; -import { alphanumeric, lowercase } from "../Data/regex"; -import { - passwordLength, - usernameLowLimit, - usernameUpLimit, -} from "../Data/constants"; +import InputField from "./InputField"; /** * Renders a registration form for the admin to add new users in. * @returns The JSX element representing the registration form. */ export default function Register(): JSX.Element { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [errMessage, setErrMessage] = useState(""); + const [username, setUsername] = useState(); + const [password, setPassword] = useState(); + const [errMessage, setErrMessage] = useState(); const handleRegister = async (): Promise => { - if ( - username.length > usernameUpLimit || - username.length < usernameLowLimit || - !alphanumeric.test(username) - ) { - alert( - "Please provide valid username: \n-Between 5-10 characters \n-No special characters (.-!?/*)", - ); - return; - } - if (password.length !== passwordLength || !lowercase.test(password)) { - alert( - "Please provide valid password: \n-Exactly 6 characters \n-No uppercase letters \n-No numbers \n-No special characters (.-!?/*)", - ); + if (username === "" || password === "") { + alert("Must provide username and password"); return; } const newUser: NewUser = { - username: username, - password: password, + username: username?.replace(/ /g, "") ?? "", + password: password ?? "", }; const response = await api.registerUser(newUser); if (response.success) { @@ -58,7 +39,7 @@ export default function Register(): JSX.Element {
    { e.preventDefault(); void handleRegister(); @@ -66,28 +47,31 @@ export default function Register(): JSX.Element { > TTIME Logo

    Register New User

    - - { - setUsername(e.target.value); - }} - /> -
    - { - setPassword(e.target.value); - }} - /> - -
    +
    + { + setUsername(e.target.value); + }} + /> + { + setPassword(e.target.value); + }} + /> +
    +
    - )}

    Member of these projects:

    @@ -189,9 +87,7 @@ function UserInfoModal(props: { text={"Close"} onClick={function (): void { setNewUsername(""); - setNewPassword(""); - setShowNameInput(false); - setShowPwordInput(false); + setShowInput(false); props.onClose(); }} type="button" diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index 23e49db..76cae9f 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import UserInfoModal from "./UserInfoModal"; -import InputField from "./InputField"; /** * A list of users for admin manage users page, that sets an onClick @@ -16,7 +15,6 @@ import InputField from "./InputField"; export function UserListAdmin(props: { users: string[] }): JSX.Element { const [modalVisible, setModalVisible] = useState(false); const [username, setUsername] = useState(""); - const [search, setSearch] = useState(""); const handleClick = (username: string): void => { setUsername(username); @@ -30,39 +28,24 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element { return ( <> -

    Manage Users

    - { - setSearch(e.target.value); - }} - /> -
      - {props.users - .filter((user) => { - return search.toLowerCase() === "" - ? user - : user.toLowerCase().includes(search.toLowerCase()); - }) - .map((user) => ( -
    • { - handleClick(user); - }} - > - {user} -
    • - ))} +
        + {props.users.map((user) => ( +
      • { + handleClick(user); + }} + > + {user} +
      • + ))}
    diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx index 8f28ce9..bc85c5b 100644 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -8,7 +8,7 @@ function UserProjectListAdmin(props: { username: string }): JSX.Element { GetProjects({ setProjectsProp: setProjects, username: props.username }); return ( -
    +
      {projects.map((project) => (
    • diff --git a/frontend/src/Components/UserProjectMenu.tsx b/frontend/src/Components/UserProjectMenu.tsx index 4be4dee..e307e90 100644 --- a/frontend/src/Components/UserProjectMenu.tsx +++ b/frontend/src/Components/UserProjectMenu.tsx @@ -16,12 +16,12 @@ function UserProjectMenu(): JSX.Element {

      {projectName}

      -

      +

      Your Time Reports

      -

      +

      New Time Report

      diff --git a/frontend/src/Components/UserStatistics.tsx b/frontend/src/Components/UserStatistics.tsx deleted file mode 100644 index c84f1a0..0000000 --- a/frontend/src/Components/UserStatistics.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { api } from "../API/API"; -import { Statistics } from "../Types/goTypes"; - -/** - * Renders the component for showing total time per role in a project. - * @returns JSX.Element - */ -export default function UserStatistics(): JSX.Element { - const [development, setDevelopment] = useState(0); - const [meeting, setMeeting] = useState(0); - const [admin, setAdmin] = useState(0); - const [own_work, setOwnWork] = useState(0); - const [study, setStudy] = useState(0); - const [testing, setTesting] = useState(0); - const total = development + meeting + admin + own_work + study + testing; - - const token = localStorage.getItem("accessToken") ?? ""; - const { projectName } = useParams(); - const { username } = useParams(); - - const fetchTimePerActivity = async (): Promise => { - const response = await api.getStatistics( - projectName ?? "", - token, - username ?? "", - ); - { - if (response.success) { - const statistics: Statistics = response.data ?? { - totalDevelopmentTime: 0, - totalMeetingTime: 0, - totalAdminTime: 0, - totalOwnWorkTime: 0, - totalStudyTime: 0, - totalTestingTime: 0, - }; - setDevelopment(statistics.totalDevelopmentTime); - setMeeting(statistics.totalMeetingTime); - setAdmin(statistics.totalAdminTime); - setOwnWork(statistics.totalOwnWorkTime); - setStudy(statistics.totalStudyTime); - setTesting(statistics.totalTestingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - } - }; - - useEffect(() => { - void fetchTimePerActivity(); - }); - - return ( - <> -

      - Total Time In: {projectName}{" "} -

      -
      -
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      Activity - Total Time (min) -
      Development - -
      Meeting - -
      Administration - -
      Own Work - -
      Studies - -
      Testing - -
      In Total: -

      {total}

      -
      -
      -
      - - ); -} diff --git a/frontend/src/Data/constants.ts b/frontend/src/Data/constants.ts deleted file mode 100644 index c803ad4..0000000 --- a/frontend/src/Data/constants.ts +++ /dev/null @@ -1,36 +0,0 @@ -//Different character limits certain strings - -/** - * Allowed character length for password - */ -export const passwordLength = 6; - -/** - * Lower limit for username length - */ -export const usernameLowLimit = 5; - -/** - * Upper limit for password length - */ -export const usernameUpLimit = 10; - -/** - * Lower limit for project name length - */ -export const projNameLowLimit = 10; - -/** - * Upper limit for project name length - */ -export const projNameHighLimit = 99; - -/** - * Upper limit for project description length - */ -export const projDescLowLimit = 0; - -/** - * Upper limit for project description length - */ -export const projDescHighLimit = 99; diff --git a/frontend/src/Data/regex.ts b/frontend/src/Data/regex.ts deleted file mode 100644 index ceb22cd..0000000 --- a/frontend/src/Data/regex.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Only alphanumerical characters - */ -export const alphanumeric = /^[a-zA-Z0-9]+$/; - -/** - * Only lowercase letters - */ -export const lowercase = /^[a-z]+$/; diff --git a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx index 296dc59..6c03c01 100644 --- a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx @@ -1,11 +1,11 @@ import { Link } from "react-router-dom"; +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; import { ProjectListAdmin } from "../../Components/ProjectListAdmin"; import { Project } from "../../Types/goTypes"; import GetProjects from "../../Components/GetProjects"; import { useState } from "react"; -import NavButton from "../../Components/NavButton"; function AdminManageProjects(): JSX.Element { const [projects, setProjects] = useState([]); @@ -13,7 +13,14 @@ function AdminManageProjects(): JSX.Element { setProjectsProp: setProjects, username: localStorage.getItem("username") ?? "", }); - const content = ; + const content = ( + <> +

      Manage Projects

      +
      + +
      + + ); const buttons = ( <> @@ -26,7 +33,7 @@ function AdminManageProjects(): JSX.Element { type="button" /> - + ); diff --git a/frontend/src/Pages/AdminPages/AdminManageUsers.tsx b/frontend/src/Pages/AdminPages/AdminManageUsers.tsx index 1c34662..353fddc 100644 --- a/frontend/src/Pages/AdminPages/AdminManageUsers.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageUsers.tsx @@ -12,7 +12,14 @@ function AdminManageUsers(): JSX.Element { const navigate = useNavigate(); - const content = ; + const content = ( + <> +

      Manage Users

      +
      + +
      + + ); const buttons = ( <> diff --git a/frontend/src/Pages/AdminPages/AdminMenuPage.tsx b/frontend/src/Pages/AdminPages/AdminMenuPage.tsx index 52a4198..ed2118d 100644 --- a/frontend/src/Pages/AdminPages/AdminMenuPage.tsx +++ b/frontend/src/Pages/AdminPages/AdminMenuPage.tsx @@ -5,14 +5,14 @@ function AdminMenuPage(): JSX.Element { const content = ( <>

      Administrator Menu

      -
      +
      -

      +

      Manage Users

      -

      +

      Manage Projects

      diff --git a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx index e28c338..fa592c9 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx @@ -1,12 +1,11 @@ import { useLocation } from "react-router-dom"; import AddUserToProject from "../../Components/AddUserToProject"; import BasicWindow from "../../Components/BasicWindow"; -import BackButton from "../../Components/BackButton"; function AdminProjectAddMember(): JSX.Element { const projectName = useLocation().search.slice(1); const content = ; - const buttons = ; + const buttons = <>; return ; } export default AdminProjectAddMember; diff --git a/frontend/src/Pages/UserPages/UserViewStatistics.tsx b/frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx similarity index 52% rename from frontend/src/Pages/UserPages/UserViewStatistics.tsx rename to frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx index afa4763..0110d65 100644 --- a/frontend/src/Pages/UserPages/UserViewStatistics.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx @@ -1,13 +1,8 @@ import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; -import UserStatistics from "../../Components/UserStatistics"; -function UserNewTimeReportPage(): JSX.Element { - const content = ( - <> - - - ); +function AdminProjectStatistics(): JSX.Element { + const content = <>; const buttons = ( <> @@ -17,4 +12,4 @@ function UserNewTimeReportPage(): JSX.Element { return ; } -export default UserNewTimeReportPage; +export default AdminProjectStatistics; diff --git a/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx b/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx index b586b64..cb558b0 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx @@ -1,12 +1,8 @@ import BasicWindow from "../../Components/BasicWindow"; import BackButton from "../../Components/BackButton"; import AllTimeReportsInProjectOtherUser from "../../Components/AllTimeReportsInProjectOtherUser"; -import Button from "../../Components/Button"; -import { useParams, Link } from "react-router-dom"; function PMOtherUsersTR(): JSX.Element { - const { projectName } = useParams(); - const { username } = useParams(); const content = ( <> @@ -15,15 +11,6 @@ function PMOtherUsersTR(): JSX.Element { const buttons = ( <> - -