diff --git a/.gitignore b/.gitignore index 313b735..3b1c6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,11 +9,15 @@ bin database.txt plantuml.jar db.sqlite3 +db.sqlite3-journal diagram.puml backend/*.png backend/*.jpg backend/*.svg +/go.work.sum +/package-lock.json + # Test binary, built with `go test -c` *.test diff --git a/Justfile b/Justfile index 432fbd1..90fabf6 100644 --- a/Justfile +++ b/Justfile @@ -15,7 +15,7 @@ remove-podman-containers: # Saves the release container to a tarball, pigz is just gzip but multithreaded save-release: build-container-release - podman save --format=oci-archive ttime-server | pigz -9 > ttime-server.tar.gz + podman save --format=oci-archive ttime-server | pigz -9 > ttime-server_`date -I`_`git rev-parse --short HEAD`.tar.gz # Loads the release container from a tarball load-release file: @@ -23,10 +23,13 @@ load-release file: # Tests every part of the project testall: + cd frontend && npm install cd frontend && npm test cd frontend && npm run lint + cd frontend && npm run build cd backend && make test cd backend && make lint + cd backend && make itest # Cleans up everything related to the project clean: remove-podman-containers diff --git a/Makefile b/Makefile index 97db62e..51fb206 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,13 @@ remove-podman-containers: # Tests every part of the project testall: + cd frontend && npm install cd frontend && npm test cd frontend && npm run lint + cd frontend && npm run build cd backend && make test cd backend && make lint + cd backend && make itest # Cleans up everything related to the project clean: remove-podman-containers diff --git a/backend/Makefile b/backend/Makefile index 65a2f3c..3443e94 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -8,17 +8,19 @@ GOGET = $(GOCMD) get # SQLite database filename DB_FILE = db.sqlite3 +PROC_NAME = ttime_server + # Directory containing migration SQL scripts MIGRATIONS_DIR = internal/database/migrations SAMPLE_DATA_DIR = internal/database/sample_data # Build target build: - $(GOBUILD) -o bin/server main.go + $(GOBUILD) -o bin/$(PROC_NAME) main.go # Run target run: build - ./bin/server + ./bin/$(PROC_NAME) watch: build watchexec -c -w . -r make run @@ -37,6 +39,16 @@ clean: test: db.sqlite3 $(GOTEST) ./... -count=1 +# Integration test target +.PHONY: itest +itest: + pgrep $(PROC_NAME) && echo "Server already running" && exit 1 || true + make build + ./bin/$(PROC_NAME) >/dev/null 2>&1 & + sleep 1 # Adjust if needed + python ../testing.py + pkill $(PROC_NAME) + # Get dependencies target deps: $(GOGET) -v ./... @@ -130,4 +142,12 @@ install-just: .PHONY: types types: - tygo generate \ No newline at end of file + tygo generate + +.PHONY: install-golds +install-golds: + go install go101.org/golds@latest + +.PHONY: golds +golds: + golds -port 6060 -nouses -plainsrc -wdpkgs-listing=promoted ./... diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index cb02a31..e8ddce8 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -5,8 +5,12 @@ import ( "testing" ) +// TestNewConfig tests the creation of a new configuration object func TestNewConfig(t *testing.T) { + // Arrange c := NewConfig() + + // Act & Assert if c.Port != 8080 { t.Errorf("Expected port to be 8080, got %d", c.Port) } @@ -24,9 +28,15 @@ func TestNewConfig(t *testing.T) { } } +// TestWriteConfig tests the function to write the configuration to a file func TestWriteConfig(t *testing.T) { + // Arrange c := NewConfig() + + //Act err := c.WriteConfigToFile("test.toml") + + // Assert if err != nil { t.Errorf("Expected no error, got %s", err) } @@ -35,14 +45,23 @@ func TestWriteConfig(t *testing.T) { _ = os.Remove("test.toml") } +// TestReadConfig tests the function to read the configuration from a file func TestReadConfig(t *testing.T) { + // Arrange c := NewConfig() + + // Act err := c.WriteConfigToFile("test.toml") + + // Assert if err != nil { t.Errorf("Expected no error, got %s", err) } + // Act c2, err := ReadConfigFromFile("test.toml") + + // Assert if err != nil { t.Errorf("Expected no error, got %s", err) } diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index bc6e1e8..3aae28d 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -3,6 +3,7 @@ package database import ( "embed" "errors" + "fmt" "path/filepath" "ttime/internal/types" @@ -19,12 +20,14 @@ type Database interface { PromoteToAdmin(username string) error 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 ChangeUserRole(username string, projectname string, role string) error + ChangeUserName(username string, newname string) error GetAllUsersProject(projectname string) ([]UserProjectMember, error) GetAllUsersApplication() ([]string, error) GetProjectsForUser(username string) ([]types.Project, error) @@ -32,8 +35,13 @@ type Database interface { GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) + GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error) SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) + IsProjectManager(username string, projectname string) (bool, 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 } // This struct is a wrapper type that holds the database connection @@ -55,19 +63,27 @@ var sampleData embed.FS // TODO: Possibly break these out into separate files bundled with the embed package? const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" -const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" +const projectInsert = "INSERT INTO projects (name, description, owner_user_id) VALUES (?, ?, (SELECT id FROM users WHERE username = ?))" const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), ProjectLookup AS (SELECT id FROM projects WHERE name = ?) INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time) VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);` -const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP -const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" - +const addUserToProject = `INSERT OR IGNORE INTO user_roles (user_id, project_id, p_role) + VALUES ((SELECT id FROM users WHERE username = ?), + (SELECT id FROM projects WHERE name = ?), ?)` +const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)" const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p JOIN user_roles ur ON p.id = ur.project_id JOIN users u ON ur.user_id = u.id WHERE u.username = ?` +const deleteProject = `DELETE FROM projects + WHERE id = ? AND owner_username = ?` + +const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles + JOIN users ON user_roles.user_id = users.id + JOIN projects ON user_roles.project_id = projects.id + WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'` // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -125,42 +141,23 @@ func (d *Db) AddWeeklyReport(projectName string, userName string, week int, deve } // AddUserToProject adds a user to a project with a specified role. -func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP - var userid int - userid, err := d.GetUserId(username) - if err != nil { - panic(err) - } - - var projectid int - projectid, err2 := d.GetProjectId(projectname) - if err2 != nil { - panic(err2) - } - - _, err3 := d.Exec(addUserToProject, userid, projectid, role) - return err3 +func (d *Db) AddUserToProject(username string, projectname string, role string) error { + _, err := d.Exec(addUserToProject, username, projectname, role) + return err } // ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { - // Get the user ID - var userid int - userid, err := d.GetUserId(username) - if err != nil { - panic(err) - } - - // Get the project ID - var projectid int - projectid, err2 := d.GetProjectId(projectname) - if err2 != nil { - panic(err2) - } - // Execute the SQL query to change the user's role - _, err3 := d.Exec(changeUserRole, role, userid, projectid) - return err3 + _, err := d.Exec(changeUserRole, role, username, projectname) + return err +} + +// ChangeUserName changes the username of a user. +func (d *Db) ChangeUserName(username string, newname string) error { + // Execute the SQL query to update the username + _, err := d.Exec("UPDATE users SET username = ? WHERE username = ?", newname, username) + return err } // GetUserRole retrieves the role of a user within a project. @@ -202,6 +199,7 @@ 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 := tx.Exec(projectInsert, name, description, username) if err != nil { if err := tx.Rollback(); err != nil { @@ -209,7 +207,9 @@ func (d *Db) AddProject(name string, description string, username string) error } return err } - _, err = tx.Exec(changeUserRole, "project_manager", username, name) + + // Add creator to project as project manager + _, err = tx.Exec(addUserToProject, username, name, "project_manager") if err != nil { if err := tx.Rollback(); err != nil { return err @@ -223,6 +223,21 @@ func (d *Db) AddProject(name string, description string, username string) error return err } +func (d *Db) DeleteProject(projectID string, username string) error { + 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 +} + func (d *Db) GetAllUsersProject(projectname string) ([]UserProjectMember, error) { // Define the SQL query to fetch users and their roles for a given project query := ` @@ -402,6 +417,63 @@ func (d *Db) Migrate() error { return nil } +// GetWeeklyReportsUser retrieves weekly reports for a specific user and project. +func (d *Db) GetWeeklyReportsUser(username string, projectName string) ([]types.WeeklyReportList, error) { + query := ` + SELECT + wr.week, + wr.development_time, + wr.meeting_time, + wr.admin_time, + wr.own_work_time, + wr.study_time, + wr.testing_time, + wr.signed_by + FROM + weekly_reports wr + INNER JOIN + users u ON wr.user_id = u.id + INNER JOIN + projects p ON wr.project_id = p.id + WHERE + u.username = ? AND p.name = ? + ` + + var reports []types.WeeklyReportList + if err := d.Select(&reports, query, username, projectName); err != nil { + return nil, err + } + + return reports, nil +} + +// IsProjectManager checks if a given username is a project manager for the specified project +func (d *Db) IsProjectManager(username string, projectname string) (bool, error) { + var manager bool + err := d.Get(&manager, isProjectManagerQuery, username, projectname) + return manager, err +} + +func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error { + query := ` + UPDATE weekly_reports + SET + development_time = ?, + meeting_time = ?, + admin_time = ?, + own_work_time = ?, + study_time = ?, + testing_time = ? + WHERE + user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?) + AND week = ? + ` + + _, err := d.Exec(query, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime, userName, projectName, week) + return err +} + // MigrateSampleData applies sample data to the database. func (d *Db) MigrateSampleData() error { // Insert sample data @@ -440,3 +512,46 @@ func (d *Db) MigrateSampleData() error { return nil } + +// GetProjectTimes retrieves a map with times per "Activity" for a given project +func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { + query := ` + SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time + FROM weekly_reports + JOIN projects ON weekly_reports.project_id = projects.id + WHERE projects.name = ? + ` + + rows, err := d.DB.Query(query, projectName) + if err != nil { + return nil, err + } + defer rows.Close() + + totalTime := make(map[string]int) + + for rows.Next() { + var developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime int + if err := rows.Scan(&developmentTime, &meetingTime, &adminTime, &ownWorkTime, &studyTime, &testingTime); err != nil { + return nil, err + } + + totalTime["development"] += developmentTime + totalTime["meeting"] += meetingTime + totalTime["admin"] += adminTime + totalTime["own_work"] += ownWorkTime + totalTime["study"] += studyTime + totalTime["testing"] += testingTime + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return totalTime, nil +} + +func (d *Db) RemoveProject(projectname string) error { + _, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index a7f3878..90ef221 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,12 +1,12 @@ package database import ( - "fmt" "testing" ) // Tests are not guaranteed to be sequential +// setupState initializes a database instance with necessary setup for testing func setupState() (Database, error) { db := DbConnect(":memory:") err := db.Migrate() @@ -16,11 +16,62 @@ func setupState() (Database, error) { return db, nil } +// This is a more advanced setup that includes more data in the database. +// This is useful for more complex testing scenarios. +func setupAdvancedState() (Database, error) { + db, err := setupState() + if err != nil { + return nil, err + } + + // Add a user + if err = db.AddUser("demouser", "password"); err != nil { + return nil, err + } + + // Add a project + if err = db.AddProject("projecttest", "description", "demouser"); err != nil { + return nil, err + } + + // Add a weekly report + if err = db.AddWeeklyReport("projecttest", "demouser", 1, 1, 1, 1, 1, 1, 1); err != nil { + return nil, err + } + + return db, nil +} + +// TestDbConnect tests the connection to the database func TestDbConnect(t *testing.T) { db := DbConnect(":memory:") _ = db } +func TestSetupAdvancedState(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupAdvancedState failed:", err) + } + + // Check if the user was added + if _, err = db.GetUserId("demouser"); err != nil { + t.Error("GetUserId failed:", err) + } + + // Check if the project was added + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if len(projects) != 1 { + t.Error("GetAllProjects failed: expected 1, got", len(projects)) + } + + // To be continued... +} + +// TestDbAddUser tests the AddUser function of the database func TestDbAddUser(t *testing.T) { db, err := setupState() if err != nil { @@ -32,6 +83,7 @@ func TestDbAddUser(t *testing.T) { } } +// TestDbGetUserId tests the GetUserID function of the database func TestDbGetUserId(t *testing.T) { db, err := setupState() if err != nil { @@ -52,18 +104,20 @@ func TestDbGetUserId(t *testing.T) { } } +// TestDbAddProject tests the AddProject function of the database func TestDbAddProject(t *testing.T) { - db, err := setupState() + db, err := setupAdvancedState() if err != nil { t.Error("setupState failed:", err) } - err = db.AddProject("test", "description", "test") + err = db.AddProject("test", "description", "demouser") if err != nil { t.Error("AddProject failed:", err) } } +// TestDbRemoveUser tests the RemoveUser function of the database func TestDbRemoveUser(t *testing.T) { db, err := setupState() if err != nil { @@ -76,6 +130,7 @@ func TestDbRemoveUser(t *testing.T) { } } +// TestPromoteToAdmin tests the PromoteToAdmin function of the database func TestPromoteToAdmin(t *testing.T) { db, err := setupState() if err != nil { @@ -93,6 +148,7 @@ func TestPromoteToAdmin(t *testing.T) { } } +// TestAddWeeklyReport tests the AddWeeklyReport function of the database func TestAddWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { @@ -115,6 +171,7 @@ func TestAddWeeklyReport(t *testing.T) { } } +// TestAddUserToProject tests the AddUseToProject function of the database func TestAddUserToProject(t *testing.T) { db, err := setupState() if err != nil { @@ -142,6 +199,7 @@ func TestAddUserToProject(t *testing.T) { } } +// TestChangeUserRole tests the ChangeUserRole function of the database func TestChangeUserRole(t *testing.T) { db, err := setupState() if err != nil { @@ -158,20 +216,15 @@ func TestChangeUserRole(t *testing.T) { t.Error("AddProject failed:", err) } - err = db.AddUserToProject("testuser", "testproject", "user") - if err != nil { - t.Error("AddUserToProject failed:", err) - } - role, err := db.GetUserRole("testuser", "testproject") if err != nil { t.Error("GetUserRole failed:", err) } - if role != "user" { - t.Error("GetUserRole failed: expected user, got", role) + if role != "project_manager" { + t.Error("GetUserRole failed: expected project_manager, got", role) } - err = db.ChangeUserRole("testuser", "testproject", "admin") + err = db.ChangeUserRole("testuser", "testproject", "member") if err != nil { t.Error("ChangeUserRole failed:", err) } @@ -180,12 +233,13 @@ func TestChangeUserRole(t *testing.T) { if err != nil { t.Error("GetUserRole failed:", err) } - if role != "admin" { - t.Error("GetUserRole failed: expected admin, got", role) + if role != "member" { + t.Error("GetUserRole failed: expected member, got", role) } } +// TestGetAllUsersProject tests the GetAllUsersProject function of the database func TestGetAllUsersProject(t *testing.T) { db, err := setupState() if err != nil { @@ -252,6 +306,7 @@ func TestGetAllUsersProject(t *testing.T) { } } +// TestGetAllUsersApplication tests the GetAllUsersApplicsation function of the database func TestGetAllUsersApplication(t *testing.T) { db, err := setupState() if err != nil { @@ -298,6 +353,7 @@ func TestGetAllUsersApplication(t *testing.T) { } } +// TestGetProjectsForUser tests the GetProjectsForUser function of the database func TestGetProjectsForUser(t *testing.T) { db, err := setupState() if err != nil { @@ -338,6 +394,7 @@ func TestGetProjectsForUser(t *testing.T) { } } +// TestAddProject tests AddProject function of the database func TestAddProject(t *testing.T) { db, err := setupState() if err != nil { @@ -373,6 +430,7 @@ func TestAddProject(t *testing.T) { } } +// TestGetWeeklyReport tests GetWeeklyReport function of the database func TestGetWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { @@ -412,6 +470,7 @@ func TestGetWeeklyReport(t *testing.T) { // Check other fields similarly } +// TestSignWeeklyReport tests SignWeeklyReport function of the database func TestSignWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { @@ -464,7 +523,6 @@ func TestSignWeeklyReport(t *testing.T) { if err != nil { t.Error("GetUserId failed:", err) } - fmt.Println("Project Manager's ID:", projectManagerID) // Sign the report with the project manager err = db.SignWeeklyReport(report.ReportId, projectManagerID) @@ -484,6 +542,7 @@ func TestSignWeeklyReport(t *testing.T) { } } +// TestSignWeeklyReportByAnotherProjectManager tests the scenario where a project manager attempts to sign a weekly report for a user who is not assigned to their project func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { db, err := setupState() if err != nil { @@ -502,7 +561,7 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { t.Error("AddUser failed:", err) } - // Add project + // Add project, projectManager is the owner err = db.AddProject("testproject", "description", "projectManager") if err != nil { t.Error("AddProject failed:", err) @@ -526,17 +585,29 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { t.Error("GetWeeklyReport failed:", err) } - anotherManagerID, err := db.GetUserId("projectManager") + managerID, err := db.GetUserId("projectManager") if err != nil { t.Error("GetUserId failed:", err) } - err = db.SignWeeklyReport(report.ReportId, anotherManagerID) - if err == nil { - t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") + err = db.SignWeeklyReport(report.ReportId, managerID) + if err != nil { + t.Error("SignWeeklyReport failed:", err) + } + + // Retrieve the report again to check if it's signed + signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Ensure the report is signed by the project manager + if *signedReport.SignedBy != managerID { + t.Errorf("Expected SignedBy to be %d, got %d", managerID, *signedReport.SignedBy) } } +// TestGetProject tests GetProject function of the database func TestGetProject(t *testing.T) { db, err := setupState() if err != nil { @@ -566,3 +637,290 @@ func TestGetProject(t *testing.T) { t.Errorf("Expected Name to be testproject, got %s", project.Name) } } + +func TestGetWeeklyReportsUser(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser", 2, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + reports, err := db.GetWeeklyReportsUser("testuser", "testproject") + if err != nil { + t.Error("GetWeeklyReportsUser failed:", err) + } + + // Check if the retrieved reports match the expected values + if len(reports) != 2 { + t.Errorf("Expected 1 report, got %d", len(reports)) + } +} + +func TestIsProjectManager(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add both regular users as members to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.AddUserToProject("projectManager", "testproject", "project_manager") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Check if the regular user is not a project manager + isManager, err := db.IsProjectManager("testuser", "testproject") + if err != nil { + t.Error("IsProjectManager failed:", err) + } + if isManager { + t.Error("Expected testuser not to be a project manager, but it is.") + } + + // Check if the project manager is indeed a project manager + isManager, err = db.IsProjectManager("projectManager", "testproject") + if err != nil { + t.Error("IsProjectManager failed:", err) + } + if !isManager { + t.Error("Expected projectManager to be a project manager, but it's not.") + } +} + +func TestGetProjectTimes(t *testing.T) { + // Initialize + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + return + } + + // Create a user + user := "TeaUser" + password := "Vanilla" + err = db.AddUser(user, password) + if err != nil { + t.Error("AddUser failed:", err) + return + } + + // Create a project + projectName := "ProjectVanilla" + projectDescription := "When tea tastes its best" + err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here + if err != nil { + t.Error("AddProject failed:", err) + return + } + + // Tests the func in db.go + totalTime, err := db.GetProjectTimes(projectName) + if err != nil { + t.Error("GetTotalTimePerActivity failed:", err) + return + } + + // Check if the totalTime map is not nil + if totalTime == nil { + t.Error("Expected non-nil totalTime map, got nil") + return + } + + // Define the expected valeus + expectedTotalTime := map[string]int{ + "development": 0, + "meeting": 0, + "admin": 0, + "own_work": 0, + "study": 0, + "testing": 0, + } + + // Compare the expectedTotalTime with the totalTime retrieved from the database + for activity, expectedTime := range expectedTotalTime { + if totalTime[activity] != expectedTime { + t.Errorf("Expected %s time to be %d, got %d", activity, expectedTime, totalTime[activity]) + } + } + + // Insert some data into the database for different activities + err = db.AddWeeklyReport(projectName, user, 1, 1, 3, 2, 1, 4, 5) + if err != nil { + t.Error("Failed to insert data into the database:", err) + return + } + + newTotalTime, err := db.GetProjectTimes(projectName) + if err != nil { + t.Error("GetTotalTimePerActivity failed:", err) + return + } + + newExpectedTotalTime := map[string]int{ + "development": 1, + "meeting": 3, + "admin": 2, + "own_work": 1, + "study": 4, + "testing": 5, + } + + for activity, newExpectedTime := range newExpectedTotalTime { + if newTotalTime[activity] != newExpectedTime { + t.Errorf("Expected %s time to be %d, got %d", activity, newExpectedTime, newTotalTime[activity]) + } + } +} +func TestEnsureManagerOfCreatedProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a project + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Set user to a project manager + // err = db.AddUserToProject("testuser", "testproject", "project_manager") + // if err != nil { + // t.Error("AddUserToProject failed:", err) + // } + + managerState, err := db.IsProjectManager("testuser", "testproject") + if err != nil { + t.Error("IsProjectManager failed:", err) + } + + if !managerState { + t.Error("Expected testuser to be a project manager, but it's not.") + } +} + +// TestUpdateWeeklyReport tests the UpdateWeeklyReport function of the database +func TestUpdateWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a project + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add a weekly report + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Update the weekly report + err = db.UpdateWeeklyReport("testproject", "testuser", 1, 2, 2, 2, 2, 2, 2) + if err != nil { + t.Error("UpdateWeeklyReport failed:", err) + } + + // Retrieve the updated report + updatedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Check if the report was updated correctly + if updatedReport.DevelopmentTime != 2 || + updatedReport.MeetingTime != 2 || + updatedReport.AdminTime != 2 || + updatedReport.OwnWorkTime != 2 || + updatedReport.StudyTime != 2 || + updatedReport.TestingTime != 2 { + t.Error("UpdateWeeklyReport failed: report not updated correctly") + } +} + +func TestRemoveProject(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) + } + + // Remove project + err = db.RemoveProject("projecttest") + if err != nil { + t.Error("RemoveProject failed:", err) + } + + // Check if the project was removed + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if len(projects) != 0 { + t.Error("RemoveProject failed: expected 0, got", len(projects)) + } + +} + \ No newline at end of file diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql index 8f76b80..b0cbe82 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS weekly_reports ( study_time INTEGER, testing_time INTEGER, signed_by INTEGER, + UNIQUE(user_id, project_id, week), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (signed_by) REFERENCES users(id) diff --git a/backend/internal/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index 4dac91b..092fbb0 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -33,3 +33,18 @@ 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, 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, 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/global_state.go b/backend/internal/handlers/global_state.go index 932451d..b832f92 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -20,23 +20,16 @@ type GlobalState interface { GetProject(c *fiber.Ctx) error AddUserToProjectHandler(c *fiber.Ctx) error PromoteToAdmin(c *fiber.Ctx) error - // GetProject(c *fiber.Ctx) error // To get a specific project - // UpdateProject(c *fiber.Ctx) error // To update a project - // DeleteProject(c *fiber.Ctx) error // To delete a project - // CreateTask(c *fiber.Ctx) error // To create a new task - // GetTasks(c *fiber.Ctx) error // To get all tasks - // GetTask(c *fiber.Ctx) error // To get a specific task - // UpdateTask(c *fiber.Ctx) error // To update a task - // DeleteTask(c *fiber.Ctx) error // To delete a task - // CreateCollection(c *fiber.Ctx) error // To create a new collection - // GetCollections(c *fiber.Ctx) error // To get all collections - // GetCollection(c *fiber.Ctx) error // To get a specific collection - // UpdateCollection(c *fiber.Ctx) error // To update a collection - // DeleteCollection(c *fiber.Ctx) error // To delete a collection - // SignCollection(c *fiber.Ctx) error // To sign a collection + GetWeeklyReportsUserHandler(c *fiber.Ctx) error + IsProjectManagerHandler(c *fiber.Ctx) error + DeleteProject(c *fiber.Ctx) error // To delete a project // WIP ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project + ChangeUserName(c *fiber.Ctx) error // WIP + GetAllUsersProject(c *fiber.Ctx) error // WIP + UpdateWeeklyReport(c *fiber.Ctx) error + RemoveProject(c *fiber.Ctx) error } // "Constructor" diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go index f3a7ea0..9c3ca67 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -30,6 +30,18 @@ func (gs *GState) CreateProject(c *fiber.Ctx) error { return c.Status(200).SendString("Project added") } +func (gs *GState) DeleteProject(c *fiber.Ctx) error { + + projectID := c.Params("projectID") + username := c.Params("username") + + if err := gs.Db.DeleteProject(projectID, username); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} + // GetUserProjects returns all projects that the user is a member of func (gs *GState) GetUserProjects(c *fiber.Ctx) error { // First we get the username from the token @@ -49,13 +61,32 @@ func (gs *GState) GetUserProjects(c *fiber.Ctx) error { // ProjectRoleChange is a handler that changes a user's role within a project func (gs *GState) ProjectRoleChange(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 - username := c.Params("username") - projectName := c.Params("projectName") - role := c.Params("role") + data := new(types.RoleChange) + if err := c.BodyParser(data); err != nil { + log.Info("error parsing username, project or role") + return c.Status(400).SendString(err.Error()) + } + + log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) + + // Dubble diping and checcking if current user is + if ismanager, err := gs.Db.IsProjectManager(username, data.Projectname); err != nil { + log.Warn("Error checking if projectmanager:", err) + return c.Status(500).SendString(err.Error()) + } else if !ismanager { + log.Warn("User is not projectmanager") + return c.Status(401).SendString("User is not projectmanager") + } // Change the user's role within the project in the database - if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil { + if err := gs.Db.ChangeUserRole(username, data.Projectname, data.Role); err != nil { return c.Status(500).SendString(err.Error()) } @@ -100,6 +131,31 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { return c.Status(400).SendString("No project name provided") } + // Get the user token + userToken := c.Locals("user").(*jwt.Token) + claims := userToken.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Check if the user is a project manager for the specified project + isManager, err := gs.Db.IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking project manager status:", err) + return c.Status(500).SendString(err.Error()) + } + + // If the user is not a project manager, check if the user is a site admin + if !isManager { + isAdmin, err := gs.Db.IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project manager nor a site admin:", username) + return c.Status(403).SendString("User is neither a project manager nor a site admin") + } + } + // Get all users associated with the project from the database users, err := gs.Db.GetAllUsersProject(projectName) if err != nil { @@ -154,3 +210,106 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { log.Info("User added to project successfully:", requestData.Username) return c.SendStatus(fiber.StatusOK) } + +// IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project +func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract necessary parameters from the request query string + projectName := c.Params("projectName") + + log.Info("Checking if user ", username, " is a project manager for project ", projectName) + + // Check if the user is a project manager for the specified project + isManager, err := gs.Db.IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking project manager status:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return the result as JSON + return c.JSON(fiber.Map{"isProjectManager": isManager}) +} + +func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Get project + projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } + + // Get all users in the project and roles + userProjects, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users in project:", err) + return c.Status(500).SendString(err.Error()) + } + + // If the user is member + isMember := false + for _, userProject := range userProjects { + if userProject.Username == username { + isMember = true + break + } + } + + // If the user is admin + if !isMember { + isAdmin, err := gs.Db.IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project member nor a site admin:", username) + return c.Status(403).SendString("User is neither a project member nor a site admin") + } + } + + // Get project times + projectTimes, err := gs.Db.GetProjectTimes(projectName) + if err != nil { + log.Info("Error getting project times:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return project times as JSON + log.Info("Returning project times for project:", projectName) + return c.JSON(projectTimes) +} + +func (gs *GState) RemoveProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Check if the user is a site admin + isAdmin, err := gs.Db.IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + log.Info("User is not a site admin:", username) + return c.Status(403).SendString("User is not a site admin") + } + + projectName := c.Params("projectName") + + if err := gs.Db.RemoveProject(projectName); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} \ No newline at end of file diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 85eb6e2..0e72ead 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -32,7 +32,7 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { } if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { - log.Info("Error adding weekly report") + log.Info("Error adding weekly report to db:", err) return c.Status(500).SendString(err.Error()) } @@ -114,3 +114,64 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { return c.Status(200).SendString("Weekly report signed successfully") } + +// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project +func (gs *GState) GetWeeklyReportsUserHandler(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 necessary (path) parameters from the request + projectName := c.Params("projectName") + + // TODO: Here we need to check whether the user is a member of the project + // If not, we should return an error. On the other hand, if the user not a member, + // the returned list of reports will (should) allways be empty. + + // Retrieve weekly reports for the user in the project from the database + reports, err := gs.Db.GetWeeklyReportsUser(username, projectName) + if err != nil { + log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning weekly reports for user:", username, "in project:", projectName) + + // Return the list of reports as JSON + return c.JSON(reports) +} + +func (gs *GState) UpdateWeeklyReport(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) + + // Parse the request body into an UpdateWeeklyReport struct + var updateReport types.UpdateWeeklyReport + if err := c.BodyParser(&updateReport); err != nil { + log.Info("Error parsing weekly report") + return c.Status(400).SendString(err.Error()) + } + + // Make sure all the fields of the report are valid + if updateReport.Week < 1 || updateReport.Week > 52 { + log.Info("Invalid week number") + return c.Status(400).SendString("Invalid week number") + } + + if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 { + log.Info("Invalid time report") + return c.Status(400).SendString("Invalid time report") + } + + // Update the weekly report in the database + if err := gs.Db.UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { + log.Info("Error updating weekly report in db:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Weekly report updated") + return c.Status(200).SendString("Weekly report updated") +} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 96fddb7..39788ae 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -101,10 +101,15 @@ func (gs *GState) Login(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusUnauthorized) } + isAdmin, err := gs.Db.IsSiteAdmin(u.Username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } // Create the Claims claims := jwt.MapClaims{ "name": u.Username, - "admin": false, + "admin": isAdmin, "exp": time.Now().Add(time.Hour * 72).Unix(), } @@ -182,17 +187,31 @@ func (gs *GState) ListAllUsers(c *fiber.Ctx) error { return c.JSON(users) } -// @Summary PromoteToAdmin -// @Description promote chosen user to admin -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully prometed user" -// @Failure 400 {string} string "bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] +func (gs *GState) GetAllUsersProject(c *fiber.Ctx) error { + // Get all users from a project + projectName := c.Params("projectName") + users, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users from project:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning all users") + // Return the list of users as JSON + return c.JSON(users) +} + +// @Summary PromoteToAdmin +// @Description promote chosen user to admin +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {json} json "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { // Extract the username from the request body var newUser types.NewUser @@ -214,3 +233,37 @@ func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { // Return a success message return c.SendStatus(fiber.StatusOK) } + +// ChangeUserName changes a user's username in the database +func (gs *GState) ChangeUserName(c *fiber.Ctx) error { + // Check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + log.Info(adminUsername) + + // Extract the necessary parameters from the request + data := new(types.StrNameChange) + if err := c.BodyParser(data); err != nil { + log.Info("Error parsing username") + return c.Status(400).SendString(err.Error()) + } + + // Check if the current user is an admin + isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + if err != nil { + log.Warn("Error checking if admin:", err) + return c.Status(500).SendString(err.Error()) + } else if !isAdmin { + log.Warn("Tried changing name when not admin") + return c.Status(401).SendString("You cannot change name unless you are an admin") + } + + // Change the user's name in the database + if err := gs.Db.ChangeUserName(data.PrevName, data.NewName); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 299395a..234781b 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -20,6 +20,27 @@ type NewWeeklyReport struct { TestingTime int `json:"testingTime"` } +type WeeklyReportList struct { + // The name of the project, as it appears in the database + ProjectName string `json:"projectName" db:"project_name"` + // The week number + Week int `json:"week" db:"week"` + // Total time spent on development + DevelopmentTime int `json:"developmentTime" db:"development_time"` + // Total time spent in meetings + MeetingTime int `json:"meetingTime" db:"meeting_time"` + // Total time spent on administrative tasks + AdminTime int `json:"adminTime" db:"admin_time"` + // Total time spent on personal projects + OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"` + // Total time spent on studying + StudyTime int `json:"studyTime" db:"study_time"` + // Total time spent on testing + TestingTime int `json:"testingTime" db:"testing_time"` + // The project manager who signed it + SignedBy *int `json:"signedBy" db:"signed_by"` +} + type WeeklyReport struct { // The ID of the report ReportId int `json:"reportId" db:"report_id"` @@ -44,3 +65,24 @@ type WeeklyReport struct { // The project manager who signed it SignedBy *int `json:"signedBy" db:"signed_by"` } + +type UpdateWeeklyReport struct { + // The name of the project, as it appears in the database + ProjectName string `json:"projectName"` + // The name of the user + UserName string `json:"userName"` + // The week number + Week int `json:"week"` + // Total time spent on development + DevelopmentTime int `json:"developmentTime"` + // Total time spent in meetings + MeetingTime int `json:"meetingTime"` + // Total time spent on administrative tasks + AdminTime int `json:"adminTime"` + // Total time spent on personal projects + OwnWorkTime int `json:"ownWorkTime"` + // Total time spent on studying + StudyTime int `json:"studyTime"` + // Total time spent on testing + TestingTime int `json:"testingTime"` +} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go index 7e1747f..2e26eb9 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -13,3 +13,17 @@ type NewProject struct { Name string `json:"name"` Description string `json:"description"` } + +// Used to change the role of a user in a project. +// If name is identical to the name contained in the token, the role can be changed. +// If the name is different, only a project manager can change the role. +type RoleChange struct { + UserName string `json:"username"` + Role string `json:"role" tstype:"'project_manager' | 'user'"` + Projectname string `json:"projectname"` +} + +type NameChange struct { + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` +} diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index d3f2170..88b4f06 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -32,3 +32,8 @@ type PublicUser struct { type Token struct { Token string `json:"token"` } + +type StrNameChange struct { + PrevName string `json:"prevName" db:"prevName"` + NewName string `json:"newName" db:"newName"` +} diff --git a/backend/main.go b/backend/main.go index e578c52..0ec638b 100644 --- a/backend/main.go +++ b/backend/main.go @@ -33,6 +33,12 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ +/** +Main function for starting the server and initializing configurations. +Reads configuration from file, pretty prints it, connects to the database, +migrates it, and sets up routes for the server. +*/ + func main() { conf, err := config.ReadConfigFromFile("config.toml") if err != nil { @@ -87,13 +93,23 @@ func main() { server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches - server.Post("/api/project", gs.CreateProject) + server.Delete("api/project/:projectID", gs.DeleteProject) // WIP + server.Post("/api/project", gs.CreateProject) // WIP server.Get("/api/project/:projectId", gs.GetProject) + server.Get("/api/project/getAllUsers", gs.GetAllUsersProject) server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) server.Post("/api/signReport", gs.SignReport) server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) + server.Put("/api/changeUserName", gs.ChangeUserName) server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) server.Get("/api/users/all", gs.ListAllUsers) + server.Get("/api/getWeeklyReportsUser/:projectName", gs.GetWeeklyReportsUserHandler) + server.Get("/api/checkIfProjectManager/:projectName", gs.IsProjectManagerHandler) + server.Post("/api/ProjectRoleChange", gs.ProjectRoleChange) + server.Get("/api/getUsersProject/:projectName", gs.ListAllUsersProject) + server.Put("/api/updateWeeklyReport", gs.UpdateWeeklyReport) + server.Delete("/api/removeProject/:projectName", gs.RemoveProject) + // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) if err != nil { diff --git a/container/Containerfile b/container/Containerfile index ecd2f84..f9cb39d 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -13,7 +13,6 @@ FROM docker.io/golang:alpine as go RUN apk add gcompat RUN apk add gcc RUN apk add musl-dev -RUN apk add make RUN apk add sqlite WORKDIR /build ADD backend/go.mod backend/go.sum ./ @@ -24,9 +23,7 @@ RUN go mod download # Add the source code ADD backend . -RUN make migrate - -# RUN go build -o server +RUN go build -o server RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go # Strip the binary for a smaller image @@ -37,6 +34,7 @@ FROM docker.io/alpine:latest as runner RUN adduser -D nonroot RUN addgroup nonroot nonroot WORKDIR /app +RUN chown nonroot:nonroot /app # Copy the frontend SPA build into public COPY --from=client /build/dist static @@ -44,9 +42,6 @@ COPY --from=client /build/dist static # Copy the server binary COPY --from=go /build/server server -# Copy the database -COPY --from=go /build/db.sqlite3 db.sqlite3 - # Expose port 8080 EXPOSE 8080 diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 37db463..e7aab0c 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -4,21 +4,39 @@ import { User, Project, NewProject, + UserProjectMember, + WeeklyReport, } from "../Types/goTypes"; -// This type of pattern should be hard to misuse +/** + * Response object returned by API methods. + */ export interface APIResponse { + /** Indicates whether the API call was successful */ success: boolean; + /** Optional message providing additional information or error description */ message?: string; + /** Optional data returned by the API method */ data?: T; } -// Note that all protected routes also require a token -// Defines all the methods that an instance of the API must implement +/** + * Interface defining methods that an instance of the API must implement. + */ interface API { - /** Register a new user */ + /** + * Register a new user + * @param {NewUser} user The user object to be registered + * @returns {Promise>} A promise containing the API response with the user data. + */ registerUser(user: NewUser): Promise>; - /** Remove a user */ + + /** + * Removes a user. + * @param {string} username The username of the user to be removed. + * @param {string} token The authentication token. + * @returns {Promise>} A promise containing the API response with the removed user data. + */ removeUser(username: string, token: string): Promise>; /** @@ -38,32 +56,90 @@ interface API { * @returns {Promise>} A promise resolving to an API response with a token. */ login(NewUser: NewUser): Promise>; - /** Renew the token */ + + /** + * Renew the token + * @param {string} token The current authentication token. + * @returns {Promise>} A promise resolving to an API response with a renewed token. + */ renewToken(token: string): Promise>; - /** Create a project */ + + /** Promote user to admin */ + + /** Creates a new project. + * @param {NewProject} project The project object containing name and description. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response with the created project. + */ createProject( project: NewProject, token: string, ): Promise>; - /** Submit a weekly report */ + + /** Submits a weekly report + * @param {NewWeeklyReport} weeklyReport The weekly report object. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response with the submitted report. + */ submitWeeklyReport( - project: NewWeeklyReport, + weeklyReport: NewWeeklyReport, token: string, - ): Promise>; - /**Gets a weekly report*/ + ): Promise>; + + /** Gets a weekly report for a specific user, project and week + * @param {string} projectName The name of the project. + * @param {string} week The week number. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response with the retrieved report. + */ getWeeklyReport( - username: string, projectName: string, week: string, token: string, - ): Promise>; - /** Gets all the projects of a user*/ + ): Promise>; + + /** + * Returns all the weekly reports for a user in a particular project + * The username is derived from the token + * @param {string} projectName The name of the project + * @param {string} token The token of the user + * @returns {APIResponse} A list of weekly reports + */ + getWeeklyReportsForUser( + projectName: string, + token: string, + ): Promise>; + + /** Gets all the projects of a user + * @param {string} token - The authentication token. + * @returns {Promise>} A promise containing the API response with the user's projects. + */ getUserProjects(token: string): Promise>; - /** Gets a project from id*/ + + /** Gets a project by its id. + * @param {number} id The id of the project to retrieve. + * @returns {Promise>} A promise resolving to an API response containing the project data. + */ getProject(id: number): Promise>; + + /** Gets a list of all users. + * @param {string} token The authentication token of the requesting user. + * @returns {Promise>} A promise resolving to an API response containing the list of users. + */ + getAllUsers(token: string): Promise>; + /** Gets all users in a project from name*/ + getAllUsersProject( + projectName: string, + token: string, + ): Promise>; + + removeProject( + projectName: string, + token: string, + ): Promise>; } -// Export an instance of the API +/** An instance of the API */ export const api: API = { async registerUser(user: NewUser): Promise> { try { @@ -97,7 +173,7 @@ export const api: API = { token: string, ): Promise> { try { - const response = await fetch("/api/userdelete", { + const response = await fetch(`/api/userdelete/${username}`, { method: "POST", headers: { "Content-Type": "application/json", @@ -223,7 +299,7 @@ export const api: API = { async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/submitWeeklyReport", { method: "POST", @@ -241,8 +317,8 @@ export const api: API = { }; } - const data = (await response.json()) as NewWeeklyReport; - return { success: true, data }; + const data = await response.text(); + return { success: true, message: data }; } catch (e) { return { success: false, @@ -252,29 +328,62 @@ export const api: API = { }, async getWeeklyReport( - username: string, projectName: string, week: string, token: string, - ): Promise> { + ): Promise> { try { - const response = await fetch("/api/getWeeklyReport", { + const response = await fetch( + `/api/getWeeklyReport?projectName=${projectName}&week=${week}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + + if (!response.ok) { + return { success: false, message: "Failed to get weekly report" }; + } else { + const data = (await response.json()) as WeeklyReport; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to get weekly report" }; + } + }, + + async getWeeklyReportsForUser( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/getWeeklyReportsUser/${projectName}`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, - body: JSON.stringify({ username, projectName, week }), }); if (!response.ok) { - return { success: false, message: "Failed to get weekly report" }; + return { + success: false, + message: + "Failed to get weekly reports for project: Response code " + + response.status, + }; } else { - const data = (await response.json()) as NewWeeklyReport; + const data = (await response.json()) as WeeklyReport[]; return { success: true, data }; } } catch (e) { - return { success: false, message: "Failed to get weekly report" }; + return { + success: false, + message: "Failed to get weekly reports for project, unknown error", + }; } }, @@ -299,7 +408,6 @@ export const api: API = { } }, - // Gets a projet by id, currently untested since we have no javascript-based tests async getProject(id: number): Promise> { try { const response = await fetch(`/api/project/${id}`, { @@ -324,4 +432,91 @@ export const api: API = { }; } }, + + async getAllUsers(token: string): Promise> { + try { + const response = await fetch("/api/users/all", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to get users", + }); + } else { + const data = (await response.json()) as string[]; + return Promise.resolve({ success: true, data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "API is not ok", + }); + } + }, + //Gets all users in a project + async getAllUsersProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/getUsersProject/${projectName}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to get users", + }); + } else { + const data = (await response.json()) as UserProjectMember[]; + return Promise.resolve({ success: true, data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "API is not ok", + }); + } + }, + + async removeProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/projectdelete/${projectName}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } else { + const data = await response.text(); + return Promise.resolve({ success: true, message: data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } + }, }; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index 45814e3..f5f4a08 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -7,7 +7,7 @@ import Button from "./Button"; /** * Tries to add a project to the system - * @param props - Project name and description + * @param {Object} props - Project name and description * @returns {boolean} True if created, false if not */ function CreateProject(props: { name: string; description: string }): boolean { @@ -34,8 +34,8 @@ function CreateProject(props: { name: string; description: string }): boolean { } /** - * Tries to add a project to the system - * @returns {JSX.Element} UI for project adding + * Provides UI for adding a project to the system. + * @returns {JSX.Element} - Returns the component UI for adding a project */ function AddProject(): JSX.Element { const [name, setName] = useState(""); diff --git a/frontend/src/Components/AdminUserList.tsx b/frontend/src/Components/AdminUserList.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/Components/AllTimeReportsInProject.tsx b/frontend/src/Components/AllTimeReportsInProject.tsx new file mode 100644 index 0000000..4fa9ad8 --- /dev/null +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -0,0 +1,71 @@ +//Info: This component is used to display all the time reports for a project. It will display the week number, +//total time spent, and if the report has been signed or not. The user can click on a report to edit it. +import { useEffect, useState } from "react"; +import { WeeklyReport } from "../Types/goTypes"; +import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; + +/** + * Renders a component that displays all the time reports for a specific project. + * @returns {JSX.Element} representing the component. + */ +function AllTimeReportsInProject(): JSX.Element { + const { projectName } = useParams(); + const [weeklyReports, setWeeklyReports] = useState([]); + + // Call getProjects when the component mounts + useEffect(() => { + const getWeeklyReports = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getWeeklyReportsForUser( + projectName ?? "", + token, + ); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } else { + console.error(response.message); + } + }; + + void getWeeklyReports(); + }, [projectName]); + + return ( + <> +
+ {weeklyReports.map((newWeeklyReport, index) => ( + +
+

+ {"Week: "} + {newWeeklyReport.week} +

+

+ {"Total Time: "} + {newWeeklyReport.developmentTime + + newWeeklyReport.meetingTime + + newWeeklyReport.adminTime + + newWeeklyReport.ownWorkTime + + newWeeklyReport.studyTime + + newWeeklyReport.testingTime}{" "} + min +

+

+ {"Signed: "} + {newWeeklyReport.signedBy ? "YES" : "NO"} +

+
+ + ))} +
+ + ); +} + +export default AllTimeReportsInProject; diff --git a/frontend/src/Components/AuthorizedRoute.tsx b/frontend/src/Components/AuthorizedRoute.tsx new file mode 100644 index 0000000..d9c8c59 --- /dev/null +++ b/frontend/src/Components/AuthorizedRoute.tsx @@ -0,0 +1,18 @@ +import { Navigate } from "react-router-dom"; +import React from "react"; + +interface AuthorizedRouteProps { + children: React.ReactNode; + isAuthorized: boolean; +} + +export function AuthorizedRoute({ + children, + isAuthorized, +}: AuthorizedRouteProps): JSX.Element { + if (!isAuthorized) { + return ; + } + + return children as React.ReactElement; +} diff --git a/frontend/src/Components/BackButton.tsx b/frontend/src/Components/BackButton.tsx index 7a1ac81..4f58140 100644 --- a/frontend/src/Components/BackButton.tsx +++ b/frontend/src/Components/BackButton.tsx @@ -1,5 +1,11 @@ +//info: Back button component to navigate back to the previous page import { useNavigate } from "react-router-dom"; +/** + * Renders a back button component. + * + * @returns The JSX element representing the back button. + */ function BackButton(): JSX.Element { const navigate = useNavigate(); const goBack = (): void => { diff --git a/frontend/src/Components/BackgroundAnimation.tsx b/frontend/src/Components/BackgroundAnimation.tsx index 5f402c0..87fca9e 100644 --- a/frontend/src/Components/BackgroundAnimation.tsx +++ b/frontend/src/Components/BackgroundAnimation.tsx @@ -1,5 +1,10 @@ +//info: Background animation component to animate the background of loginpage import { useEffect } from "react"; +/** + * Renders a background animation component. + * This component pre-loads images and starts a background transition animation. + */ const BackgroundAnimation = (): JSX.Element => { useEffect(() => { const images = [ diff --git a/frontend/src/Components/BasicWindow.tsx b/frontend/src/Components/BasicWindow.tsx index d5fd3b6..d53d367 100644 --- a/frontend/src/Components/BasicWindow.tsx +++ b/frontend/src/Components/BasicWindow.tsx @@ -1,6 +1,16 @@ +//info: Basic window component to display content and buttons of a page, inclduing header and footer +//content to insert is placed in the content prop, and buttons in the buttons prop import Header from "./Header"; import Footer from "./Footer"; +/** + * Renders a basic window component with a header, content, and footer. + * + * @param {Object} props - The component props. + * @param {React.ReactNode} props.content - The content to be rendered in the window. + * @param {React.ReactNode} props.buttons - The buttons to be rendered in the footer. + * @returns {JSX.Element} The rendered basic window component. + */ function BasicWindow({ content, buttons, diff --git a/frontend/src/Components/Button.tsx b/frontend/src/Components/Button.tsx index 38a1853..13ae807 100644 --- a/frontend/src/Components/Button.tsx +++ b/frontend/src/Components/Button.tsx @@ -1,3 +1,12 @@ +/** + * Button component to display a button with text and onClick function. + * + * @param {Object} props - The component props. + * @param {string} props.text - The text to display on the button. + * @param {Function} props.onClick - The function to run when the button is clicked. + * @param {"submit" | "button" | "reset"} props.type - The type of button. + * @returns {JSX.Element} The rendered Button component. + */ function Button({ text, onClick, diff --git a/frontend/src/Components/ChangeRoles.tsx b/frontend/src/Components/ChangeRoles.tsx new file mode 100644 index 0000000..e11d623 --- /dev/null +++ b/frontend/src/Components/ChangeRoles.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import Button from "./Button"; + +export default function ChangeRoles(): JSX.Element { + const [selectedRole, setSelectedRole] = useState(""); + const { username } = useParams(); + + const handleRoleChange = ( + event: React.ChangeEvent, + ): void => { + setSelectedRole(event.target.value); + }; + + // const handleSubmit = async (event: React.FormEvent) => { + // event.preventDefault(); + + // const response = await api.changeRole(username, selectedRole, token); + // if (response.success) { + // console.log("Role changed successfully"); + // } else { + // console.error("Failed to change role:", response.message); + // } + // }; + + return ( + <> +

+ Change roll for: {username} +

+
+
+
+ +
+
+ +
+
+ +
+
+
; -} diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx new file mode 100644 index 0000000..f4fd782 --- /dev/null +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -0,0 +1,45 @@ +import { useState, useEffect } from "react"; +import { Project } from "../Types/goTypes"; +import { Link } from "react-router-dom"; +import { api } from "../API/API"; + +/** + * Renders a component that displays the projects a user is a part of and links to the projects start-page. + * @returns The JSX element representing the component. + */ +function DisplayUserProject(): JSX.Element { + const [projects, setProjects] = useState([]); + + const getProjects = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getUserProjects(token); + console.log(response); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error(response.message); + } + }; + + // Call getProjects when the component mounts + useEffect(() => { + void getProjects(); + }, []); + + return ( + <> +

