diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 9a92b80..d444b85 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,11 +2,12 @@ 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" ) @@ -22,8 +23,6 @@ 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 @@ -41,6 +40,7 @@ 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 @@ -52,7 +52,7 @@ type Database interface { // 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.DB + *sqlx.Tx } type UserProjectMember struct { @@ -94,8 +94,19 @@ 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) Database { +func DbConnect(dbpath string) sqlx.DB { // Open the database db, err := sqlx.Connect("sqlite", dbpath) if err != nil { @@ -108,7 +119,25 @@ func DbConnect(dbpath string) Database { panic(err) } - return &Db{db} + 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 } func (d *Db) CheckUser(username string, password string) bool { @@ -212,25 +241,15 @@ 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) + _, err := d.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 = tx.Exec(addUserToProject, username, name, "project_manager") + _, err = d.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 } @@ -238,16 +257,7 @@ func (d *Db) AddProject(name string, description string, username string) error } 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) - } + _, err := d.Exec(deleteProject, projectID, username) return err } @@ -471,7 +481,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 (d *Db) Migrate() error { +func Migrate(db sqlx.DB) error { // Read the embedded scripts directory files, err := scripts.ReadDir("migrations") if err != nil { @@ -483,7 +493,7 @@ func (d *Db) Migrate() error { return nil } - tr := d.MustBegin() + tr := db.MustBegin() // Iterate over each SQL file and execute it for _, file := range files { @@ -569,7 +579,7 @@ func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, d } // MigrateSampleData applies sample data to the database. -func (d *Db) MigrateSampleData() error { +func MigrateSampleData(db sqlx.DB) error { // Insert sample data files, err := sampleData.ReadDir("sample_data") if err != nil { @@ -579,7 +589,7 @@ func (d *Db) MigrateSampleData() error { if len(files) == 0 { println("No sample data files found") } - tr := d.MustBegin() + tr := db.MustBegin() // Iterate over each SQL file and execute it for _, file := range files { @@ -616,7 +626,7 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { WHERE projects.name = ? ` - rows, err := d.DB.Query(query, projectName) + rows, err := d.Query(query, projectName) if err != nil { return nil, err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index b5a598c..f24175a 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -9,11 +9,13 @@ import ( // setupState initializes a database instance with necessary setup for testing func setupState() (Database, error) { db := DbConnect(":memory:") - err := db.Migrate() + err := Migrate(db) if err != nil { return nil, err } - return db, nil + + db_iface := Db{db.MustBegin()} + return &db_iface, nil } // This is a more advanced setup that includes more data in the database. @@ -1078,7 +1080,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) } @@ -1088,5 +1090,5 @@ func TestDeleteReport(t *testing.T) { if err == nil { t.Error("RemoveReport failed: report not removed") } - + } diff --git a/backend/internal/database/middleware.go b/backend/internal/database/middleware.go index 69fa3a2..b73a42f 100644 --- a/backend/internal/database/middleware.go +++ b/backend/internal/database/middleware.go @@ -1,11 +1,28 @@ package database -import "github.com/gofiber/fiber/v2" +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/jmoiron/sqlx" +) -// Simple middleware that provides a shared database pool as a local key "db" -func DbMiddleware(db *Database) func(c *fiber.Ctx) error { +// Simple middleware that provides a transaction as a local key "db" +func DbMiddleware(db *sqlx.DB) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { - c.Locals("db", db) + 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) 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 70499b0..f519608 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -1,58 +1,220 @@ INSERT OR IGNORE INTO users(username, password) -VALUES ("admin", "123"); +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"); -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 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 user_roles(user_id,project_id,p_role) -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"); +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"); 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); +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); -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); +INSERT OR IGNORE INTO site_admin VALUES (1); \ No newline at end of file diff --git a/backend/internal/handlers/reports/Statistics.go b/backend/internal/handlers/reports/Statistics.go new file mode 100644 index 0000000..dac017d --- /dev/null +++ b/backend/internal/handlers/reports/Statistics.go @@ -0,0 +1,56 @@ +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/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 234781b..5550b3f 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -66,6 +66,15 @@ 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 dbf5151..8a3466a 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 = db.Migrate(); err != nil { + if err = database.Migrate(db); err != nil { fmt.Println("Error migrating database: ", err) os.Exit(1) } // Migrate sample data, should not be used in production - if err = db.MigrateSampleData(); err != nil { + if err = database.MigrateSampleData(db); err != nil { fmt.Println("Error migrating sample data: ", err) os.Exit(1) } @@ -126,12 +126,12 @@ func main() { api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) - // 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 de5f19d..6f9769f 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -11,6 +11,7 @@ import { NewProject, WeeklyReport, StrNameChange, + Statistics, } from "../Types/goTypes"; /** @@ -258,6 +259,18 @@ 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>; } /** An instance of the API */ @@ -664,7 +677,11 @@ export const api: API = { }); if (!response.ok) { - return { success: false, message: "Failed to login" }; + return { + success: false, + data: `${response.status}`, + message: "Failed to login", + }; } else { const data = (await response.json()) as { token: string }; // Update the type of 'data' return { success: true, data: data.token }; @@ -962,4 +979,31 @@ 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" }; + } + }, }; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index c8a1c66..8c9c566 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -1,9 +1,13 @@ 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. @@ -12,11 +16,26 @@ import Button from "./Button"; 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(), @@ -30,6 +49,7 @@ 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); @@ -44,7 +64,7 @@ function AddProject(): JSX.Element {
{ e.preventDefault(); void handleCreateProject(); @@ -52,33 +72,29 @@ function AddProject(): JSX.Element { > TTIME Logo

Create a new project

-
- { - e.preventDefault(); - setName(e.target.value); - }} - /> - { - e.preventDefault(); - setDescription(e.target.value); - }} - /> -
-
+ { + e.preventDefault(); + setName(e.target.value); + }} + /> +
+ { + e.preventDefault(); + setDescription(e.target.value); + }} + placeholder={"Description (Optional)"} + /> +
diff --git a/frontend/src/Components/InputField.tsx b/frontend/src/Components/InputField.tsx index 699d8fa..5a5cdaf 100644 --- a/frontend/src/Components/InputField.tsx +++ b/frontend/src/Components/InputField.tsx @@ -4,19 +4,21 @@ * @returns {JSX.Element} The input field * @example * { * setExample(e.target.value); * }} - * value={example} * /> */ function InputField(props: { - label: string; - type: string; - value: string; - onChange: (e: React.ChangeEvent) => void; + label?: string; + placeholder?: string; + type?: string; + value?: string; + onChange?: (e: React.ChangeEvent) => void; }): JSX.Element { return (
@@ -30,7 +32,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.label} + placeholder={props.placeholder} value={props.value} onChange={props.onChange} /> diff --git a/frontend/src/Components/Inputs/DescriptionInput.tsx b/frontend/src/Components/Inputs/DescriptionInput.tsx new file mode 100644 index 0000000..43e046c --- /dev/null +++ b/frontend/src/Components/Inputs/DescriptionInput.tsx @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..9f67e98 --- /dev/null +++ b/frontend/src/Components/Inputs/PasswordInput.tsx @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..de28c12 --- /dev/null +++ b/frontend/src/Components/Inputs/ProjectNameInput.tsx @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..8f653ba --- /dev/null +++ b/frontend/src/Components/Inputs/UsernameInput.tsx @@ -0,0 +1,50 @@ +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 f44d7f3..50ffb98 100644 --- a/frontend/src/Components/LoginCheck.tsx +++ b/frontend/src/Components/LoginCheck.tsx @@ -11,6 +11,10 @@ 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, @@ -42,7 +46,15 @@ function LoginCheck(props: { console.error("Token was undefined"); } } else { - console.error("Token could not be fetched/No such user"); + 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"); + } } }) .catch((error) => { diff --git a/frontend/src/Components/LoginField.tsx b/frontend/src/Components/LoginField.tsx index dda1714..8d0aa62 100644 --- a/frontend/src/Components/LoginField.tsx +++ b/frontend/src/Components/LoginField.tsx @@ -33,6 +33,7 @@ function Login(props: { props.setUsername(e.target.value); }} value={props.username} + placeholder={"Username"} />
+ ); +} diff --git a/frontend/src/Components/PMProjectMenu.tsx b/frontend/src/Components/PMProjectMenu.tsx index ce7c5c5..f0cb492 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 1f98d79..71a72fb 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -4,19 +4,60 @@ 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"; function ProjectInfoModal(props: { projectname: string; onClose: () => void; - onClick: (username: string) => void; + onClick: (username: string, userRole: 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}?`, + ) + ) { + //TODO: change and insert change name functionality + alert("Not implemented yet"); + setNewProjName(""); + } else { + alert("Name was not changed!"); + } + }; + useEffect(() => { if (times?.totalTime !== undefined) { totalTime.current = times.totalTime; @@ -28,44 +69,88 @@ function ProjectInfoModal(props: { className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm flex justify-center items-center" > -
+
-

