diff --git a/.gitignore b/.gitignore index 05f913b..3b1c6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ backend/*.png backend/*.jpg backend/*.svg +/go.work.sum +/package-lock.json + # Test binary, built with `go test -c` *.test 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 7410b16..30a672a 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -40,7 +40,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 @@ -498,6 +499,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 @@ -537,11 +558,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 fb394e0..275836b 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -770,27 +770,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 { @@ -824,3 +887,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 6b197bb..9190ea4 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -29,6 +29,7 @@ type GlobalState interface { ChangeUserName(c *fiber.Ctx) error // WIP GetAllUsersProject(c *fiber.Ctx) error // WIP GetUnsignedReports(c *fiber.Ctx) error // + UpdateWeeklyReport(c *fiber.Ctx) error } // "Constructor" diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go index 603f4cd..f64d013 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -233,3 +233,57 @@ func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { // Return the result as JSON return c.JSON(fiber.Map{"isProjectManager": isManager}) } + +func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Get project + projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } + + // Get all users in the project and roles + userProjects, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users in project:", err) + return c.Status(500).SendString(err.Error()) + } + + // If the user is member + isMember := false + for _, userProject := range userProjects { + if userProject.Username == username { + isMember = true + break + } + } + + // If the user is admin + if !isMember { + isAdmin, err := gs.Db.IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project member nor a site admin:", username) + return c.Status(403).SendString("User is neither a project member nor a site admin") + } + } + + // Get project times + projectTimes, err := gs.Db.GetProjectTimes(projectName) + if err != nil { + log.Info("Error getting project times:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return project times as JSON + log.Info("Returning project times for project:", projectName) + return c.JSON(projectTimes) +} \ No newline at end of file diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 5ac49b0..52e1564 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -177,3 +177,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 dc5b86b..5d0a9af 100644 --- a/backend/main.go +++ b/backend/main.go @@ -33,6 +33,12 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ +/** +Main function for starting the server and initializing configurations. +Reads configuration from file, pretty prints it, connects to the database, +migrates it, and sets up routes for the server. +*/ + func main() { conf, err := config.ReadConfigFromFile("config.toml") if err != nil { @@ -102,6 +108,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..c6cef66 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"; @@ -84,7 +85,7 @@ interface API { submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise>; + ): Promise>; /** Gets a weekly report for a specific user, project and week * @param {string} projectName The name of the project. @@ -127,6 +128,11 @@ interface API { * @returns {Promise>} A promise resolving to an API response containing the list of users. */ getAllUsers(token: string): Promise>; + /** Gets all users in a project from name*/ + getAllUsersProject( + projectName: string, + token: string, + ): Promise>; } /** An instance of the API */ @@ -288,7 +294,7 @@ export const api: API = { async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/submitWeeklyReport", { method: "POST", @@ -306,8 +312,8 @@ export const api: API = { }; } - const data = (await response.json()) as NewWeeklyReport; - return { success: true, data }; + const data = await response.text(); + return { success: true, message: data }; } catch (e) { return { success: false, @@ -448,4 +454,34 @@ export const api: API = { }); } }, + //Gets all users in a project + async getAllUsersProject( + projectName: string, + token: string, + ): Promise> { + 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([]); - const getWeeklyReports = async (): Promise => { - 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 => { + 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/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 76c2b94..be96329 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -18,13 +18,11 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const username = localStorage.getItem("username") ?? ""; const { projectName } = useParams(); const { fetchedWeek } = useParams(); const fetchWeeklyReport = async (): Promise => { const response = await api.getWeeklyReport( - username, projectName ?? "", fetchedWeek?.toString() ?? "0", token, 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([]); + * GetAllUsers({ setProjectsProp: setProjects }); + */ +function GetProjects(props: { + setProjectsProp: Dispatch>; +}): void { + const setProjects: Dispatch> = + props.setProjectsProp; + useEffect(() => { + const fetchUsers = async (): Promise => { + 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([]); + * GetAllUsers({ setProjectsProp: setProjects }); + */ +function GetUsersInProject(props: { + projectName: string; + setUsersProp: Dispatch>; +}): void { + const setUsers: Dispatch> = + props.setUsersProp; + useEffect(() => { + const fetchUsers = async (): Promise => { + 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/NewWeeklyReport.tsx b/frontend/src/Components/NewWeeklyReport.tsx index 292ddf5..a128b8d 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -60,8 +60,7 @@ export default function NewWeeklyReport(): JSX.Element { type="week" placeholder="Week" onChange={(e) => { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); + setWeek(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; 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([]); + GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); + if (!props.isVisible) return <>; + + return ( +
+
+
+

Project members:

+
+
    +
    + {users.map((user) => ( +
  • { + props.onClick(user.Username); + }} + > + + Name: {user.Username} +
    + Role: {user.UserRole} +
    +
  • + ))} +
+
+
+
+
+
+
+ ); +} + +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
  • 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 + */ + +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 ( + <> + + DeleteUser} + isVisible={userModalVisible} + username={username} + /> +
    +
      + {props.projects.map((project) => ( +
    • { + handleClickProject(project.name); + }} + > + {project.name} +
    • + ))} +
    +
    + + ); +} 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 ( + +

    + (Change Role) +

    + + ); + } + return ( + +

    + (Change Username) +

    + + ); + }; return (
    -
    +

    {props.username}

    - -

    - (Change Username) -

    - + {ManageUserOrMember(props.manageMember)}

    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 ( <> 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([]); + GetProjects({ setProjectsProp: setProjects }); + const content = ( + <> +

    Manage Projects

    +
    + +
    + + ); 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" /> -