diff --git a/.gitignore b/.gitignore index 281e866..c50fe24 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ diagram.puml backend/*.png backend/*.jpg backend/*.svg +__pycache__ /go.work.sum /package-lock.json diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 7a08b0e..c8b020d 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -108,6 +108,56 @@ const docTemplate = `{ } } }, + "/promote/{projectName}": { + "put": { + "security": [ + { + "JWT": [] + } + ], + "description": "Promote a user to project manager", + "consumes": [ + "text/plain" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "Auth" + ], + "summary": "Promote to project manager", + "parameters": [ + { + "type": "string", + "description": "Project name", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User name", + "name": "userName", + "in": "query", + "required": true + } + ], + "responses": { + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, "/promoteToAdmin": { "post": { "security": [ diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index f4c0f6e..0bd67bc 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -17,6 +17,7 @@ type Database interface { AddUser(username string, password string) error CheckUser(username string, password string) bool RemoveUser(username string) error + RemoveUserFromProject(username string, projectname string) error PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error @@ -35,7 +36,7 @@ type Database interface { GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) - GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error) + GetAllWeeklyReports(username string, projectname string) ([]types.WeeklyReportList, error) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) @@ -86,6 +87,10 @@ const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles JOIN projects ON user_roles.project_id = projects.id WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'` +const removeUserFromProjectQuery = `DELETE FROM user_roles + WHERE user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?)` + // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database @@ -147,6 +152,11 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) return err } +func (d *Db) RemoveUserFromProject(username string, projectname string) error { + _, err := d.Exec(removeUserFromProjectQuery, username, projectname) + return err +} + // ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { // Execute the SQL query to change the user's role @@ -463,8 +473,8 @@ func (d *Db) Migrate() error { return nil } -// GetWeeklyReportsUser retrieves weekly reports for a specific user and project. -func (d *Db) GetWeeklyReportsUser(username string, projectName string) ([]types.WeeklyReportList, error) { +// GetAllWeeklyReports retrieves weekly reports for a specific user and project. +func (d *Db) GetAllWeeklyReports(username string, projectName string) ([]types.WeeklyReportList, error) { query := ` SELECT wr.week, diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index fe3e6cd..a691a4d 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -705,7 +705,7 @@ func TestGetWeeklyReportsUser(t *testing.T) { t.Error("AddWeeklyReport failed:", err) } - reports, err := db.GetWeeklyReportsUser("testuser", "testproject") + reports, err := db.GetAllWeeklyReports("testuser", "testproject") if err != nil { t.Error("GetWeeklyReportsUser failed:", err) } @@ -962,6 +962,5 @@ func TestRemoveProject(t *testing.T) { if len(projects) != 0 { t.Error("RemoveProject failed: expected 0, got", len(projects)) } - + } - \ No newline at end of file diff --git a/backend/internal/handlers/projects/AddUserToProject.go b/backend/internal/handlers/projects/AddUserToProject.go index 702b7dd..3195314 100644 --- a/backend/internal/handlers/projects/AddUserToProject.go +++ b/backend/internal/handlers/projects/AddUserToProject.go @@ -10,42 +10,33 @@ import ( // AddUserToProjectHandler is a handler that adds a user to a project with a specified role func AddUserToProjectHandler(c *fiber.Ctx) error { - // Extract necessary parameters from the request - var requestData struct { - Username string `json:"username"` - ProjectName string `json:"projectName"` - Role string `json:"role"` - } - if err := c.BodyParser(&requestData); err != nil { - log.Info("Error parsing request body:", err) - return c.Status(400).SendString("Bad request") - } - - // Check if the user adding another user to the project is a site admin user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) - adminUsername := claims["name"].(string) - log.Info("Admin username from claims:", adminUsername) + pm_name := claims["name"].(string) - isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) + project := c.Params("projectName") + username := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) if err != nil { - log.Info("Error checking admin status:", err) + log.Info("Error checking if user is project manager:", err) return c.Status(500).SendString(err.Error()) } - if !isAdmin { - log.Info("User is not a site admin:", adminUsername) - return c.Status(403).SendString("User is not a site admin") + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") } // Add the user to the project with the specified role - err = db.GetDb(c).AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + err = db.GetDb(c).AddUserToProject(username, project, "member") if err != nil { log.Info("Error adding user to project:", err) return c.Status(500).SendString(err.Error()) } // Return success message - log.Info("User added to project successfully:", requestData.Username) + log.Info("User : ", username, " added to project: ", project) return c.SendStatus(fiber.StatusOK) } diff --git a/backend/internal/handlers/projects/PromoteToPm.go b/backend/internal/handlers/projects/PromoteToPm.go new file mode 100644 index 0000000..ffe2215 --- /dev/null +++ b/backend/internal/handlers/projects/PromoteToPm.go @@ -0,0 +1,51 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// @Summary Promote to project manager +// @Description Promote a user to project manager +// @Tags Auth +// @Security JWT +// @Accept plain +// @Produce plain +// @Param projectName path string true "Project name" +// @Param userName query string true "User name" +// @Failure 500 {string} string "Internal server error" +// @Failure 403 {string} string "Forbidden" +// @Router /promote/{projectName} [put] +// +// Login logs in a user and returns a JWT token +// Promote to project manager +func PromoteToPm(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + pm_name := claims["name"].(string) + + project := c.Params("projectName") + new_pm_name := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") + } + + // Add the user to the project with the specified role + err = db.GetDb(c).ChangeUserRole(new_pm_name, project, "project_manager") + + // Return success message + log.Info("User : ", new_pm_name, " promoted to project manager in project: ", project) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/projects/RemoveUserFromProject.go b/backend/internal/handlers/projects/RemoveUserFromProject.go new file mode 100644 index 0000000..7aefcf8 --- /dev/null +++ b/backend/internal/handlers/projects/RemoveUserFromProject.go @@ -0,0 +1,40 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func RemoveUserFromProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + pm_name := claims["name"].(string) + + project := c.Params("projectName") + username := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") + } + + // Remove the user from the project + if err = db.GetDb(c).RemoveUserFromProject(username, project); err != nil { + log.Info("Error removing user from project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User : ", username, " removed from project: ", project) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/reports/GetAllWeeklyReports.go b/backend/internal/handlers/reports/GetAllWeeklyReports.go new file mode 100644 index 0000000..ee81c82 --- /dev/null +++ b/backend/internal/handlers/reports/GetAllWeeklyReports.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" +) + +// GetAllWeeklyReports retrieves all weekly reports for a user in a specific project +func GetAllWeeklyReports(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 and week from query parameters + projectName := c.Params("projectName") + target_user := c.Query("targetUser") // The user whose reports are being requested + + // If the target user is not empty, use it as the username + if target_user == "" { + target_user = username + } + + log.Info(username, " trying to get all weekly reports for: ", target_user) + + if projectName == "" { + log.Info("Missing project name") + return c.Status(400).SendString("Missing project name") + } + + // If the user is not a project manager, they can only view their own reports + 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 == false && target_user != username { + log.Info("Unauthorized access") + return c.Status(403).SendString("Unauthorized access") + } + + // Retrieve weekly reports for the user in the project from the database + reports, err := db.GetDb(c).GetAllWeeklyReports(target_user, projectName) + if err != nil { + log.Error("Error getting weekly reports for user:", target_user, "in project:", projectName, ":", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning weekly report") + // Return the retrieved weekly report + return c.JSON(reports) +} diff --git a/backend/internal/handlers/reports/GetWeeklyReport.go b/backend/internal/handlers/reports/GetWeeklyReport.go index 422bc0b..04bdc0d 100644 --- a/backend/internal/handlers/reports/GetWeeklyReport.go +++ b/backend/internal/handlers/reports/GetWeeklyReport.go @@ -16,11 +16,17 @@ func GetWeeklyReport(c *fiber.Ctx) error { claims := user.Claims.(jwt.MapClaims) username := claims["name"].(string) - log.Info("Getting weekly report for: ", username) - // Extract project name and week from query parameters projectName := c.Query("projectName") week := c.Query("week") + target_user := c.Query("targetUser") // The user whose report is being requested + + // If the target user is not empty, use it as the username + if target_user == "" { + target_user = username + } + + log.Info(username, " trying to get weekly report for: ", target_user) if projectName == "" || week == "" { log.Info("Missing project name or week number") @@ -34,8 +40,20 @@ func GetWeeklyReport(c *fiber.Ctx) error { return c.Status(400).SendString("Invalid week number") } + // If the token user is not an admin, check if the target user is the same as the token user + 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 == false && target_user != username { + log.Info("Unauthorized access") + return c.Status(403).SendString("Unauthorized access") + } + // Call the database function to get the weekly report - report, err := db.GetDb(c).GetWeeklyReport(username, projectName, weekInt) + report, err := db.GetDb(c).GetWeeklyReport(target_user, projectName, weekInt) if err != nil { log.Info("Error getting weekly report from db:", err) return c.Status(500).SendString(err.Error()) diff --git a/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go b/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go deleted file mode 100644 index da8a90b..0000000 --- a/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go +++ /dev/null @@ -1,36 +0,0 @@ -package reports - -import ( - db "ttime/internal/database" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project -func GetWeeklyReportsUserHandler(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract necessary (path) parameters from the request - projectName := c.Params("projectName") - - // TODO: Here we need to check whether the user is a member of the project - // If not, we should return an error. On the other hand, if the user not a member, - // the returned list of reports will (should) allways be empty. - - // Retrieve weekly reports for the user in the project from the database - reports, err := db.GetDb(c).GetWeeklyReportsUser(username, projectName) - if err != nil { - log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning weekly reports for user:", username, "in project:", projectName) - - // Return the list of reports as JSON - return c.JSON(reports) -} diff --git a/backend/main.go b/backend/main.go index 6e65386..42daa5c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -119,6 +119,9 @@ func main() { api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) api.Post("/project", projects.CreateProject) api.Post("/ProjectRoleChange", projects.ProjectRoleChange) + api.Put("/promoteToPm/:projectName", projects.PromoteToPm) + api.Put("/addUserToProject/:projectName", projects.AddUserToProjectHandler) + api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) api.Delete("/removeProject/:projectName", projects.RemoveProject) api.Delete("/project/:projectID", projects.DeleteProject) @@ -126,10 +129,9 @@ func main() { // reportGroup := api.Group("/report") // Not currently in use api.Get("/getWeeklyReport", reports.GetWeeklyReport) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) - api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) + api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports) api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Put("/signReport/:reportId", reports.SignReport) - api.Put("/addUserToProject", projects.AddUserToProjectHandler) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) // Announce the port we are listening on and start the server diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 890a7e6..cf3d1eb 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,8 +1,9 @@ -import { NewProjMember } from "../Components/AddMember"; +import { AddMemberInfo } from "../Components/AddMember"; import { ProjectRoleChange } from "../Components/ChangeRole"; import { projectTimes } from "../Components/GetProjectTimes"; import { ProjectMember } from "../Components/GetUsersInProject"; import { + UpdateWeeklyReport, NewWeeklyReport, NewUser, User, @@ -87,16 +88,31 @@ interface API { token: string, ): Promise>; - /** Gets a weekly report for a specific user, project and week + /** + * Updates a weekly report. + * @param {UpdateWeeklyReport} weeklyReport The updated weekly report object. + * @param {string} token The authentication token. + * @returns {Promise>} A promise containing the API response with the updated report. + */ + updateWeeklyReport( + weeklyReport: UpdateWeeklyReport, + token: string, + ): Promise>; + + /** Gets a weekly report for a specific user, project and week. + * Keep in mind that the user within the token needs to be PM + * of the project to get the report, unless the user is the target user. * @param {string} projectName The name of the project. * @param {string} week The week number. * @param {string} token The authentication token. + * @param {string} targetUser The username of the target user. Defaults to token user. * @returns {Promise>} A promise resolving to an API response with the retrieved report. */ getWeeklyReport( projectName: string, week: string, token: string, + targetUser?: string, ): Promise>; /** @@ -106,9 +122,10 @@ interface API { * @param {string} token The token of the user * @returns {APIResponse} A list of weekly reports */ - getWeeklyReportsForUser( + getAllWeeklyReportsForUser( projectName: string, token: string, + targetUser?: string, ): Promise>; /** Gets all the projects of a user @@ -147,6 +164,17 @@ interface API { projectName: string, token: string, ): Promise>; + + /** Gets all unsigned reports in a project. + * @param {string} projectName The name of the project. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response containing the list of unsigned reports. + */ + getUnsignedReportsInProject( + projectName: string, + token: string, + ): Promise>; + /** * Changes the username of a user in the database. * @param {StrNameChange} data The object containing the previous and new username. @@ -169,7 +197,13 @@ interface API { ): Promise>; addUserToProject( - user: NewProjMember, + addMemberInfo: AddMemberInfo, + token: string, + ): Promise>; + + removeUserFromProject( + user: string, + project: string, token: string, ): Promise>; @@ -186,6 +220,19 @@ interface API { * @param {string} token The authentication token */ signReport(reportId: number, token: string): Promise>; + + /** + * Promotes a user to project manager within a project. + * + * @param {string} userName The username of the user to promote + * @param {string} projectName The name of the project to promote the user in + * @returns {Promise} A promise resolving to an API response. + */ + promoteToPm( + userName: string, + projectName: string, + token: string, + ): Promise>; } /** An instance of the API */ @@ -295,18 +342,20 @@ export const api: API = { }, async addUserToProject( - user: NewProjMember, + addMemberInfo: AddMemberInfo, token: string, ): Promise> { try { - const response = await fetch("/api/addUserToProject", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/addUserToProject/${addMemberInfo.projectName}/?userName=${addMemberInfo.userName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - body: JSON.stringify(user), - }); + ); if (!response.ok) { return { success: false, message: "Failed to add member" }; @@ -318,6 +367,31 @@ export const api: API = { } }, + async removeUserFromProject( + user: string, + project: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/removeUserFromProject/${project}?userName=${user}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + if (!response.ok) { + return { success: false, message: "Failed to remove member" }; + } + } catch (e) { + return { success: false, message: "Failed to remove member" }; + } + return { success: true, message: "Removed member" }; + }, + async renewToken(token: string): Promise> { try { const response = await fetch("/api/loginrenew", { @@ -458,14 +532,46 @@ export const api: API = { } }, + async updateWeeklyReport( + weeklyReport: UpdateWeeklyReport, + token: string, + ): Promise> { + try { + const response = await fetch("/api/updateWeeklyReport", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(weeklyReport), + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to update weekly report", + }; + } + + const data = await response.text(); + return { success: true, message: data }; + } catch (e) { + return { + success: false, + message: "Failed to update weekly report", + }; + } + }, + async getWeeklyReport( projectName: string, week: string, token: string, + targetUser?: string, ): Promise> { try { const response = await fetch( - `/api/getWeeklyReport?projectName=${projectName}&week=${week}`, + `/api/getWeeklyReport?projectName=${projectName}&week=${week}&targetUser=${targetUser ?? ""}`, { method: "GET", headers: { @@ -486,18 +592,22 @@ export const api: API = { } }, - async getWeeklyReportsForUser( + async getAllWeeklyReportsForUser( projectName: string, token: string, + targetUser?: string, ): Promise> { try { - const response = await fetch(`/api/getWeeklyReportsUser/${projectName}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/getAllWeeklyReports/${projectName}?targetUser=${targetUser ?? ""}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - }); + ); if (!response.ok) { return { @@ -621,6 +731,38 @@ export const api: API = { } }, + async getUnsignedReportsInProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/getUnsignedReports/${projectName}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { + success: false, + message: + "Failed to get unsigned reports for project: Response code " + + response.status, + }; + } else { + const data = (await response.json()) as WeeklyReport[]; + return { success: true, data }; + } + } catch (e) { + return { + success: false, + message: "Failed to get unsigned reports for project, unknown error", + }; + } + }, + async changeUserName( data: StrNameChange, token: string, @@ -697,4 +839,35 @@ export const api: API = { return { success: false, message: "Failed to sign report" }; } }, + + async promoteToPm( + userName: string, + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/promoteToPm/${projectName}?userName=${userName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + if (!response.ok) { + return { + success: false, + message: "Failed to promote user to project manager", + }; + } + } catch (e) { + return { + success: false, + message: "Failed to promote user to project manager", + }; + } + return { success: true, message: "User promoted to project manager" }; + }, }; diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx index 194afe8..d8036b7 100644 --- a/frontend/src/Components/AddMember.tsx +++ b/frontend/src/Components/AddMember.tsx @@ -1,44 +1,35 @@ -import { APIResponse, api } from "../API/API"; +import { api } from "../API/API"; -export interface NewProjMember { - username: string; - role: string; - projectname: string; +export interface AddMemberInfo { + userName: string; + projectName: string; } /** * Tries to add a member to a project - * @param {Object} props - A NewProjMember - * @returns {boolean} True if added, false if not + * @param {AddMemberInfo} props.membertoAdd - Contains user's name and project's name + * @returns {Promise} */ -function AddMember(props: { memberToAdd: NewProjMember }): boolean { - let added = false; - if ( - props.memberToAdd.username === "" || - props.memberToAdd.role === "" || - props.memberToAdd.projectname === "" - ) { - alert("All fields must be filled before adding"); - return added; +async function AddMember(props: { memberToAdd: AddMemberInfo }): Promise { + if (props.memberToAdd.userName === "") { + alert("You must choose at least one user to add"); + return; } - api - .addUserToProject( + try { + const response = await api.addUserToProject( props.memberToAdd, localStorage.getItem("accessToken") ?? "", - ) - .then((response: APIResponse) => { - if (response.success) { - alert("Member added"); - added = true; - } else { - alert("Member not added"); - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during member add:", error); - }); - return added; + ); + if (response.success) { + alert(`[${props.memberToAdd.userName}] added`); + } else { + alert(`[${props.memberToAdd.userName}] not added`); + console.error(response.message); + } + } catch (error) { + alert(`[${props.memberToAdd.userName}] not added`); + console.error("An error occurred during member add:", error); + } } export default AddMember; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index e2ad8b9..c8a1c66 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -1,37 +1,10 @@ import { useState } from "react"; -import { APIResponse, api } from "../API/API"; +import { api } from "../API/API"; import { NewProject } from "../Types/goTypes"; import InputField from "./InputField"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -/** - * Tries to add a project to the system - * @param {Object} props - Project name and description - * @returns {boolean} True if created, false if not - */ -function CreateProject(props: { name: string; description: string }): void { - const project: NewProject = { - name: props.name, - description: props.description, - }; - - api - .createProject(project, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { - if (response.success) { - alert("Project added!"); - } else { - alert("Project NOT added!"); - console.error(response.message); - } - }) - .catch((error) => { - alert("Project NOT added!"); - console.error("An error occurred during creation:", error); - }); -} - /** * Provides UI for adding a project to the system. * @returns {JSX.Element} - Returns the component UI for adding a project @@ -40,6 +13,33 @@ function AddProject(): JSX.Element { const [name, setName] = useState(""); const [description, setDescription] = useState(""); + /** + * Tries to add a project to the system + */ + const handleCreateProject = async (): Promise => { + const project: NewProject = { + name: name.replace(/ /g, ""), + description: description.trim(), + }; + try { + const response = await api.createProject( + project, + localStorage.getItem("accessToken") ?? "", + ); + if (response.success) { + alert(`${project.name} added!`); + setDescription(""); + setName(""); + } else { + alert("Project not added, name could be taken"); + console.error(response.message); + } + } catch (error) { + alert("Project not added"); + console.error(error); + } + }; + return (
@@ -47,10 +47,7 @@ function AddProject(): JSX.Element { className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit" onSubmit={(e) => { e.preventDefault(); - CreateProject({ - name: name, - description: description, - }); + void handleCreateProject(); }} > ([]); const [users, setUsers] = useState([]); - const [role, setRole] = useState(""); - GetAllUsers({ setUsersProp: setUsers }); + const [usersProj, setUsersProj] = useState([]); - const handleClick = (): boolean => { - const newMember: NewProjMember = { - username: name, - projectname: props.projectName, - role: role, - }; - return AddMember({ memberToAdd: newMember }); + // Gets all users and project members for filtering + GetAllUsers({ setUsersProp: setUsers }); + GetUsersInProject({ + setUsersProp: setUsersProj, + projectName: props.projectName, + }); + /* + * Filters the members from users so that users who are already + * members are not shown + */ + useEffect(() => { + setUsers((prevUsers) => { + const filteredUsers = prevUsers.filter( + (user) => + !usersProj.some((projectUser) => projectUser.Username === user), + ); + return filteredUsers; + }); + }, [usersProj]); + + // Attempts to add all of the selected users to the project + const handleAddClick = async (): Promise => { + if (names.length === 0) + alert("You have to choose at least one user to add"); + for (const name of names) { + const newMember: AddMemberInfo = { + userName: name, + projectName: props.projectName, + }; + await AddMember({ memberToAdd: newMember }); + } + setNames([]); + location.reload(); + }; + + // Updates the names that have been selected + const handleUserClick = (user: string): void => { + setNames((prevNames): string[] => { + if (!prevNames.includes(user)) { + return [...prevNames, user]; + } + return prevNames.filter((name) => name !== user); + }); }; return ( -
-

- User chosen: [{name}] +

+

+ {props.projectName} +

+

+ Choose users to add:

-

- Role chosen: [{role}] -

-

- Project chosen: [{props.projectName}] -

-

Choose role:

-
-
    -
  • { - setRole("member"); - }} - > - {"Member"} -
  • -
  • { - setRole("project_manager"); - }} - > - {"Project manager"} -
  • -
