diff --git a/Justfile b/Justfile index cb905e4..90fabf6 100644 --- a/Justfile +++ b/Justfile @@ -23,10 +23,13 @@ load-release file: # Tests every part of the project testall: + cd frontend && npm install cd frontend && npm test cd frontend && npm run lint + cd frontend && npm run build cd backend && make test cd backend && make lint + cd backend && make itest # Cleans up everything related to the project clean: remove-podman-containers diff --git a/Makefile b/Makefile index 97db62e..51fb206 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,13 @@ remove-podman-containers: # Tests every part of the project testall: + cd frontend && npm install cd frontend && npm test cd frontend && npm run lint + cd frontend && npm run build cd backend && make test cd backend && make lint + cd backend && make itest # Cleans up everything related to the project clean: remove-podman-containers diff --git a/backend/Makefile b/backend/Makefile index 331f8d5..3443e94 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -8,17 +8,19 @@ GOGET = $(GOCMD) get # SQLite database filename DB_FILE = db.sqlite3 +PROC_NAME = ttime_server + # Directory containing migration SQL scripts MIGRATIONS_DIR = internal/database/migrations SAMPLE_DATA_DIR = internal/database/sample_data # Build target build: - $(GOBUILD) -o bin/server main.go + $(GOBUILD) -o bin/$(PROC_NAME) main.go # Run target run: build - ./bin/server + ./bin/$(PROC_NAME) watch: build watchexec -c -w . -r make run @@ -37,6 +39,16 @@ clean: test: db.sqlite3 $(GOTEST) ./... -count=1 +# Integration test target +.PHONY: itest +itest: + pgrep $(PROC_NAME) && echo "Server already running" && exit 1 || true + make build + ./bin/$(PROC_NAME) >/dev/null 2>&1 & + sleep 1 # Adjust if needed + python ../testing.py + pkill $(PROC_NAME) + # Get dependencies target deps: $(GOGET) -v ./... diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index fd0a083..f871755 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -39,7 +39,8 @@ type Database interface { SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) IsProjectManager(username string, projectname string) (bool, error) - GetTotalTimePerActivity(projectName string) (map[string]int, error) + GetProjectTimes(projectName string) (map[string]int, error) + UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error } // This struct is a wrapper type that holds the database connection @@ -452,6 +453,26 @@ func (d *Db) IsProjectManager(username string, projectname string) (bool, error) return manager, err } +func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error { + query := ` + UPDATE weekly_reports + SET + development_time = ?, + meeting_time = ?, + admin_time = ?, + own_work_time = ?, + study_time = ?, + testing_time = ? + WHERE + user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?) + AND week = ? + ` + + _, err := d.Exec(query, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime, userName, projectName, week) + return err +} + // MigrateSampleData applies sample data to the database. func (d *Db) MigrateSampleData() error { // Insert sample data @@ -491,11 +512,11 @@ func (d *Db) MigrateSampleData() error { return nil } -func (d *Db) GetTotalTimePerActivity(projectName string) (map[string]int, error) { - +// GetProjectTimes retrieves a map with times per "Activity" for a given project +func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { query := ` SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time - FROM weekly_reports + FROM weekly_reports JOIN projects ON weekly_reports.project_id = projects.id WHERE projects.name = ? ` diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 139fba9..b68d446 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -729,27 +729,90 @@ func TestIsProjectManager(t *testing.T) { } } -func TestGetTotalTimePerActivity(t *testing.T) { - // Initialize your test database connection +func TestGetProjectTimes(t *testing.T) { + // Initialize db, err := setupState() if err != nil { t.Error("setupState failed:", err) + return } - // Run the query to get total time per activity - totalTime, err := db.GetTotalTimePerActivity("projecttest") + // Create a user + user := "TeaUser" + password := "Vanilla" + err = db.AddUser(user, password) + if err != nil { + t.Error("AddUser failed:", err) + return + } + + // Create a project + projectName := "ProjectVanilla" + projectDescription := "When tea tastes its best" + err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here + if err != nil { + t.Error("AddProject failed:", err) + return + } + + // Tests the func in db.go + totalTime, err := db.GetProjectTimes(projectName) if err != nil { t.Error("GetTotalTimePerActivity failed:", err) + return } // Check if the totalTime map is not nil if totalTime == nil { t.Error("Expected non-nil totalTime map, got nil") + return } - // ska lägga till fler assertions -} + // Define the expected valeus + expectedTotalTime := map[string]int{ + "development": 0, + "meeting": 0, + "admin": 0, + "own_work": 0, + "study": 0, + "testing": 0, + } + // Compare the expectedTotalTime with the totalTime retrieved from the database + for activity, expectedTime := range expectedTotalTime { + if totalTime[activity] != expectedTime { + t.Errorf("Expected %s time to be %d, got %d", activity, expectedTime, totalTime[activity]) + } + } + + // Insert some data into the database for different activities + err = db.AddWeeklyReport(projectName, user, 1, 1, 3, 2, 1, 4, 5) + if err != nil { + t.Error("Failed to insert data into the database:", err) + return + } + + newTotalTime, err := db.GetProjectTimes(projectName) + if err != nil { + t.Error("GetTotalTimePerActivity failed:", err) + return + } + + newExpectedTotalTime := map[string]int{ + "development": 1, + "meeting": 3, + "admin": 2, + "own_work": 1, + "study": 4, + "testing": 5, + } + + for activity, newExpectedTime := range newExpectedTotalTime { + if newTotalTime[activity] != newExpectedTime { + t.Errorf("Expected %s time to be %d, got %d", activity, newExpectedTime, newTotalTime[activity]) + } + } +} func TestEnsureManagerOfCreatedProject(t *testing.T) { db, err := setupState() if err != nil { @@ -783,3 +846,51 @@ func TestEnsureManagerOfCreatedProject(t *testing.T) { t.Error("Expected testuser to be a project manager, but it's not.") } } + +// TestUpdateWeeklyReport tests the UpdateWeeklyReport function of the database +func TestUpdateWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a project + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add a weekly report + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Update the weekly report + err = db.UpdateWeeklyReport("testproject", "testuser", 1, 2, 2, 2, 2, 2, 2) + if err != nil { + t.Error("UpdateWeeklyReport failed:", err) + } + + // Retrieve the updated report + updatedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Check if the report was updated correctly + if updatedReport.DevelopmentTime != 2 || + updatedReport.MeetingTime != 2 || + updatedReport.AdminTime != 2 || + updatedReport.OwnWorkTime != 2 || + updatedReport.StudyTime != 2 || + updatedReport.TestingTime != 2 { + t.Error("UpdateWeeklyReport failed: report not updated correctly") + } +} diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index b88bdcd..49c8c09 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -28,6 +28,7 @@ type GlobalState interface { ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project ChangeUserName(c *fiber.Ctx) error // WIP GetAllUsersProject(c *fiber.Ctx) error // WIP + UpdateWeeklyReport(c *fiber.Ctx) error } // "Constructor" diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 47d076d..0e72ead 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -32,7 +32,7 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { } if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { - log.Info("Error adding weekly report") + log.Info("Error adding weekly report to db:", err) return c.Status(500).SendString(err.Error()) } @@ -141,3 +141,37 @@ func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { // Return the list of reports as JSON return c.JSON(reports) } + +func (gs *GState) UpdateWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Parse the request body into an UpdateWeeklyReport struct + var updateReport types.UpdateWeeklyReport + if err := c.BodyParser(&updateReport); err != nil { + log.Info("Error parsing weekly report") + return c.Status(400).SendString(err.Error()) + } + + // Make sure all the fields of the report are valid + if updateReport.Week < 1 || updateReport.Week > 52 { + log.Info("Invalid week number") + return c.Status(400).SendString("Invalid week number") + } + + if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 { + log.Info("Invalid time report") + return c.Status(400).SendString("Invalid time report") + } + + // Update the weekly report in the database + if err := gs.Db.UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { + log.Info("Error updating weekly report in db:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Weekly report updated") + return c.Status(200).SendString("Weekly report updated") +} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 116ce90..39788ae 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -207,8 +207,8 @@ func (gs *GState) GetAllUsersProject(c *fiber.Ctx) error { // @Accept json // @Produce plain // @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully prometed user" -// @Failure 400 {string} string "bad request" +// @Success 200 {json} json "Successfully promoted user" +// @Failure 400 {string} string "Bad request" // @Failure 401 {string} string "Unauthorized" // @Failure 500 {string} string "Internal server error" // @Router /promoteToAdmin [post] @@ -234,33 +234,33 @@ func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } -// Changes a users name in the database +// ChangeUserName changes a user's username in the database func (gs *GState) ChangeUserName(c *fiber.Ctx) error { - - //check token and get username of current user + // Check token and get username of current user user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) - projectManagerUsername := claims["name"].(string) - log.Info(projectManagerUsername) + adminUsername := claims["name"].(string) + log.Info(adminUsername) + // Extract the necessary parameters from the request - data := new(types.NameChange) + data := new(types.StrNameChange) if err := c.BodyParser(data); err != nil { - log.Info("error parsing username, project or role") + log.Info("Error parsing username") return c.Status(400).SendString(err.Error()) } - // dubble diping and checcking if current user is - - if ismanager, err := gs.Db.IsProjectManager(projectManagerUsername, c.Params(data.Name)); err != nil { - log.Warn("Error checking if projectmanager:", err) + // Check if the current user is an admin + isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + if err != nil { + log.Warn("Error checking if admin:", err) return c.Status(500).SendString(err.Error()) - } else if !ismanager { - log.Warn("tried changing name when not projectmanager:", err) - return c.Status(401).SendString("you can not change name when not projectmanager") + } else if !isAdmin { + log.Warn("Tried changing name when not admin") + return c.Status(401).SendString("You cannot change name unless you are an admin") } - // Change the user's name within the project in the database - if err := gs.Db.ChangeUserName(projectManagerUsername, data.Name); err != nil { + // Change the user's name in the database + if err := gs.Db.ChangeUserName(data.PrevName, data.NewName); err != nil { return c.Status(500).SendString(err.Error()) } diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 8d22b6a..234781b 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -65,3 +65,24 @@ type WeeklyReport struct { // The project manager who signed it SignedBy *int `json:"signedBy" db:"signed_by"` } + +type UpdateWeeklyReport struct { + // The name of the project, as it appears in the database + ProjectName string `json:"projectName"` + // The name of the user + UserName string `json:"userName"` + // The week number + Week int `json:"week"` + // Total time spent on development + DevelopmentTime int `json:"developmentTime"` + // Total time spent in meetings + MeetingTime int `json:"meetingTime"` + // Total time spent on administrative tasks + AdminTime int `json:"adminTime"` + // Total time spent on personal projects + OwnWorkTime int `json:"ownWorkTime"` + // Total time spent on studying + StudyTime int `json:"studyTime"` + // Total time spent on testing + TestingTime int `json:"testingTime"` +} diff --git a/backend/main.go b/backend/main.go index ff6b94e..b42bcab 100644 --- a/backend/main.go +++ b/backend/main.go @@ -101,6 +101,7 @@ func main() { server.Get("/api/checkIfProjectManager/:projectName", gs.IsProjectManagerHandler) server.Post("/api/ProjectRoleChange", gs.ProjectRoleChange) server.Get("/api/getUsersProject/:projectName", gs.ListAllUsersProject) + server.Put("/api/updateWeeklyReport", gs.UpdateWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/container/Containerfile b/container/Containerfile index ecd2f84..f9cb39d 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -13,7 +13,6 @@ FROM docker.io/golang:alpine as go RUN apk add gcompat RUN apk add gcc RUN apk add musl-dev -RUN apk add make RUN apk add sqlite WORKDIR /build ADD backend/go.mod backend/go.sum ./ @@ -24,9 +23,7 @@ RUN go mod download # Add the source code ADD backend . -RUN make migrate - -# RUN go build -o server +RUN go build -o server RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go # Strip the binary for a smaller image @@ -37,6 +34,7 @@ FROM docker.io/alpine:latest as runner RUN adduser -D nonroot RUN addgroup nonroot nonroot WORKDIR /app +RUN chown nonroot:nonroot /app # Copy the frontend SPA build into public COPY --from=client /build/dist static @@ -44,9 +42,6 @@ COPY --from=client /build/dist static # Copy the server binary COPY --from=go /build/server server -# Copy the database -COPY --from=go /build/db.sqlite3 db.sqlite3 - # Expose port 8080 EXPOSE 8080 diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 0859748..5c49a8d 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -4,6 +4,7 @@ import { User, Project, NewProject, + UserProjectMember, WeeklyReport, } from "../Types/goTypes"; @@ -127,6 +128,11 @@ interface API { * @returns {Promise<APIResponse<string[]>>} A promise resolving to an API response containing the list of users. */ getAllUsers(token: string): Promise<APIResponse<string[]>>; + /** Gets all users in a project from name*/ + getAllUsersProject( + projectName: string, + token: string, + ): Promise<APIResponse<UserProjectMember[]>>; } /** An instance of the API */ @@ -448,4 +454,34 @@ export const api: API = { }); } }, + //Gets all users in a project + async getAllUsersProject( + projectName: string, + token: string, + ): Promise<APIResponse<UserProjectMember[]>> { + try { + const response = await fetch(`/api/getUsersProject/${projectName}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to get users", + }); + } else { + const data = (await response.json()) as UserProjectMember[]; + return Promise.resolve({ success: true, data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "API is not ok", + }); + } + }, }; diff --git a/frontend/src/Components/AdminUserList.tsx b/frontend/src/Components/AdminUserList.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/Components/AllTimeReportsInProject.tsx b/frontend/src/Components/AllTimeReportsInProject.tsx index 14d3af9..4fa9ad8 100644 --- a/frontend/src/Components/AllTimeReportsInProject.tsx +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -13,24 +13,24 @@ function AllTimeReportsInProject(): JSX.Element { const { projectName } = useParams(); const [weeklyReports, setWeeklyReports] = useState<WeeklyReport[]>([]); - const getWeeklyReports = async (): Promise<void> => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getWeeklyReportsForUser( - projectName ?? "", - token, - ); - console.log(response); - if (response.success) { - setWeeklyReports(response.data ?? []); - } else { - console.error(response.message); - } - }; - // Call getProjects when the component mounts useEffect(() => { + const getWeeklyReports = async (): Promise<void> => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getWeeklyReportsForUser( + projectName ?? "", + token, + ); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } else { + console.error(response.message); + } + }; + void getWeeklyReports(); - }, []); + }, [projectName]); return ( <> diff --git a/frontend/src/Components/GetProjects.tsx b/frontend/src/Components/GetProjects.tsx new file mode 100644 index 0000000..d6ab1f7 --- /dev/null +++ b/frontend/src/Components/GetProjects.tsx @@ -0,0 +1,37 @@ +import { Dispatch, useEffect } from "react"; +import { Project } from "../Types/goTypes"; +import { api } from "../API/API"; + +/** + * Gets all projects that user is a member of + * @param props - A setStateAction for the array you want to put projects in + * @returns {void} Nothing + * @example + * const [projects, setProjects] = useState<Project[]>([]); + * GetAllUsers({ setProjectsProp: setProjects }); + */ +function GetProjects(props: { + setProjectsProp: Dispatch<React.SetStateAction<Project[]>>; +}): void { + const setProjects: Dispatch<React.SetStateAction<Project[]>> = + props.setProjectsProp; + useEffect(() => { + const fetchUsers = async (): Promise<void> => { + try { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getUserProjects(token); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error("Failed to fetch projects:", response.message); + } + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + void fetchUsers(); + }, [setProjects]); +} + +export default GetProjects; diff --git a/frontend/src/Components/GetUsersInProject.tsx b/frontend/src/Components/GetUsersInProject.tsx new file mode 100644 index 0000000..acdd965 --- /dev/null +++ b/frontend/src/Components/GetUsersInProject.tsx @@ -0,0 +1,37 @@ +import { Dispatch, useEffect } from "react"; +import { UserProjectMember } from "../Types/goTypes"; +import { api } from "../API/API"; + +/** + * Gets all projects that user is a member of + * @param props - A setStateAction for the array you want to put projects in + * @returns {void} Nothing + * @example + * const [projects, setProjects] = useState<Project[]>([]); + * GetAllUsers({ setProjectsProp: setProjects }); + */ +function GetUsersInProject(props: { + projectName: string; + setUsersProp: Dispatch<React.SetStateAction<UserProjectMember[]>>; +}): void { + const setUsers: Dispatch<React.SetStateAction<UserProjectMember[]>> = + props.setUsersProp; + useEffect(() => { + const fetchUsers = async (): Promise<void> => { + try { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getAllUsersProject(props.projectName, token); + if (response.success) { + setUsers(response.data ?? []); + } else { + console.error("Failed to fetch projects:", response.message); + } + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + void fetchUsers(); + }, [props.projectName, setUsers]); +} + +export default GetUsersInProject; diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx new file mode 100644 index 0000000..b153e9c --- /dev/null +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import Button from "./Button"; +import { UserProjectMember } from "../Types/goTypes"; +import GetUsersInProject from "./GetUsersInProject"; + +function ProjectInfoModal(props: { + isVisible: boolean; + projectname: string; + onClose: () => void; + onClick: (username: string) => void; +}): JSX.Element { + const [users, setUsers] = useState<UserProjectMember[]>([]); + GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); + if (!props.isVisible) return <></>; + + return ( + <div + className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm + flex justify-center items-center" + > + <div className="border-4 border-black bg-white p-2 rounded-2xl text-center h-[41vh] w-[40vw] flex flex-col"> + <div className="pl-20 pr-20"> + <h1 className="font-bold text-[32px] mb-[20px]">Project members:</h1> + <div className="border-2 border-black p-2 rounded-lg text-center overflow-scroll h-[26vh]"> + <ul className="text-left font-medium space-y-2"> + <div></div> + {users.map((user) => ( + <li + className="items-start p-1 border-2 border-black rounded-lg bg-orange-200 hover:bg-orange-600 hover:text-slate-100 hover:cursor-pointer" + key={user.Username} + onClick={() => { + props.onClick(user.Username); + }} + > + <span> + Name: {user.Username} + <div></div> + Role: {user.UserRole} + </span> + </li> + ))} + </ul> + </div> + </div> + <div className="space-x-16"> + <Button + text={"Delete"} + onClick={function (): void { + //DELETE PROJECT + }} + type="button" + /> + <Button + text={"Close"} + onClick={function (): void { + props.onClose(); + }} + type="button" + /> + </div> + </div> + </div> + ); +} + +export default ProjectInfoModal; diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx new file mode 100644 index 0000000..4ebdaf8 --- /dev/null +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { NewProject } from "../Types/goTypes"; +import ProjectInfoModal from "./ProjectInfoModal"; +import UserInfoModal from "./UserInfoModal"; +import DeleteUser from "./DeleteUser"; + +/** + * A list of projects for admin manage projects page, that sets an onClick + * function for eact project <li> element, which displays a modul with + * user info. + * @param props - An array of projects to display + * @returns {JSX.Element} The project list + * @example + * const projects: NewProject[] = [{ name: "Project", description: "New" }]; + * return <ProjectListAdmin projects={projects} /> + */ + +export function ProjectListAdmin(props: { + projects: NewProject[]; +}): JSX.Element { + const [projectModalVisible, setProjectModalVisible] = useState(false); + const [projectname, setProjectname] = useState(""); + const [userModalVisible, setUserModalVisible] = useState(false); + const [username, setUsername] = useState(""); + + const handleClickUser = (username: string): void => { + setUsername(username); + setUserModalVisible(true); + }; + + const handleClickProject = (username: string): void => { + setProjectname(username); + setProjectModalVisible(true); + }; + + const handleCloseProject = (): void => { + setProjectname(""); + setProjectModalVisible(false); + }; + + const handleCloseUser = (): void => { + setProjectname(""); + setUserModalVisible(false); + }; + + return ( + <> + <ProjectInfoModal + onClose={handleCloseProject} + onClick={handleClickUser} + isVisible={projectModalVisible} + projectname={projectname} + /> + <UserInfoModal + manageMember={true} + onClose={handleCloseUser} + //TODO: CHANGE TO REMOVE USER FROM PROJECT + onDelete={() => DeleteUser} + isVisible={userModalVisible} + username={username} + /> + <div> + <ul className="font-bold underline text-[30px] cursor-pointer padding"> + {props.projects.map((project) => ( + <li + className="pt-5" + key={project.name} + onClick={() => { + handleClickProject(project.name); + }} + > + {project.name} + </li> + ))} + </ul> + </div> + </> + ); +} diff --git a/frontend/src/Components/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index a22ef01..2334388 100644 --- a/frontend/src/Components/UserInfoModal.tsx +++ b/frontend/src/Components/UserInfoModal.tsx @@ -5,23 +5,38 @@ import UserProjectListAdmin from "./UserProjectListAdmin"; function UserInfoModal(props: { isVisible: boolean; + manageMember: boolean; username: string; onClose: () => void; + onDelete: (username: string) => void; }): JSX.Element { if (!props.isVisible) return <></>; - + const ManageUserOrMember = (check: boolean): JSX.Element => { + if (check) { + return ( + <Link to="/AdminChangeRole"> + <p className="mb-[20px] hover:font-bold hover:cursor-pointer underline"> + (Change Role) + </p> + </Link> + ); + } + return ( + <Link to="/AdminChangeUserName"> + <p className="mb-[20px] hover:font-bold hover:cursor-pointer underline"> + (Change Username) + </p> + </Link> + ); + }; return ( <div className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm flex justify-center items-center" > - <div className="border-4 border-black bg-white p-2 rounded-lg text-center"> + <div className="border-4 border-black bg-white p-2 rounded-lg text-center flex flex-col"> <p className="font-bold text-[30px]">{props.username}</p> - <Link to="/AdminChangeUserName"> - <p className="mb-[20px] hover:font-bold hover:cursor-pointer underline"> - (Change Username) - </p> - </Link> + {ManageUserOrMember(props.manageMember)} <div> <h2 className="font-bold text-[22px] mb-[20px]"> Member of these projects: diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index 76cae9f..c08b05c 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 DeleteUser from "./DeleteUser"; /** * A list of users for admin manage users page, that sets an onClick @@ -29,7 +30,9 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element { return ( <> <UserInfoModal + manageMember={false} onClose={handleClose} + onDelete={() => DeleteUser} isVisible={modalVisible} username={username} /> diff --git a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx index 177f55b..7ea45df 100644 --- a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx @@ -2,9 +2,22 @@ import { Link } from "react-router-dom"; import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; +import { ProjectListAdmin } from "../../Components/ProjectListAdmin"; +import { Project } from "../../Types/goTypes"; +import GetProjects from "../../Components/GetProjects"; +import { useState } from "react"; function AdminManageProjects(): JSX.Element { - const content = <></>; + const [projects, setProjects] = useState<Project[]>([]); + GetProjects({ setProjectsProp: setProjects }); + const content = ( + <> + <h1 className="font-bold text-[30px] mb-[20px]">Manage Projects</h1> + <div className="border-4 border-black bg-white flex flex-col items-center h-[65vh] w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> + <ProjectListAdmin projects={projects} /> + </div> + </> + ); const buttons = ( <> diff --git a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx index 96167cb..712df86 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx @@ -1,3 +1,4 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; @@ -13,13 +14,7 @@ function AdminProjectAddMember(): JSX.Element { }} type="button" /> - <Button - text="Back" - onClick={(): void => { - return; - }} - type="button" - /> + <BackButton /> </> ); diff --git a/frontend/src/Pages/AdminPages/AdminProjectChangeUserRole.tsx b/frontend/src/Pages/AdminPages/AdminProjectChangeUserRole.tsx index dd355e8..ad66b44 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectChangeUserRole.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectChangeUserRole.tsx @@ -1,3 +1,4 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; @@ -13,13 +14,7 @@ function AdminProjectChangeUserRole(): JSX.Element { }} type="button" /> - <Button - text="Back" - onClick={(): void => { - return; - }} - type="button" - /> + <BackButton /> </> ); diff --git a/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx b/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx index c89e4c4..7d06a46 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx @@ -1,3 +1,4 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; @@ -13,13 +14,7 @@ function AdminProjectManageMembers(): JSX.Element { }} type="button" /> - <Button - text="Back" - onClick={(): void => { - return; - }} - type="button" - /> + <BackButton /> </> ); diff --git a/frontend/src/Pages/AdminPages/AdminProjectPage.tsx b/frontend/src/Pages/AdminPages/AdminProjectPage.tsx index a1266ad..0faae7e 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectPage.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectPage.tsx @@ -1,3 +1,4 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; @@ -13,13 +14,7 @@ function AdminProjectPage(): JSX.Element { }} type="button" /> - <Button - text="Back" - onClick={(): void => { - return; - }} - type="button" - /> + <BackButton /> </> ); diff --git a/frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx b/frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx index dbf3428..0110d65 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectStatistics.tsx @@ -1,18 +1,12 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; -import Button from "../../Components/Button"; function AdminProjectStatistics(): JSX.Element { const content = <></>; const buttons = ( <> - <Button - text="Back" - onClick={(): void => { - return; - }} - type="button" - /> + <BackButton /> </> ); diff --git a/frontend/src/Pages/AdminPages/AdminProjectViewMemberInfo.tsx b/frontend/src/Pages/AdminPages/AdminProjectViewMemberInfo.tsx index 1c9f28c..98ed5b8 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectViewMemberInfo.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectViewMemberInfo.tsx @@ -1,3 +1,4 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; @@ -13,13 +14,7 @@ function AdminProjectViewMemberInfo(): JSX.Element { }} type="button" /> - <Button - text="Back" - onClick={(): void => { - return; - }} - type="button" - /> + <BackButton /> </> ); diff --git a/frontend/src/Types/goTypes.ts b/frontend/src/Types/goTypes.ts index f43ede7..6433b13 100644 --- a/frontend/src/Types/goTypes.ts +++ b/frontend/src/Types/goTypes.ts @@ -184,6 +184,11 @@ export interface PublicUser { userId: string; username: string; } + +export interface UserProjectMember { + Username: string; + UserRole: string; +} /** * wrapper type for token */ diff --git a/testing.py b/testing.py index 384d7ce..3515720 100644 --- a/testing.py +++ b/testing.py @@ -41,6 +41,8 @@ getWeeklyReportsUserPath = base_url + "/api/getWeeklyReportsUser" checkIfProjectManagerPath = base_url + "/api/checkIfProjectManager" ProjectRoleChangePath = base_url + "/api/ProjectRoleChange" getUsersProjectPath = base_url + "/api/getUsersProject" +getChangeUserNamePath = base_url + "/api/changeUserName" +getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport" #ta bort auth i handlern för att få testet att gå igenom def test_ProjectRoleChange(): @@ -274,7 +276,7 @@ def test_sign_report(): submitReportPath, json={ "projectName": projectName, - "week": 1, + "week": 2, "developmentTime": 10, "meetingTime": 5, "adminTime": 5, @@ -367,6 +369,98 @@ def test_ensure_manager_of_created_project(): assert response.json()["isProjectManager"] == True, "User is not project manager" gprint("test_ensure_admin_of_created_project successful") +def test_change_user_name(): + # Register a new user + new_user = randomString() + register(new_user, "password") + + # Log in as the new user + token = login(new_user, "password").json()["token"] + + # Register a new admin + admin_username = randomString() + admin_password = "admin_password" + dprint( + "Registering with username: ", admin_username, " and password: ", admin_password + ) + response = requests.post( + registerPath, json={"username": admin_username, "password": admin_password} + ) + admin_token = login(admin_username, admin_password).json()["token"] + + # Promote to admin + response = requests.post( + promoteToAdminPath, + json={"username": admin_username}, + headers={"Authorization": "Bearer " + admin_token}, + ) + + # Change the user's name + response = requests.put( + getChangeUserNamePath, + json={"prevName": new_user, "newName": randomString()}, + headers={"Authorization": "Bearer " + admin_token}, + ) + + # Check if the change was successful + assert response.status_code == 200, "Change user name failed" + gprint("test_change_user_name successful") + +def test_list_all_users_project(): + # Log in as a user who is a member of the project + admin_username = randomString() + admin_password = "admin_password2" + dprint( + "Registering with username: ", admin_username, " and password: ", admin_password + ) + response = requests.post( + registerPath, json={"username": admin_username, "password": admin_password} + ) + dprint(response.text) + + # Log in as the admin + admin_token = login(admin_username, admin_password).json()["token"] + response = requests.post( + promoteToAdminPath, + json={"username": admin_username}, + headers={"Authorization": "Bearer " + admin_token}, + ) + + # Make a request to list all users associated with the project + response = requests.get( + getUsersProjectPath + "/" + projectName, + headers={"Authorization": "Bearer " + admin_token}, + ) + assert response.status_code == 200, "List all users project failed" + gprint("test_list_all_users_project sucessful") + +def test_update_weekly_report(): + # Log in as the user + token = login(username, "always_same").json()["token"] + + # Prepare the JSON data for updating the weekly report + update_data = { + "projectName": projectName, + "userName": username, + "week": 1, + "developmentTime": 8, + "meetingTime": 6, + "adminTime": 4, + "ownWorkTime": 11, + "studyTime": 8, + "testingTime": 18, + } + + # Send a request to update the weekly report + response = requests.put( + getUpdateWeeklyReportPath, + json=update_data, + headers={"Authorization": "Bearer " + token}, + ) + + # Check if the update was successful + assert response.status_code == 200, "Update weekly report failed" + gprint("test_update_weekly_report successful") if __name__ == "__main__": test_get_user_projects() @@ -381,5 +475,7 @@ if __name__ == "__main__": test_get_weekly_reports_user() test_check_if_project_manager() test_ProjectRoleChange() - #test_list_all_users_project() test_ensure_manager_of_created_project() + test_list_all_users_project() + test_change_user_name() + test_update_weekly_report() \ No newline at end of file