- {props.projectname} -

-
-

Statistics:

-
-
-

Number of members: {users.length}

-

+

{props.projectname}

+

+ (Change project name) +

+ {showInput && ( + <> +

Change name:

+
+ +
+
+
+ + )} +

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:

-
-
-
    -
    - {users.map((user) => ( -
  • { - props.onClick(user.Username); - }} - > - - Name: {user.Username} -
    - Role: {user.UserRole} -
    -
  • - ))} +

    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} +
      +
    • + ))}
    diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index 294a131..6461dae 100644 --- a/frontend/src/Components/ProjectListAdmin.tsx +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -2,6 +2,7 @@ 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 @@ -21,9 +22,12 @@ 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): void => { + const handleClickUser = (username: string, userRole: string): void => { setUsername(username); + setUserRole(userRole); setUserModalVisible(true); }; @@ -39,11 +43,13 @@ export function ProjectListAdmin(props: { const handleCloseUser = (): void => { setUsername(""); + setUserRole(""); setUserModalVisible(false); }; return ( <> +

    Manage Projects

    {projectModalVisible && ( )}
    -
      - {props.projects.map((project) => ( -
    • { - handleClickProject(project.name); - }} - > - {project.name} -
    • - ))} + { + 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} +
      • + ))}
    diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 06825bc..e06ed75 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -50,7 +50,7 @@ function ProjectMembers(): JSX.Element { {projectMember.Username !== localStorage.getItem("username") && (

    { confirm( "Are you sure you want to delete this user? This action cannot be undone.", @@ -64,7 +64,7 @@ function ProjectMembers(): JSX.Element { -

    +

    View Reports

    diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index be35a74..7310e4f 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -3,25 +3,44 @@ import { NewUser } from "../Types/goTypes"; import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -import InputField from "./InputField"; +import UsernameInput from "./Inputs/UsernameInput"; +import PasswordInput from "./Inputs/PasswordInput"; +import { alphanumeric, lowercase } from "../Data/regex"; +import { + passwordLength, + usernameLowLimit, + usernameUpLimit, +} from "../Data/constants"; /** * 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 === "" || password === "") { - alert("Must provide username and password"); + 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 (.-!?/*)", + ); return; } const newUser: NewUser = { - username: username?.replace(/ /g, "") ?? "", - password: password ?? "", + username: username, + password: password, }; const response = await api.registerUser(newUser); if (response.success) { @@ -39,7 +58,7 @@ export default function Register(): JSX.Element {
    { e.preventDefault(); void handleRegister(); @@ -47,31 +66,28 @@ 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:

    @@ -87,7 +187,9 @@ function UserInfoModal(props: { text={"Close"} onClick={function (): void { setNewUsername(""); - setShowInput(false); + setNewPassword(""); + setShowNameInput(false); + setShowPwordInput(false); props.onClose(); }} type="button" diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index 76cae9f..23e49db 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,5 +1,6 @@ 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 @@ -15,6 +16,7 @@ import UserInfoModal from "./UserInfoModal"; 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); @@ -28,24 +30,39 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element { return ( <> +

    Manage Users

    -
      - {props.users.map((user) => ( -
    • { - handleClick(user); - }} - > - {user} -
    • - ))} + { + setSearch(e.target.value); + }} + /> +
        + {props.users + .filter((user) => { + return search.toLowerCase() === "" + ? user + : user.toLowerCase().includes(search.toLowerCase()); + }) + .map((user) => ( +
      • { + handleClick(user); + }} + > + {user} +
      • + ))}
    diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx index bc85c5b..8f28ce9 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 e307e90..4be4dee 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 new file mode 100644 index 0000000..c84f1a0 --- /dev/null +++ b/frontend/src/Components/UserStatistics.tsx @@ -0,0 +1,150 @@ +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 new file mode 100644 index 0000000..c803ad4 --- /dev/null +++ b/frontend/src/Data/constants.ts @@ -0,0 +1,36 @@ +//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 new file mode 100644 index 0000000..ceb22cd --- /dev/null +++ b/frontend/src/Data/regex.ts @@ -0,0 +1,9 @@ +/** + * 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 6c03c01..296dc59 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,14 +13,7 @@ function AdminManageProjects(): JSX.Element { setProjectsProp: setProjects, username: localStorage.getItem("username") ?? "", }); - const content = ( - <> -

      Manage Projects

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

      Manage Users

      -
      - -
      - - ); + const content = ; const buttons = ( <> diff --git a/frontend/src/Pages/AdminPages/AdminMenuPage.tsx b/frontend/src/Pages/AdminPages/AdminMenuPage.tsx index ed2118d..52a4198 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 fa592c9..e28c338 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx @@ -1,11 +1,12 @@ 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/ProjectManagerPages/PMOtherUsersTR.tsx b/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx index cb558b0..b586b64 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx @@ -1,8 +1,12 @@ 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 = ( <> @@ -11,6 +15,15 @@ function PMOtherUsersTR(): JSX.Element { const buttons = ( <> + +