-
-

Choose user:

-
+
    {users.map((user) => (
  • { - setName(user); + handleUserClick(user); }} > {user} @@ -73,13 +88,16 @@ function AddUserToProject(props: { projectName: string }): JSX.Element { ))}
-
+

+ Number of users to be added: {names.length} +

+
diff --git a/frontend/src/Components/AllTimeReportsInProject.tsx b/frontend/src/Components/AllTimeReportsInProject.tsx index 4fa9ad8..0d5916b 100644 --- a/frontend/src/Components/AllTimeReportsInProject.tsx +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -17,7 +17,7 @@ function AllTimeReportsInProject(): JSX.Element { useEffect(() => { const getWeeklyReports = async (): Promise => { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getWeeklyReportsForUser( + const response = await api.getAllWeeklyReportsForUser( projectName ?? "", token, ); diff --git a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx index 09ca6dc..cde9fa7 100644 --- a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx +++ b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx @@ -1,8 +1,9 @@ //Info: This component is used to display all the time reports for a project. It will display the week number, //total time spent, and if the report has been signed or not. The user can click on a report to edit it. import { useEffect, useState } from "react"; -import { NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport } from "../Types/goTypes"; import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; /** * Renders a component that displays all the time reports for a specific project. @@ -11,15 +12,15 @@ import { Link, useParams } from "react-router-dom"; function AllTimeReportsInProject(): JSX.Element { const { username } = useParams(); const { projectName } = useParams(); - const [weeklyReports, setWeeklyReports] = useState([]); + const [weeklyReports, setWeeklyReports] = useState([]); - /* // Call getProjects when the component mounts useEffect(() => { const getWeeklyReports = async (): Promise => { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getWeeklyReportsForUser( + const response = await api.getAllWeeklyReportsForUser( projectName ?? "", token, + username ?? "", ); console.log(response); if (response.success) { @@ -27,41 +28,10 @@ function AllTimeReportsInProject(): JSX.Element { } else { console.error(response.message); } - }; */ - // Mock data - const getWeeklyReports = async (): Promise => { - // Simulate a delay - await Promise.resolve(); - const mockWeeklyReports: NewWeeklyReport[] = [ - { - projectName: "Project 1", - week: 1, - developmentTime: 10, - meetingTime: 2, - adminTime: 1, - ownWorkTime: 3, - studyTime: 4, - testingTime: 5, - }, - { - projectName: "Project 1", - week: 2, - developmentTime: 8, - meetingTime: 2, - adminTime: 1, - ownWorkTime: 3, - studyTime: 4, - testingTime: 5, - }, - // Add more reports as needed - ]; + }; - // Use the mock data instead of the real data - setWeeklyReports(mockWeeklyReports); - }; - useEffect(() => { void getWeeklyReports(); - }, []); + }, [projectName, username]); return ( <> diff --git a/frontend/src/Components/ChangeRoleView.tsx b/frontend/src/Components/ChangeRoleView.tsx index 30dce3c..782ad8d 100644 --- a/frontend/src/Components/ChangeRoleView.tsx +++ b/frontend/src/Components/ChangeRoleView.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import Button from "./Button"; import ChangeRole, { ProjectRoleChange } from "./ChangeRole"; -export default function ChangeRoles(props: { +export default function ChangeRoleView(props: { projectName: string; username: string; }): JSX.Element { diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index 78d7da9..2f73bb6 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -2,8 +2,11 @@ import { APIResponse, api } from "../API/API"; import { StrNameChange } from "../Types/goTypes"; function ChangeUsername(props: { nameChange: StrNameChange }): void { - if (props.nameChange.newName === "") { - alert("You have to select a new name"); + if ( + props.nameChange.newName === "" || + props.nameChange.newName === props.nameChange.prevName + ) { + alert("You have to give a new name\n\nName not changed"); return; } api @@ -13,7 +16,7 @@ function ChangeUsername(props: { nameChange: StrNameChange }): void { alert("Name changed successfully"); location.reload(); } else { - alert("Name not changed"); + alert("Name not changed, name could be taken"); console.error(response.message); } }) diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx index 780f20c..232cb31 100644 --- a/frontend/src/Components/DisplayUnsignedReports.tsx +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -1,12 +1,7 @@ import { useState, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; - -interface UnsignedReports { - projectName: string; - username: string; - week: number; - signed: boolean; -} +import { api } from "../API/API"; +import { WeeklyReport } from "../Types/goTypes"; /** * Renders a component that displays the projects a user is a part of and links to the projects start-page. @@ -14,80 +9,25 @@ interface UnsignedReports { */ function DisplayUserProject(): JSX.Element { const { projectName } = useParams(); - const [unsignedReports, setUnsignedReports] = useState([]); + const [unsignedReports, setUnsignedReports] = useState([]); //const navigate = useNavigate(); - - // const getUnsignedReports = async (): Promise => { - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.getUserProjects(token); - // console.log(response); - // if (response.success) { - // setUnsignedReports(response.data ?? []); - // } else { - // console.error(response.message); - // } - // }; - - // const handleReportClick = async (projectName: string): Promise => { - // const username = localStorage.getItem("username") ?? ""; - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.checkIfProjectManager( - // username, - // projectName, - // token, - // ); - // if (response.success) { - // if (response.data) { - // navigate(`/PMProjectPage/${projectName}`); - // } else { - // navigate(`/project/${projectName}`); - // } - // } else { - // // handle error - // console.error(response.message); - // } - // }; - - const getUnsignedReports = async (): Promise => { - // Simulate a delay - await Promise.resolve(); - - // Use mock data - const reports: UnsignedReports[] = [ - { - projectName: "projecttest", - username: "user1", - week: 2, - signed: false, - }, - { - projectName: "projecttest", - username: "user2", - week: 2, - signed: false, - }, - { - projectName: "projecttest", - username: "user3", - week: 2, - signed: false, - }, - { - projectName: "projecttest", - username: "user4", - week: 2, - signed: false, - }, - ]; - - // Set the state with the mock data - setUnsignedReports(reports); - }; - - // Call getProjects when the component mounts useEffect(() => { + const getUnsignedReports = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getUnsignedReportsInProject( + projectName ?? "", + token, + ); + console.log(response); + if (response.success) { + setUnsignedReports(response.data ?? []); + } else { + console.error(response.message); + } + }; + void getUnsignedReports(); - }, []); + }, [projectName]); // Include 'projectName' in the dependency array return ( <> @@ -95,32 +35,40 @@ function DisplayUserProject(): JSX.Element { All Unsigned Reports In: {projectName}{" "}
- {unsignedReports.map( - (unsignedReport: UnsignedReports, index: number) => ( -

-
-
-

{unsignedReport.username}

- Week: -

{unsignedReport.week}

- Signed: -

NO

-
-
-
- -

- View Report -

- -
+ {unsignedReports.map((unsignedReport: WeeklyReport, index: number) => ( +

+
+
+ UserID: +

{unsignedReport.userId}

+ Week: +

{unsignedReport.week}

+ Total Time: +

+ {unsignedReport.developmentTime + + unsignedReport.meetingTime + + unsignedReport.adminTime + + unsignedReport.ownWorkTime + + unsignedReport.studyTime + + unsignedReport.testingTime} +

+ Signed: +

NO

+
+
+
+ +

+ View Report +

+
-

- ), - )} +
+

+ ))}
); diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 384359e..d56ee42 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport, UpdateWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; @@ -22,6 +22,7 @@ export default function GetWeeklyReport(): JSX.Element { projectName: string; fetchedWeek: string; }>(); + const username = localStorage.getItem("userName") ?? ""; console.log(projectName, fetchedWeek); useEffect(() => { @@ -60,8 +61,9 @@ export default function GetWeeklyReport(): JSX.Element { void fetchWeeklyReport(); }, [projectName, fetchedWeek, token]); - const handleNewWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { + const handleUpdateWeeklyReport = async (): Promise => { + const updateWeeklyReport: UpdateWeeklyReport = { + userName: username, projectName: projectName ?? "", week, developmentTime, @@ -72,7 +74,7 @@ export default function GetWeeklyReport(): JSX.Element { testingTime, }; - await api.submitWeeklyReport(newWeeklyReport, token); + await api.updateWeeklyReport(updateWeeklyReport, token); }; const navigate = useNavigate(); @@ -89,7 +91,8 @@ export default function GetWeeklyReport(): JSX.Element { return; } e.preventDefault(); - void handleNewWeeklyReport(); + void handleUpdateWeeklyReport(); + alert("Changes submitted"); navigate(-1); }} > @@ -128,7 +131,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -152,7 +160,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -176,7 +189,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -200,7 +218,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -224,7 +247,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -248,7 +276,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> diff --git a/frontend/src/Components/MemberInfoModal.tsx b/frontend/src/Components/MemberInfoModal.tsx index 8b32367..dac11f8 100644 --- a/frontend/src/Components/MemberInfoModal.tsx +++ b/frontend/src/Components/MemberInfoModal.tsx @@ -1,8 +1,8 @@ import Button from "./Button"; -import DeleteUser from "./DeleteUser"; import UserProjectListAdmin from "./UserProjectListAdmin"; import { useState } from "react"; import ChangeRoleView from "./ChangeRoleView"; +import RemoveUserFromProj from "./RemoveUserFromProj"; function MemberInfoModal(props: { projectName: string; @@ -20,7 +20,7 @@ function MemberInfoModal(props: { }; return (
@@ -42,13 +42,16 @@ function MemberInfoModal(props: {