diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 7f4a89c..5cbb13f 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,6 +2,7 @@ package database import ( "embed" + "errors" "path/filepath" "ttime/internal/types" @@ -30,6 +31,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) + SignWeeklyReport(reportId int, projectManagerId int) error } // This struct is a wrapper type that holds the database connection @@ -270,7 +272,8 @@ func (d *Db) GetWeeklyReport(username string, projectName string, week int) (typ admin_time, own_work_time, study_time, - testing_time + testing_time, + signed_by FROM weekly_reports WHERE @@ -282,6 +285,34 @@ func (d *Db) GetWeeklyReport(username string, projectName string, week int) (typ return report, err } +// SignWeeklyReport signs a weekly report by updating the signed_by field +// with the provided project manager's ID, but only if the project manager +// is in the same project as the report +func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { + // Retrieve the project ID associated with the report + var reportProjectID int + err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId) + if err != nil { + return err + } + + // Retrieve the project ID associated with the project manager + var managerProjectID int + err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId) + if err != nil { + return err + } + + // Check if the project manager is in the same project as the report + if reportProjectID != managerProjectID { + return errors.New("project manager doesn't have permission to sign the report") + } + + // Update the signed_by field of the specified report + _, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId) + return err +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate() error { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index f791066..09de45b 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,6 +1,7 @@ package database import ( + "fmt" "testing" ) @@ -410,3 +411,128 @@ func TestGetWeeklyReport(t *testing.T) { } // Check other fields similarly } + +func TestSignWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add both regular users as members to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.AddUserToProject("projectManager", "testproject", "project_manager") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Add a weekly report for one of the regular users + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Retrieve the added report + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Print project manager's ID + projectManagerID, err := db.GetUserId("projectManager") + if err != nil { + t.Error("GetUserId failed:", err) + } + fmt.Println("Project Manager's ID:", projectManagerID) + + // Sign the report with the project manager + err = db.SignWeeklyReport(report.ReportId, projectManagerID) + if err != nil { + t.Error("SignWeeklyReport failed:", err) + } + + // Retrieve the report again to check if it's signed + signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Ensure the report is signed by the project manager + if *signedReport.SignedBy != projectManagerID { + t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy) + } +} + +func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add the regular user as a member to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Add a weekly report for the regular user + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Retrieve the added report + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + anotherManagerID, err := db.GetUserId("projectManager") + if err != nil { + t.Error("GetUserId failed:", err) + } + + err = db.SignWeeklyReport(report.ReportId, anotherManagerID) + if err == nil { + t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") + } +} diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 57a1969..c8beb1c 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -16,6 +16,7 @@ type GlobalState interface { GetUserProjects(c *fiber.Ctx) error // To get all projects SubmitWeeklyReport(c *fiber.Ctx) error GetWeeklyReport(c *fiber.Ctx) error + SignReport(c *fiber.Ctx) error // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project // DeleteProject(c *fiber.Ctx) error // To delete a project diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 8754afd..509bd67 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -37,13 +37,16 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { // Handler for retrieving weekly report func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the request + println("GetWeeklyReport") 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.Query("projectName") + println(projectName) week := c.Query("week") + println(week) // Convert week to integer weekInt, err := strconv.Atoi(week) @@ -60,3 +63,31 @@ func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { // Return the retrieved weekly report return c.JSON(report) } + +func (gs *GState) SignReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + managerUsername := claims["name"].(string) + + // Extract the report ID and project manager ID from request parameters + reportID, err := strconv.Atoi(c.Params("reportId")) + if err != nil { + return c.Status(400).SendString("Invalid report ID") + } + + // Call the database function to get the project manager ID + managerID, err := gs.Db.GetUserId(managerUsername) + if err != nil { + return c.Status(500).SendString("Failed to get project manager ID") + } + + // Call the database function to sign the weekly report + err = gs.Db.SignWeeklyReport(reportID, managerID) + if err != nil { + return c.Status(500).SendString("Failed to sign the weekly report: " + err.Error()) + } + + // Return success response + return c.Status(200).SendString("Weekly report signed successfully") +} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index b704cc8..299395a 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -41,4 +41,6 @@ type WeeklyReport struct { StudyTime int `json:"studyTime" db:"study_time"` // Total time spent on testing TestingTime int `json:"testingTime" db:"testing_time"` + // The project manager who signed it + SignedBy *int `json:"signedBy" db:"signed_by"` } diff --git a/backend/main.go b/backend/main.go index 7f0f81e..bc33942 100644 --- a/backend/main.go +++ b/backend/main.go @@ -78,6 +78,7 @@ func main() { server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) + server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 248ad37..ac0f531 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,8 +1,13 @@ -import { NewProject, Project } from "../Types/Project"; -import { NewUser, User } from "../Types/Users"; +import { + NewWeeklyReport, + NewUser, + User, + Project, + NewProject, +} from "../Types/goTypes"; // This type of pattern should be hard to misuse -interface APIResponse { +export interface APIResponse { success: boolean; message?: string; data?: T; @@ -15,13 +20,32 @@ interface API { registerUser(user: NewUser): Promise>; /** Remove a user */ removeUser(username: string, token: string): Promise>; + /** Login */ + login(NewUser: NewUser): Promise>; + /** Renew the token */ + renewToken(token: string): Promise>; /** Create a project */ createProject( project: NewProject, token: string, ): Promise>; - /** Renew the token */ - renewToken(token: string): Promise>; + /** Submit a weekly report */ + submitWeeklyReport( + project: NewWeeklyReport, + token: string, + ): Promise>; + /**Gets a weekly report*/ + getWeeklyReport( + username: string, + projectName: string, + week: string, + token: string, + ): Promise>; + /** Gets all the projects of a user*/ + getUserProjects( + username: string, + token: string, + ): Promise>; } // Export an instance of the API @@ -37,13 +61,19 @@ export const api: API = { }); if (!response.ok) { - return { success: false, message: "Failed to register user" }; + return { + success: false, + message: "Failed to register user: " + response.status, + }; } else { - const data = (await response.json()) as User; - return { success: true, data }; + // const data = (await response.json()) as User; // The API does not currently return the user + return { success: true }; } } catch (e) { - return { success: false, message: "Failed to register user" }; + return { + success: false, + message: "Unknown error while registering user", + }; } }, @@ -117,4 +147,110 @@ export const api: API = { return { success: false, message: "Failed to renew token" }; } }, + + async getUserProjects(token: string): Promise> { + try { + const response = await fetch("/api/getUserProjects", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to get user projects", + }); + } else { + const data = (await response.json()) as Project[]; + return Promise.resolve({ success: true, data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "Failed to get user projects", + }); + } + }, + + async submitWeeklyReport( + weeklyReport: NewWeeklyReport, + token: string, + ): Promise> { + try { + const response = await fetch("/api/submitWeeklyReport", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(weeklyReport), + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to submit weekly report", + }; + } + + const data = (await response.json()) as NewWeeklyReport; + return { success: true, data }; + } catch (e) { + return { + success: false, + message: "Failed to submit weekly report", + }; + } + }, + + async getWeeklyReport( + username: string, + projectName: string, + week: string, + token: string, + ): Promise> { + try { + const response = await fetch("/api/getWeeklyReport", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify({ username, projectName, week }), + }); + + if (!response.ok) { + return { success: false, message: "Failed to get weekly report" }; + } else { + const data = (await response.json()) as NewWeeklyReport; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to get weekly report" }; + } + }, + + async login(NewUser: NewUser): Promise> { + try { + const response = await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(NewUser), + }); + + if (!response.ok) { + return { success: false, message: "Failed to login" }; + } else { + const data = (await response.json()) as { token: string }; // Update the type of 'data' + return { success: true, data: data.token }; + } + } catch (e) { + return Promise.resolve({ success: false, message: "Failed to login" }); + } + }, }; diff --git a/frontend/src/Components/BackButton.tsx b/frontend/src/Components/BackButton.tsx new file mode 100644 index 0000000..7a1ac81 --- /dev/null +++ b/frontend/src/Components/BackButton.tsx @@ -0,0 +1,18 @@ +import { useNavigate } from "react-router-dom"; + +function BackButton(): JSX.Element { + const navigate = useNavigate(); + const goBack = (): void => { + navigate(-1); + }; + return ( + + ); +} + +export default BackButton; diff --git a/frontend/src/Components/BackgroundAnimation.tsx b/frontend/src/Components/BackgroundAnimation.tsx new file mode 100644 index 0000000..5f402c0 --- /dev/null +++ b/frontend/src/Components/BackgroundAnimation.tsx @@ -0,0 +1,24 @@ +import { useEffect } from "react"; + +const BackgroundAnimation = (): JSX.Element => { + useEffect(() => { + const images = [ + "src/assets/1.jpg", + "src/assets/2.jpg", + "src/assets/3.jpg", + "src/assets/4.jpg", + ]; + + // Pre-load images + for (const i of images) { + console.log(i); + } + + // Start animation + document.body.style.animation = "backgroundTransition 30s infinite"; + }, []); + + return <>; +}; + +export default BackgroundAnimation; diff --git a/frontend/src/Components/InputField.tsx b/frontend/src/Components/InputField.tsx new file mode 100644 index 0000000..639b4ca --- /dev/null +++ b/frontend/src/Components/InputField.tsx @@ -0,0 +1,41 @@ +/** + * A customizable input field + * @param props - Settings for the field + * @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; +}): JSX.Element { + return ( +
+ + +
+ ); +} + +export default InputField; diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx new file mode 100644 index 0000000..3658cbf --- /dev/null +++ b/frontend/src/Components/LoginCheck.tsx @@ -0,0 +1,55 @@ +import { NewUser } from "../Types/goTypes"; +import { api, APIResponse } from "../API/API"; +import { Dispatch, SetStateAction } from "react"; + +/* + * Checks if user is in database with api.login and then sets proper authority level + * TODO: change so that it checks for user type (admin, user, pm) somehow instead + **/ +function LoginCheck(props: { + username: string; + password: string; + setAuthority: Dispatch>; +}): number { + const user: NewUser = { + username: props.username, + password: props.password, + }; + api + .login(user) + .then((response: APIResponse) => { + if (response.success) { + if (response.data !== undefined) { + const token = response.data; + //TODO: change so that it checks for user type (admin, user, pm) instead + if (token !== "" && props.username === "admin") { + props.setAuthority((prevAuth) => { + prevAuth = 1; + return prevAuth; + }); + } else if (token !== "" && props.username === "pm") { + props.setAuthority((prevAuth) => { + prevAuth = 2; + return prevAuth; + }); + } else if (token !== "" && props.username === "user") { + props.setAuthority((prevAuth) => { + prevAuth = 3; + return prevAuth; + }); + } + } else { + console.error("Token was undefined"); + } + } else { + console.error("Token could not be fetched"); + } + }) + .catch((error) => { + console.error("An error occurred during login:", error); + }); + + return 0; +} + +export default LoginCheck; diff --git a/frontend/src/Components/LoginField.tsx b/frontend/src/Components/LoginField.tsx new file mode 100644 index 0000000..d7768c0 --- /dev/null +++ b/frontend/src/Components/LoginField.tsx @@ -0,0 +1,55 @@ +import { Dispatch, FormEventHandler, SetStateAction } from "react"; +import Button from "./Button"; +import InputField from "./InputField"; + +/** + * A login field complete with input fields + * and a button for submitting the information + * @param props - Settings + * @returns {JSX.Element} A login component + * @example + * + */ +function Login(props: { + handleSubmit: FormEventHandler; + setUsername: Dispatch>; + setPassword: Dispatch>; + username: string; + password: string; +}): JSX.Element { + return ( +
+ { + props.setUsername(e.target.value); + }} + value={props.username} + /> + { + props.setPassword(e.target.value); + }} + value={props.password} + /> +