diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index d444b85..12b0ee1 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -47,6 +47,8 @@ type Database interface { GetUserName(id int) (string, error) UnsignWeeklyReport(reportId int, projectManagerId int) error DeleteReport(reportID int) error + ChangeProjectName(projectName string, newProjectName string) error + ChangeUserPassword(username string, password string) error } // This struct is a wrapper type that holds the database connection @@ -670,3 +672,14 @@ func (d *Db) DeleteReport(reportID int) error { _, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID) return err } + +// ChangeProjectName is a handler that changes the name of a project +func (d *Db) ChangeProjectName(projectName string, newProjectName string) error { + _, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName) + return err +} + +func (d *Db) ChangeUserPassword(username string, password string) error { + _, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index f24175a..7b599f2 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1092,3 +1092,53 @@ func TestDeleteReport(t *testing.T) { } } + +func TestChangeProjectName(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Promote user to Admin + err = db.PromoteToAdmin("demouser") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } + + // Change project name + err = db.ChangeProjectName("projecttest", "newprojectname") + if err != nil { + t.Error("ChangeProjectName failed:", err) + } + + // Check if the project name was changed + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if projects[0].Name != "newprojectname" { + t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name) + } +} + +func TestChangeUserPassword(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + _ = db.AddUser("testuser", "password") + + // Change user password + err = db.ChangeUserPassword("testuser", "newpassword") + if err != nil { + t.Error("ChangeUserPassword failed:", err) + } + + // Check if the password was changed + if !db.CheckUser("testuser", "newpassword") { + t.Error("ChangeUserPassword failed: password not changed") + } + +} diff --git a/backend/internal/database/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/projects/ChangeProjectName.go b/backend/internal/handlers/projects/ChangeProjectName.go new file mode 100644 index 0000000..f6831db --- /dev/null +++ b/backend/internal/handlers/projects/ChangeProjectName.go @@ -0,0 +1,43 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ChangeProjectName is a handler that changes the name of a project +func ChangeProjectName(c *fiber.Ctx) error { + + //check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract the necessary parameters from the request + projectName := c.Params("projectName") + newProjectName := c.Query("newProjectName") + + // Check if user is site admin + issiteadmin, err := db.GetDb(c).IsSiteAdmin(username) + if err != nil { + log.Warn("Error checking if siteadmin:", err) + return c.Status(500).SendString(err.Error()) + } else if !issiteadmin { + log.Warn("User is not siteadmin") + return c.Status(401).SendString("User is not siteadmin") + } + + + // Perform the project name change + err = db.GetDb(c).ChangeProjectName(projectName, newProjectName) + if err != nil { + log.Warn("Error changing project name:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.Status(200).SendString("Project name changed successfully") +} diff --git a/backend/internal/handlers/reports/Statistics.go b/backend/internal/handlers/reports/Statistics.go index 8afa0f0..dac017d 100644 --- a/backend/internal/handlers/reports/Statistics.go +++ b/backend/internal/handlers/reports/Statistics.go @@ -16,6 +16,7 @@ func GetStatistics(c *fiber.Ctx) error { // Extract project name from query parameters projectName := c.Query("projectName") + userNameParam := c.Query("userName") log.Info(username, " trying to get statistics for project: ", projectName) @@ -24,18 +25,23 @@ func GetStatistics(c *fiber.Ctx) error { return c.Status(400).SendString("Missing project name") } - // If the user is not a project manager, they can't view statistics + // 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()) } - if !pm { - log.Info("Unauthorized access") + // 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 { diff --git a/backend/internal/handlers/users/ChangeUserPassword.go b/backend/internal/handlers/users/ChangeUserPassword.go new file mode 100644 index 0000000..1596247 --- /dev/null +++ b/backend/internal/handlers/users/ChangeUserPassword.go @@ -0,0 +1,42 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ChangeUserPassword is a handler that changes the password of a user +func ChangeUserPassword(c *fiber.Ctx) error { + + //Check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + admin := claims["name"].(string) + + // Extract the necessary parameters from the request + username := c.Params("username") + newPassword := c.Query("newPassword") + + // Check if user is site admin + issiteadmin, err := db.GetDb(c).IsSiteAdmin(admin) + if err != nil { + log.Warn("Error checking if siteadmin:", err) + return c.Status(500).SendString(err.Error()) + } else if !issiteadmin { + log.Warn("User is not siteadmin") + return c.Status(401).SendString("User is not siteadmin") + } + + // Perform the password change + err = db.GetDb(c).ChangeUserPassword(username, newPassword) + if err != nil { + log.Warn("Error changing password:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.Status(200).SendString("Password changed successfully") +} diff --git a/backend/main.go b/backend/main.go index 8a3466a..0c66369 100644 --- a/backend/main.go +++ b/backend/main.go @@ -110,6 +110,7 @@ func main() { api.Post("/promoteToAdmin", users.PromoteToAdmin) api.Put("/changeUserName", users.ChangeUserName) api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches + api.Put("/changeUserPassword/:username", users.ChangeUserPassword) // All project related routes // projectGroup := api.Group("/project") // Not currently in use @@ -125,6 +126,7 @@ func main() { api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) + api.Put("/changeProjectName/:projectName", projects.ChangeProjectName) // All report related routes // reportGroup := api.Group("/report") // Not currently in use diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index eb9f3f0..67768c2 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -269,7 +269,32 @@ interface API { getStatistics( projectName: string, token: string, + userName?: string, ): Promise>; + + /** + * Changes the name of a project + * @param {string} projectName The name of the project + * @param {string} newProjectName The new name of the project + * @param {string} token The authentication token + */ + changeProjectName( + projectName: string, + newProjectName: string, + token: string, + ): Promise>; + + /** + * Changes the password of a user + * @param {string} username The username of the user + * @param {string} newPassword The new password + * @param {string} token The authentication token + */ + changeUserPassword( + username: string, + newPassword: string, + token: string, + ): Promise>; } /** An instance of the API */ @@ -676,7 +701,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 }; @@ -975,12 +1004,13 @@ export const api: API = { } }, async getStatistics( - token: string, projectName: string, + token: string, + userName?: string, ): Promise> { try { const response = await fetch( - `/api/getStatistics/?projectName=${projectName}`, + `/api/getStatistics/?projectName=${projectName}&userName=${userName ?? ""}`, { method: "GET", headers: { @@ -1000,4 +1030,58 @@ export const api: API = { return { success: false, message: "Failed to get statistics" }; } }, + + async changeProjectName( + projectName: string, + newProjectName: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/changeProjectName/${projectName}?newProjectName=${newProjectName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + + if (!response.ok) { + return { success: false, message: "Failed to change project name" }; + } else { + return { success: true, message: "Project name changed" }; + } + } catch (e) { + return { success: false, message: "Failed to change project name" }; + } + }, + + async changeUserPassword( + username: string, + newPassword: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/changeUserPassword/${username}?newPassword=${newPassword}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + + if (!response.ok) { + return { success: false, message: "Failed to change password" }; + } else { + return { success: true, message: "Password changed" }; + } + } catch (e) { + return { success: false, message: "Failed to change password" }; + } + }, }; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index 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/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index d56ee42..5037a76 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -18,12 +18,13 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const { projectName, fetchedWeek } = useParams<{ + const { projectName, fetchedWeek, signedOrUnsigned } = useParams<{ projectName: string; fetchedWeek: string; + signedOrUnsigned: string; }>(); const username = localStorage.getItem("userName") ?? ""; - console.log(projectName, fetchedWeek); + console.log(projectName, fetchedWeek, signedOrUnsigned); useEffect(() => { const fetchWeeklyReport = async (): Promise => { @@ -59,7 +60,7 @@ export default function GetWeeklyReport(): JSX.Element { }; void fetchWeeklyReport(); - }, [projectName, fetchedWeek, token]); + }, [projectName, fetchedWeek, signedOrUnsigned, token]); const handleUpdateWeeklyReport = async (): Promise => { const updateWeeklyReport: UpdateWeeklyReport = { @@ -139,6 +140,12 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} + onClick={() => { + if (signedOrUnsigned === "signed") { + alert("You cannot edit a signed report."); + } + }} + readOnly={signedOrUnsigned === "signed"} /> @@ -168,6 +175,12 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} + onClick={() => { + if (signedOrUnsigned === "signed") { + alert("You cannot edit a signed report."); + } + }} + readOnly={signedOrUnsigned === "signed"} /> @@ -197,6 +210,12 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} + onClick={() => { + if (signedOrUnsigned === "signed") { + alert("You cannot edit a signed report."); + } + }} + readOnly={signedOrUnsigned === "signed"} /> @@ -226,6 +245,12 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} + onClick={() => { + if (signedOrUnsigned === "signed") { + alert("You cannot edit a signed report."); + } + }} + readOnly={signedOrUnsigned === "signed"} /> @@ -255,6 +280,12 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} + onClick={() => { + if (signedOrUnsigned === "signed") { + alert("You cannot edit a signed report."); + } + }} + readOnly={signedOrUnsigned === "signed"} /> @@ -284,18 +315,26 @@ export default function GetWeeklyReport(): JSX.Element { ) event.preventDefault(); }} + onClick={() => { + if (signedOrUnsigned === "signed") { + alert("You cannot edit a signed report."); + } + }} + readOnly={signedOrUnsigned === "signed"} /> -
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/OtherUsersTR.tsx b/frontend/src/Components/OtherUsersTR.tsx index ce7761c..40e0b94 100644 --- a/frontend/src/Components/OtherUsersTR.tsx +++ b/frontend/src/Components/OtherUsersTR.tsx @@ -1,7 +1,8 @@ import { useState, useEffect } from "react"; import { WeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; +import Button from "./Button"; /** * Renders the component for editing a weekly report. @@ -17,11 +18,14 @@ export default function OtherUsersTR(): JSX.Element { const [ownWorkTime, setOwnWorkTime] = useState(0); const [studyTime, setStudyTime] = useState(0); const [testingTime, setTestingTime] = useState(0); + const [reportId, setReportId] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; const { projectName } = useParams(); const { username } = useParams(); const { fetchedWeek } = useParams(); + const { signedOrUnsigned } = useParams(); + console.log(projectName, username, fetchedWeek, signedOrUnsigned); useEffect(() => { const fetchUsersWeeklyReport = async (): Promise => { @@ -45,6 +49,7 @@ export default function OtherUsersTR(): JSX.Element { studyTime: 0, testingTime: 0, }; + setReportId(report.reportId); setWeek(report.week); setDevelopmentTime(report.developmentTime); setMeetingTime(report.meetingTime); @@ -60,6 +65,27 @@ export default function OtherUsersTR(): JSX.Element { void fetchUsersWeeklyReport(); }); + const handleUnsignWeeklyReport = async (): Promise => { + const response = await api.unsignReport(reportId, token); + console.log(response); + console.log(reportId); + if (response.success) { + return true; + } else { + return false; + } + }; + + const handleDeleteWeeklyReport = async (): Promise => { + const response = await api.deleteWeeklyReport(reportId, token); + console.log(response); + if (response.success) { + return true; + } + return false; + }; + const navigate = useNavigate(); + return ( <>

{username}'s Report

@@ -153,6 +179,48 @@ export default function OtherUsersTR(): JSX.Element { +
+ {signedOrUnsigned === "signed" && ( +
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..4be3397 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -4,19 +4,62 @@ import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; import { Link } from "react-router-dom"; import GetProjectTimes, { projectTimes } from "./GetProjectTimes"; import DeleteProject from "./DeleteProject"; +import InputField from "./InputField"; +import ProjectNameInput from "./Inputs/ProjectNameInput"; +import { alphanumeric } from "../Data/regex"; +import { projNameHighLimit, projNameLowLimit } from "../Data/constants"; +import ChangeProjectName from "./ChangeProjectName"; function ProjectInfoModal(props: { projectname: string; onClose: () => void; - onClick: (username: string) => 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}?`, + ) + ) { + ChangeProjectName({ + projectName: props.projectname, + newProjectName: newProjName, + }); + } else { + alert("Name was not changed!"); + } + }; + useEffect(() => { if (times?.totalTime !== undefined) { totalTime.current = times.totalTime; @@ -28,44 +71,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 52e8559..e06ed75 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { Link, useParams } from "react-router-dom"; import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; +import { api } from "../API/API"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); @@ -11,34 +12,68 @@ function ProjectMembers(): JSX.Element { setUsersProp: setProjectMembers, }); + const handleUserDeleteClick = async (username: string): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.removeUserFromProject( + username, + projectName ?? "", + token, + ); + console.log(response.data); + + // Remove the deleted user from the state + setProjectMembers((prevMembers) => + prevMembers.filter((member) => member.Username !== username), + ); + }; + return ( <>

    All Members In: {projectName}{" "}

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

    -
    -
    -

    {projectMember.Username}

    - Role: -

    {projectMember.UserRole}

    -
    -
    -
    - -

    - View Reports -

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

    +
    +
    +

    {projectMember.Username}

    + Role: +

    {projectMember.UserRole}

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

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

    + )} + +

    + View Reports +

    + +
    -

    -

    - ))} + + ); + })}
    ); 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 +189,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 = ( <> + +