Your Projects

+
+ {projects.map((project, index) => ( + +

+ {project.name} +

+ + ))} +
+ + ); +} + +export default DisplayUserProject; diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index b0e8771..be96329 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -1,11 +1,14 @@ import { useState, useEffect } from "react"; -import { NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; +/** + * Renders the component for editing a weekly report. + * @returns JSX.Element + */ export default function GetWeeklyReport(): JSX.Element { - const [projectName, setProjectName] = useState(""); const [week, setWeek] = useState(0); const [developmentTime, setDevelopmentTime] = useState(0); const [meetingTime, setMeetingTime] = useState(0); @@ -15,47 +18,48 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + const { fetchedWeek } = useParams(); + + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; useEffect(() => { - const fetchWeeklyReport = async (): Promise => { - const response = await api.getWeeklyReport( - username, - projectName, - week.toString(), - token, - ); - - if (response.success) { - const report: NewWeeklyReport = response.data ?? { - projectName: "", - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setProjectName(report.projectName); - setWeek(report.week); - setDevelopmentTime(report.developmentTime); - setMeetingTime(report.meetingTime); - setAdminTime(report.adminTime); - setOwnWorkTime(report.ownWorkTime); - setStudyTime(report.studyTime); - setTestingTime(report.testingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - }; - void fetchWeeklyReport(); - }, [projectName, token, username, week]); + }); const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { - projectName, + projectName: projectName ?? "", week, developmentTime, meetingTime, @@ -82,7 +86,7 @@ export default function GetWeeklyReport(): JSX.Element { } e.preventDefault(); void handleNewWeeklyReport(); - navigate("/project"); + navigate(-1); }} >
@@ -233,7 +237,7 @@ export default function GetWeeklyReport(): JSX.Element {
+ + + ); +} + +export default ProjectInfoModal; diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx new file mode 100644 index 0000000..4ebdaf8 --- /dev/null +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { NewProject } from "../Types/goTypes"; +import ProjectInfoModal from "./ProjectInfoModal"; +import UserInfoModal from "./UserInfoModal"; +import DeleteUser from "./DeleteUser"; + +/** + * A list of projects for admin manage projects page, that sets an onClick + * function for eact project
  • element, which displays a modul with + * user info. + * @param props - An array of projects to display + * @returns {JSX.Element} The project list + * @example + * const projects: NewProject[] = [{ name: "Project", description: "New" }]; + * return + */ + +export function ProjectListAdmin(props: { + projects: NewProject[]; +}): JSX.Element { + const [projectModalVisible, setProjectModalVisible] = useState(false); + const [projectname, setProjectname] = useState(""); + const [userModalVisible, setUserModalVisible] = useState(false); + const [username, setUsername] = useState(""); + + const handleClickUser = (username: string): void => { + setUsername(username); + setUserModalVisible(true); + }; + + const handleClickProject = (username: string): void => { + setProjectname(username); + setProjectModalVisible(true); + }; + + const handleCloseProject = (): void => { + setProjectname(""); + setProjectModalVisible(false); + }; + + const handleCloseUser = (): void => { + setProjectname(""); + setUserModalVisible(false); + }; + + return ( + <> + + DeleteUser} + isVisible={userModalVisible} + username={username} + /> +
    +
      + {props.projects.map((project) => ( +
    • { + handleClickProject(project.name); + }} + > + {project.name} +
    • + ))} +
    +
    + + ); +} diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx new file mode 100644 index 0000000..73e29e5 --- /dev/null +++ b/frontend/src/Components/ProjectMembers.tsx @@ -0,0 +1,99 @@ +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router-dom"; + +function ProjectMembers(): JSX.Element { + const { projectName } = useParams(); + const [projectMembers, setProjectMembers] = useState([]); + + // const getProjectMembers = async (): Promise => { + // const token = localStorage.getItem("accessToken") ?? ""; + // const response = await api.getProjectMembers(projectName ?? "", token); + // console.log(response); + // if (response.success) { + // setProjectMembers(response.data ?? []); + // } else { + // console.error(response.message); + // } + // }; + + interface ProjectMember { + username: string; + role: string; + } + + const mockProjectMembers = [ + { + username: "username1", + role: "Project Manager", + }, + { + username: "username2", + role: "System Manager", + }, + { + username: "username3", + role: "Developer", + }, + { + username: "username4", + role: "Tester", + }, + { + username: "username5", + role: "Tester", + }, + { + username: "username6", + role: "Tester", + }, + ]; + + const getProjectMembers = async (): Promise => { + // Use the mock data + setProjectMembers(mockProjectMembers); + + await Promise.resolve(); + }; + + useEffect(() => { + void getProjectMembers(); + }); + + return ( + <> +
    + {projectMembers.map((projectMember, index) => ( +

    +
    +
    +

    {projectMember.username}

    + Role: +

    {projectMember.role}

    +
    +
    +
    + +

    + View Reports +

    + + +

    + Change Role +

    + +
    +
    +
    +

    + ))} +
    + + ); +} + +export default ProjectMembers; diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 7b003cb..df07c6e 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -6,6 +6,10 @@ import Button from "./Button"; import InputField from "./InputField"; import { useNavigate } from "react-router-dom"; +/** + * 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(); diff --git a/frontend/src/Components/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index a22ef01..2334388 100644 --- a/frontend/src/Components/UserInfoModal.tsx +++ b/frontend/src/Components/UserInfoModal.tsx @@ -5,23 +5,38 @@ import UserProjectListAdmin from "./UserProjectListAdmin"; function UserInfoModal(props: { isVisible: boolean; + manageMember: boolean; username: string; onClose: () => void; + onDelete: (username: string) => void; }): JSX.Element { if (!props.isVisible) return <>; - + const ManageUserOrMember = (check: boolean): JSX.Element => { + if (check) { + return ( + +

    + (Change Role) +

    + + ); + } + return ( + +

    + (Change Username) +

    + + ); + }; return (
    -
    +

    {props.username}

    - -

    - (Change Username) -

    - + {ManageUserOrMember(props.manageMember)}

    Member of these projects: diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index 3d2bcae..c08b05c 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,13 +1,6 @@ import { useState } from "react"; -import { PublicUser } from "../Types/goTypes"; import UserInfoModal from "./UserInfoModal"; - -/** - * The props for the UserProps component - */ -interface UserProps { - users: PublicUser[]; -} +import DeleteUser from "./DeleteUser"; /** * A list of users for admin manage users page, that sets an onClick @@ -20,7 +13,7 @@ interface UserProps { * return ; */ -export function UserListAdmin(props: UserProps): JSX.Element { +export function UserListAdmin(props: { users: string[] }): JSX.Element { const [modalVisible, setModalVisible] = useState(false); const [username, setUsername] = useState(""); @@ -37,7 +30,9 @@ export function UserListAdmin(props: UserProps): JSX.Element { return ( <> DeleteUser} isVisible={modalVisible} username={username} /> @@ -46,12 +41,12 @@ export function UserListAdmin(props: UserProps): JSX.Element { {props.users.map((user) => (
  • { - handleClick(user.username); + handleClick(user); }} > - {user.username} + {user}
  • ))} diff --git a/frontend/src/Components/UserProjectMenu.tsx b/frontend/src/Components/UserProjectMenu.tsx new file mode 100644 index 0000000..e307e90 --- /dev/null +++ b/frontend/src/Components/UserProjectMenu.tsx @@ -0,0 +1,32 @@ +//info: User project menu component to display the user project menu where the user can navigate to +//existing time reports in a project and create a new time report +import { useParams, Link } from "react-router-dom"; +import { JSX } from "react/jsx-runtime"; + +/** + * Renders the user project menu component. + * + * @returns JSX.Element representing the user project menu. + */ +function UserProjectMenu(): JSX.Element { + const { projectName } = useParams(); + + return ( + <> +

    {projectName}

    +
    + +

    + Your Time Reports +

    + + +

    + New Time Report +

    + +
    + + ); +} +export default UserProjectMenu; diff --git a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx index 7eb2e2e..b130fae 100644 --- a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx +++ b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx @@ -1,9 +1,14 @@ import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; +import ChangeUsername from "../../Components/ChangeUsername"; function AdminChangeUsername(): JSX.Element { - const content = <>; + const content = ( + <> + + + ); const buttons = ( <> diff --git a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx index 177f55b..7ea45df 100644 --- a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx @@ -2,9 +2,22 @@ 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"; function AdminManageProjects(): JSX.Element { - const content = <>; + const [projects, setProjects] = useState([]); + GetProjects({ setProjectsProp: setProjects }); + const content = ( + <> +

    Manage Projects

    +
    + +
    + + ); const buttons = ( <> diff --git a/frontend/src/Pages/AdminPages/AdminManageUsers.tsx b/frontend/src/Pages/AdminPages/AdminManageUsers.tsx index 0939d77..353fddc 100644 --- a/frontend/src/Pages/AdminPages/AdminManageUsers.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageUsers.tsx @@ -2,15 +2,13 @@ import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; import BackButton from "../../Components/BackButton"; import { UserListAdmin } from "../../Components/UserListAdmin"; -import { PublicUser } from "../../Types/goTypes"; import { useNavigate } from "react-router-dom"; +import GetAllUsers from "../../Components/GetAllUsers"; +import { useState } from "react"; function AdminManageUsers(): JSX.Element { - //TODO: Change so that it reads users from database - const users: PublicUser[] = []; - for (let i = 1; i <= 20; i++) { - users.push({ userId: "id" + i, username: "Example User " + i }); - } + const [users, setUsers] = useState([]); + GetAllUsers({ setUsersProp: setUsers }); const navigate = useNavigate(); diff --git a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx index 96167cb..712df86 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx @@ -1,3 +1,4 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; @@ -13,13 +14,7 @@ function AdminProjectAddMember(): JSX.Element { }} type="button" /> -

    + ); +} diff --git a/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx b/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx index 2cdeb15..cd69b3b 100644 --- a/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx +++ b/frontend/src/Pages/UserPages/UserNewTimeReportPage.tsx @@ -1,7 +1,6 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; -import Button from "../../Components/Button"; import NewWeeklyReport from "../../Components/NewWeeklyReport"; -import { Link } from "react-router-dom"; function UserNewTimeReportPage(): JSX.Element { const content = ( @@ -13,15 +12,7 @@ function UserNewTimeReportPage(): JSX.Element { const buttons = ( <> - -