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..f4c0f6e 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -40,7 +40,9 @@ 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 + RemoveProject(projectname string) error } // This struct is a wrapper type that holds the database connection @@ -498,6 +500,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 +559,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 = ? ` @@ -574,3 +596,8 @@ func (d *Db) GetTotalTimePerActivity(projectName string) (map[string]int, error) return totalTime, nil } + +func (d *Db) RemoveProject(projectname string) error { + _, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index cff70a0..fe3e6cd 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -481,6 +481,11 @@ func TestGetUnsignedWeeklyReports(t *testing.T) { t.Error("AddUser failed:", err) } + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + err = db.AddProject("testproject", "description", "testuser") if err != nil { t.Error("AddProject failed:", err) @@ -491,6 +496,11 @@ func TestGetUnsignedWeeklyReports(t *testing.T) { t.Error("AddWeeklyReport failed:", err) } + err = db.AddWeeklyReport("testproject", "testuser1", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + reports, err := db.GetUnsignedWeeklyReports("testproject") if err != nil { t.Error("GetUnsignedWeeklyReports failed:", err) @@ -760,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 { @@ -814,3 +887,81 @@ 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") + } +} + +func TestRemoveProject(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Promote user to Admin + err = db.PromoteToAdmin("demouser") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } + + // Remove project + err = db.RemoveProject("projecttest") + if err != nil { + t.Error("RemoveProject failed:", err) + } + + // Check if the project was removed + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if len(projects) != 0 { + t.Error("RemoveProject failed: expected 0, got", len(projects)) + } + +} + \ No newline at end of file diff --git a/backend/internal/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index 092fbb0..ab74f1a 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -7,6 +7,8 @@ VALUES ("user", "123"); INSERT OR IGNORE INTO users(username, password) VALUES ("user2", "123"); +INSERT OR IGNORE INTO site_admin VALUES (1); + INSERT OR IGNORE INTO projects(name,description,owner_user_id) VALUES ("projecttest","test project", 1); diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 6b197bb..0db4340 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -29,6 +29,8 @@ 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 + RemoveProject(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..d63d7eb 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -233,3 +233,83 @@ 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) +} + +func (gs *GState) RemoveProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Check if the user is a site admin + 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 not a site admin:", username) + return c.Status(403).SendString("User is not a site admin") + } + + projectName := c.Params("projectName") + + if err := gs.Db.RemoveProject(projectName); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 534050a..52e1564 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -122,7 +122,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { projectManagerUsername := claims["name"].(string) // Extract project name and week from query parameters - projectName := c.Query("projectName") + projectName := c.Params("projectName") log.Info("Getting unsigned reports for") @@ -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..bc4ae2d 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -59,9 +59,9 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error { // Read username from Locals auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) - if username != auth_username { - log.Info("User tried to delete another user") - return c.Status(403).SendString("You can only delete yourself") + if username == auth_username { + log.Info("User tried to delete itself") + return c.Status(403).SendString("You can't delete yourself") } if err := gs.Db.RemoveUser(username); err != nil { @@ -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 835524c..669bbc7 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 { @@ -92,7 +98,7 @@ func main() { server.Get("/api/project/:projectId", gs.GetProject) server.Get("/api/project/getAllUsers", gs.GetAllUsersProject) server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) - server.Get("/api/getUnsignedReports", gs.GetUnsignedReports) + server.Get("/api/getUnsignedReports/:projectName", gs.GetUnsignedReports) server.Post("/api/signReport", gs.SignReport) server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) server.Put("/api/changeUserName", gs.ChangeUserName) @@ -102,6 +108,8 @@ 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) + server.Delete("/api/removeProject/:projectName", gs.RemoveProject) // 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..0160e15 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -4,7 +4,10 @@ import { User, Project, NewProject, + UserProjectMember, WeeklyReport, + StrNameChange, + NewProjMember, } from "../Types/goTypes"; /** @@ -46,7 +49,6 @@ interface API { * @returns {Promise>} A promise containing the API response indicating if the user is a project manager. */ checkIfProjectManager( - username: string, projectName: string, token: string, ): Promise>; @@ -84,7 +86,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 +129,30 @@ 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>; + /** + * Changes the username of a user in the database. + * @param {StrNameChange} data The object containing the previous and new username. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response. + */ + changeUserName( + data: StrNameChange, + token: string, + ): Promise>; + addUserToProject( + user: NewProjMember, + token: string, + ): Promise>; + + removeProject( + projectName: string, + token: string, + ): Promise>; } /** An instance of the API */ @@ -164,19 +190,17 @@ export const api: API = { ): Promise> { try { const response = await fetch(`/api/userdelete/${username}`, { - method: "POST", + method: "DELETE", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, body: JSON.stringify(username), }); - if (!response.ok) { - return { success: false, message: "Failed to remove user" }; + return { success: false, message: "Could not remove user" }; } else { - const data = (await response.json()) as User; - return { success: true, data }; + return { success: true }; } } catch (e) { return { success: false, message: "Failed to remove user" }; @@ -184,19 +208,20 @@ export const api: API = { }, async checkIfProjectManager( - username: string, projectName: string, token: string, ): Promise> { try { - const response = await fetch("/api/checkIfProjectManager", { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/checkIfProjectManager/${projectName}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - body: JSON.stringify({ username, projectName }), - }); + ); if (!response.ok) { return { @@ -208,7 +233,7 @@ export const api: API = { return { success: true, data }; } } catch (e) { - return { success: false, message: "fuck" }; + return { success: false, message: "Failed to check if project manager" }; } }, @@ -237,6 +262,30 @@ export const api: API = { } }, + async addUserToProject( + user: NewProjMember, + token: string, + ): Promise> { + try { + const response = await fetch("/api/addUserToProject", { + 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" }; + } else { + return { success: true, message: "Added member" }; + } + } catch (e) { + return { success: false, message: "Failed to add member" }; + } + }, + async renewToken(token: string): Promise> { try { const response = await fetch("/api/loginrenew", { @@ -288,7 +337,7 @@ export const api: API = { async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/submitWeeklyReport", { method: "POST", @@ -306,8 +355,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 +497,88 @@ 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", + }); + } + }, + + async changeUserName( + data: StrNameChange, + token: string, + ): Promise> { + try { + const response = await fetch("/api/changeUserName", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + return { success: false, message: "Failed to change username" }; + } else { + return { success: true }; + } + } catch (e) { + return { success: false, message: "Failed to change username" }; + } + }, + + async removeProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/projectdelete/${projectName}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } else { + const data = await response.text(); + return Promise.resolve({ success: true, message: data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } + }, }; diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx new file mode 100644 index 0000000..d29be68 --- /dev/null +++ b/frontend/src/Components/AddMember.tsx @@ -0,0 +1,39 @@ +import { APIResponse, api } from "../API/API"; +import { NewProjMember } from "../Types/goTypes"; + +/** + * Tries to add a member to a project + * @param {Object} props - A NewProjMember + * @returns {boolean} True if added, false if not + */ +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; + } + 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; +} + +export default AddMember; diff --git a/frontend/src/Components/AddUserToProject.tsx b/frontend/src/Components/AddUserToProject.tsx new file mode 100644 index 0000000..9f4439b --- /dev/null +++ b/frontend/src/Components/AddUserToProject.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { NewProjMember } from "../Types/goTypes"; +import Button from "./Button"; +import GetAllUsers from "./GetAllUsers"; +import AddMember from "./AddMember"; +import BackButton from "./BackButton"; + +/** + * Provides UI for adding a member to a project. + * @returns {JSX.Element} - Returns the component UI for adding a member + */ +function AddUserToProject(): JSX.Element { + const [name, setName] = useState(""); + const [users, setUsers] = useState([]); + const [role, setRole] = useState(""); + GetAllUsers({ setUsersProp: setUsers }); + + const handleClick = (): boolean => { + const newMember: NewProjMember = { + username: name, + projectname: localStorage.getItem("projectName") ?? "", + role: role, + }; + return AddMember({ memberToAdd: newMember }); + }; + + return ( +
+

+ User chosen: [{name}] +

+

+ Role chosen: [{role}] +

+

+ Project chosen: [{localStorage.getItem("projectName") ?? ""}] +

+

Choose role:

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

Choose user:

+
+
    +
    + {users.map((user) => ( +
  • { + setName(user); + }} + > + {user} +
  • + ))} +
+
+
+
+

+
+ ); +} + +export default AddUserToProject; 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/AllTimeReportsInProjectOtherUser.tsx b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx new file mode 100644 index 0000000..09ca6dc --- /dev/null +++ b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx @@ -0,0 +1,103 @@ +//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 { Link, useParams } from "react-router-dom"; + +/** + * Renders a component that displays all the time reports for a specific project. + * @returns {JSX.Element} representing the component. + */ +function AllTimeReportsInProject(): JSX.Element { + const { username } = useParams(); + const { projectName } = useParams(); + 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( + projectName ?? "", + token, + ); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } 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(); + }, []); + + return ( + <> +

{username}'s Time Reports

+
+ {weeklyReports.map((newWeeklyReport, index) => ( + +
+

+ {"Week: "} + {newWeeklyReport.week} +

+

+ {"Total Time: "} + {newWeeklyReport.developmentTime + + newWeeklyReport.meetingTime + + newWeeklyReport.adminTime + + newWeeklyReport.ownWorkTime + + newWeeklyReport.studyTime + + newWeeklyReport.testingTime}{" "} + min +

+

+ {"Signed: "} + NO +

+
+ + ))} +
+ + ); +} + +export default AllTimeReportsInProject; diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index 3c35e94..e297a04 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,23 +1,48 @@ import React, { useState } from "react"; import InputField from "./InputField"; +import { api } from "../API/API"; function ChangeUsername(): JSX.Element { const [newUsername, setNewUsername] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); const handleChange = (e: React.ChangeEvent): void => { setNewUsername(e.target.value); }; - // const handleSubmit = async (): Promise => { - // try { - // // Call the API function to update the username - // await api.updateUsername(newUsername); - // // Optionally, add a success message or redirect the user - // } catch (error) { - // console.error("Error updating username:", error); - // // Optionally, handle the error - // } - // }; + const handleSubmit = async (): Promise => { + try { + // Call the API function to change the username + const token = localStorage.getItem("accessToken"); + if (!token) { + throw new Error("Access token not found"); + } + + const response = await api.changeUserName( + { prevName: "currentName", newName: newUsername }, + token, + ); + + if (response.success) { + // Optionally, add a success message or redirect the user + console.log("Username changed successfully"); + } else { + // Handle the error message + console.error("Failed to change username:", response.message); + setErrorMessage(response.message ?? "Failed to change username"); + } + } catch (error) { + console.error("Error changing username:", error); + // Optionally, handle the error + setErrorMessage("Failed to change username"); + } + }; + + const handleButtonClick = (): void => { + handleSubmit().catch((error) => { + console.error("Error in handleSubmit:", error); + }); + }; return (
@@ -27,6 +52,8 @@ function ChangeUsername(): JSX.Element { value={newUsername} onChange={handleChange} /> + {errorMessage &&
{errorMessage}
} +
); } diff --git a/frontend/src/Components/DeleteUser.tsx b/frontend/src/Components/DeleteUser.tsx index db49724..d1dbc7f 100644 --- a/frontend/src/Components/DeleteUser.tsx +++ b/frontend/src/Components/DeleteUser.tsx @@ -11,7 +11,6 @@ import { api, APIResponse } from "../API/API"; */ function DeleteUser(props: { usernameToDelete: string }): boolean { - //console.log(props.usernameToDelete); FOR DEBUG let removed = false; api .removeUser( @@ -20,12 +19,16 @@ function DeleteUser(props: { usernameToDelete: string }): boolean { ) .then((response: APIResponse) => { if (response.success) { + alert("User has been deleted!"); + location.reload(); removed = true; } else { + alert("User has not been deleted"); console.error(response.message); } }) .catch((error) => { + alert("User has not been deleted"); console.error("An error occurred during creation:", error); }); return removed; diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx new file mode 100644 index 0000000..780f20c --- /dev/null +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -0,0 +1,129 @@ +import { useState, useEffect } from "react"; +import { Link, useParams } from "react-router-dom"; + +interface UnsignedReports { + projectName: string; + username: string; + week: number; + signed: boolean; +} + +/** + * Renders a component that displays the projects a user is a part of and links to the projects start-page. + * @returns The JSX element representing the component. + */ +function DisplayUserProject(): JSX.Element { + const { projectName } = useParams(); + 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(() => { + void getUnsignedReports(); + }, []); + + return ( + <> +

+ All Unsigned Reports In: {projectName}{" "} +

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

+
+
+

{unsignedReport.username}

+ Week: +

{unsignedReport.week}

+ Signed: +

NO

+
+
+
+ +

+ View Report +

+ +
+
+
+

+ ), + )} +
+ + ); +} + +export default DisplayUserProject; diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index f4fd782..29e4bcb 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Project } from "../Types/goTypes"; -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { api } from "../API/API"; /** @@ -9,6 +9,7 @@ import { api } from "../API/API"; */ function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); + const navigate = useNavigate(); const getProjects = async (): Promise => { const token = localStorage.getItem("accessToken") ?? ""; @@ -21,6 +22,21 @@ function DisplayUserProject(): JSX.Element { } }; + const handleProjectClick = async (projectName: string): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.checkIfProjectManager(projectName, token); + if (response.success) { + if (response.data) { + navigate(`/PMProjectPage/${projectName}`); + } else { + navigate(`/project/${projectName}`); + } + } else { + // handle error + console.error(response.message); + } + }; + // Call getProjects when the component mounts useEffect(() => { void getProjects(); @@ -30,12 +46,15 @@ function DisplayUserProject(): JSX.Element { <>

Your Projects

- {projects.map((project, index) => ( - + {projects.map((project) => ( +
void handleProjectClick(project.name)} + key={project.id} + >

{project.name}

- +
))}
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 76c2b94..384359e 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -18,46 +18,47 @@ 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, - ); - - if (response.success) { - const report: WeeklyReport = response.data ?? { - reportId: 0, - userId: 0, - projectId: 0, - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setWeek(report.week); - setDevelopmentTime(report.developmentTime); - setMeetingTime(report.meetingTime); - setAdminTime(report.adminTime); - setOwnWorkTime(report.ownWorkTime); - setStudyTime(report.studyTime); - setTestingTime(report.testingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - }; + const { projectName, fetchedWeek } = useParams<{ + projectName: string; + fetchedWeek: string; + }>(); + console.log(projectName, fetchedWeek); useEffect(() => { + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek ?? "", + token, + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + void fetchWeeklyReport(); - }); + }, [projectName, fetchedWeek, token]); const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { @@ -78,6 +79,7 @@ export default function GetWeeklyReport(): JSX.Element { return ( <> +

Edit Time Report

{ @@ -92,24 +94,10 @@ export default function GetWeeklyReport(): JSX.Element { }} >
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> +
+

Week: {week}

+
+ @@ -129,9 +117,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime} + value={developmentTime === 0 ? "" : developmentTime} onChange={(e) => { - setDevelopmentTime(parseInt(e.target.value)); + if (e.target.value === "") { + setDevelopmentTime(0); + return; + } else { + setDevelopmentTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -148,9 +141,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime} + value={meetingTime === 0 ? "" : meetingTime} onChange={(e) => { - setMeetingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setMeetingTime(0); + return; + } else { + setMeetingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -167,9 +165,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime} + value={adminTime === 0 ? "" : adminTime} onChange={(e) => { - setAdminTime(parseInt(e.target.value)); + if (e.target.value === "") { + setAdminTime(0); + return; + } else { + setAdminTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -186,9 +189,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime} + value={ownWorkTime === 0 ? "" : ownWorkTime} onChange={(e) => { - setOwnWorkTime(parseInt(e.target.value)); + if (e.target.value === "") { + setOwnWorkTime(0); + return; + } else { + setOwnWorkTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -205,9 +213,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime} + value={studyTime === 0 ? "" : studyTime} onChange={(e) => { - setStudyTime(parseInt(e.target.value)); + if (e.target.value === "") { + setStudyTime(0); + return; + } else { + setStudyTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -224,9 +237,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime} + value={testingTime === 0 ? "" : testingTime} onChange={(e) => { - setTestingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setTestingTime(0); + return; + } else { + setTestingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; 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..f684b0c 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -12,66 +12,103 @@ import Button from "./Button"; */ export default function NewWeeklyReport(): JSX.Element { const [week, setWeek] = useState(0); - const [developmentTime, setDevelopmentTime] = useState(); - const [meetingTime, setMeetingTime] = useState(); - const [adminTime, setAdminTime] = useState(); - const [ownWorkTime, setOwnWorkTime] = useState(); - const [studyTime, setStudyTime] = useState(); - const [testingTime, setTestingTime] = useState(); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); const { projectName } = useParams(); const token = localStorage.getItem("accessToken") ?? ""; - const handleNewWeeklyReport = async (): Promise => { + const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { projectName: projectName ?? "", week: week, - developmentTime: developmentTime ?? 0, - meetingTime: meetingTime ?? 0, - adminTime: adminTime ?? 0, - ownWorkTime: ownWorkTime ?? 0, - studyTime: studyTime ?? 0, - testingTime: testingTime ?? 0, + developmentTime: developmentTime, + meetingTime: meetingTime, + adminTime: adminTime, + ownWorkTime: ownWorkTime, + studyTime: studyTime, + testingTime: testingTime, }; - await api.submitWeeklyReport(newWeeklyReport, token); + const response = await api.submitWeeklyReport(newWeeklyReport, token); + console.log(response); + if (response.success) { + return true; + } else { + return false; + } }; const navigate = useNavigate(); + // Check if the browser is Chrome or Edge + const isChromeOrEdge = /Chrome|Edg/.test(navigator.userAgent); return ( <>
{ - if (week === 0) { - alert("Please enter a week number"); - e.preventDefault(); - return; - } e.preventDefault(); - void handleNewWeeklyReport(); - navigate(-1); + void (async (): Promise => { + if (week === 0 || week > 53 || week < 1) { + alert("Please enter a valid week number"); + return; + } + + const success = await handleNewWeeklyReport(); + if (!success) { + alert( + "A Time Report for this week already exists, please go to the edit page to edit it or change week number.", + ); + return; + } + alert("Weekly report submitted successfully"); + navigate(-1); + })(); }} >
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + {isChromeOrEdge ? ( + { + const weekNumber = parseInt(e.target.value.split("-W")[1]); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + onPaste={(event) => { event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> + }} + /> + ) : ( + { + const weekNumber = parseInt(e.target.value); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> + )}
@@ -91,9 +128,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime} + value={developmentTime === 0 ? "" : developmentTime} onChange={(e) => { - setDevelopmentTime(parseInt(e.target.value)); + if (e.target.value === "") { + setDevelopmentTime(0); + return; + } else { + setDevelopmentTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -110,9 +152,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime} + value={meetingTime === 0 ? "" : meetingTime} onChange={(e) => { - setMeetingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setMeetingTime(0); + return; + } else { + setMeetingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -129,9 +176,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime} + value={adminTime === 0 ? "" : adminTime} onChange={(e) => { - setAdminTime(parseInt(e.target.value)); + if (e.target.value === "") { + setAdminTime(0); + return; + } else { + setAdminTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -148,9 +200,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime} + value={ownWorkTime === 0 ? "" : ownWorkTime} onChange={(e) => { - setOwnWorkTime(parseInt(e.target.value)); + if (e.target.value === "") { + setOwnWorkTime(0); + return; + } else { + setOwnWorkTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -167,9 +224,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime} + value={studyTime === 0 ? "" : studyTime} onChange={(e) => { - setStudyTime(parseInt(e.target.value)); + if (e.target.value === "") { + setStudyTime(0); + return; + } else { + setStudyTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -186,9 +248,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime} + value={testingTime === 0 ? "" : testingTime} onChange={(e) => { - setTestingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setTestingTime(0); + return; + } else { + setTestingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; diff --git a/frontend/src/Components/OtherUsersTR.tsx b/frontend/src/Components/OtherUsersTR.tsx new file mode 100644 index 0000000..2b00e16 --- /dev/null +++ b/frontend/src/Components/OtherUsersTR.tsx @@ -0,0 +1,153 @@ +import { useState, useEffect } from "react"; +import { WeeklyReport } from "../Types/goTypes"; +import { api } from "../API/API"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for editing a weekly report. + * @returns JSX.Element + */ + +//This component does not yet work as intended. It is supposed to display the weekly report of a user in a project. +export default function OtherUsersTR(): JSX.Element { + const [week, setWeek] = useState(0); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); + + const token = localStorage.getItem("accessToken") ?? ""; + const { projectName } = useParams(); + const { username } = useParams(); + const { fetchedWeek } = useParams(); + + useEffect(() => { + const fetchUsersWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + + void fetchUsersWeeklyReport(); + }); + + return ( + <> +

{username}'s Report

+
+
+
+

Week: {week}

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Activity + Total Time (min) +
Development + +
Meeting + +
Administration + +
Own Work + +
Studies + +
Testing + +
+
+
+ + ); +} diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx new file mode 100644 index 0000000..3075b19 --- /dev/null +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import Button from "./Button"; +import { UserProjectMember } from "../Types/goTypes"; +import GetUsersInProject from "./GetUsersInProject"; +import { Link } from "react-router-dom"; + +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 ( +
+
+
+

+ {localStorage.getItem("projectName") ?? ""} +

+

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..f25ee47 --- /dev/null +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; +import { NewProject } from "../Types/goTypes"; +import ProjectInfoModal from "./ProjectInfoModal"; +import UserInfoModal from "./UserInfoModal"; + +/** + * 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 = (projectname: string): void => { + setProjectname(projectname); + localStorage.setItem("projectName", projectname); + setProjectModalVisible(true); + }; + + const handleCloseProject = (): void => { + setProjectname(""); + setProjectModalVisible(false); + }; + + const handleCloseUser = (): void => { + setProjectname(""); + setUserModalVisible(false); + }; + + return ( + <> + + { + return; + }} + isVisible={userModalVisible} + username={username} + /> +
    +
      + {props.projects.map((project) => ( +
    • { + handleClickProject(project.name); + }} + > + {project.name} +
    • + ))} +
    +
    + + ); +} diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 73e29e5..60ffcd9 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,91 +1,55 @@ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; +import { UserProjectMember } from "../Types/goTypes"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); - const [projectMembers, setProjectMembers] = useState([]); - - // const getProjectMembers = async (): Promise => { - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.getProjectMembers(projectName ?? "", token); - // console.log(response); - // if (response.success) { - // setProjectMembers(response.data ?? []); - // } else { - // console.error(response.message); - // } - // }; - - interface ProjectMember { - username: string; - role: string; - } - - const mockProjectMembers = [ - { - username: "username1", - role: "Project Manager", - }, - { - username: "username2", - role: "System Manager", - }, - { - username: "username3", - role: "Developer", - }, - { - username: "username4", - role: "Tester", - }, - { - username: "username5", - role: "Tester", - }, - { - username: "username6", - role: "Tester", - }, - ]; - - const getProjectMembers = async (): Promise => { - // Use the mock data - setProjectMembers(mockProjectMembers); - - await Promise.resolve(); - }; + const [projectMembers, setProjectMembers] = useState([]); useEffect(() => { + const getProjectMembers = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getAllUsersProject(projectName ?? "", token); + console.log(response); + if (response.success) { + setProjectMembers(response.data ?? []); + } else { + console.error(response.message); + } + }; + void getProjectMembers(); - }); + }, [projectName]); + + interface ProjectMember { + Username: string; + UserRole: string; + } return ( <> +

    + All Members In: {projectName}{" "} +

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

    -

    {projectMember.username}

    +

    {projectMember.Username}

    Role: -

    {projectMember.role}

    +

    {projectMember.UserRole}

    View Reports

    - -

    - Change Role -

    -
    diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index df07c6e..6192637 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -4,7 +4,6 @@ import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; import InputField from "./InputField"; -import { useNavigate } from "react-router-dom"; /** * Renders a registration form for the admin to add new users in. @@ -15,8 +14,6 @@ export default function Register(): JSX.Element { const [password, setPassword] = useState(); const [errMessage, setErrMessage] = useState(); - const nav = useNavigate(); - const handleRegister = async (): Promise => { const newUser: NewUser = { username: username ?? "", @@ -24,8 +21,9 @@ export default function Register(): JSX.Element { }; const response = await api.registerUser(newUser); if (response.success) { - nav("/"); // Instantly navigate to the login page + alert("User added!"); } else { + alert("User not added"); setErrMessage(response.message ?? "Unknown error"); console.error(errMessage); } diff --git a/frontend/src/Components/TimePerActivity.tsx b/frontend/src/Components/TimePerActivity.tsx new file mode 100644 index 0000000..3dc1a6b --- /dev/null +++ b/frontend/src/Components/TimePerActivity.tsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for showing total time per role in a project. + * @returns JSX.Element + */ +export default function TimePerRole(): JSX.Element { + const [developmentTime, setDevelopmentTime] = useState(); + const [meetingTime, setMeetingTime] = useState(); + const [adminTime, setAdminTime] = useState(); + const [ownWorkTime, setOwnWorkTime] = useState(); + const [studyTime, setStudyTime] = useState(); + const [testingTime, setTestingTime] = useState(); + + // const token = localStorage.getItem("accessToken") ?? ""; + // const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + + // const fetchTimePerRole = async (): Promise => { + // const response = await api.getTimePerRole( + // username, + // projectName ?? "", + // token, + // ); + // { + // if (response.success) { + // const report: TimePerRole = response.data ?? { + // PManagerTime: 0, + // SManagerTime: 0, + // DeveloperTime: 0, + // TesterTime: 0, + // }; + // } else { + // console.error("Failed to fetch weekly report:", response.message); + // } + // } + + interface TimePerActivity { + developmentTime: number; + meetingTime: number; + adminTime: number; + ownWorkTime: number; + studyTime: number; + testingTime: number; + } + + const fetchTimePerActivity = async (): Promise => { + // Use mock data + const report: TimePerActivity = { + developmentTime: 100, + meetingTime: 200, + adminTime: 300, + ownWorkTime: 50, + studyTime: 75, + testingTime: 110, + }; + + // Set the state with the mock data + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + + await Promise.resolve(); + }; + + useEffect(() => { + void fetchTimePerActivity(); + }); + + return ( + <> +

    + Total Time Per Activity In: {projectName}{" "} +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Activity + Total Time (min) +
    Development + { + event.preventDefault(); + }} + /> +
    Meeting + { + event.preventDefault(); + }} + /> +
    Administration + { + event.preventDefault(); + }} + /> +
    Own Work + { + event.preventDefault(); + }} + /> +
    Studies + { + event.preventDefault(); + }} + /> +
    Testing + { + event.preventDefault(); + }} + /> +
    +
    +
    + + ); +} diff --git a/frontend/src/Components/TimePerRole.tsx b/frontend/src/Components/TimePerRole.tsx new file mode 100644 index 0000000..f62d83a --- /dev/null +++ b/frontend/src/Components/TimePerRole.tsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for showing total time per role in a project. + * @returns JSX.Element + */ +export default function TimePerRole(): JSX.Element { + const [PManagerTime, setPManagerTime] = useState(0); + const [SManagerTime, setSManagerTime] = useState(0); + const [DeveloperTime, setDeveloperTime] = useState(0); + const [TesterTime, setTesterTime] = useState(0); + + // const token = localStorage.getItem("accessToken") ?? ""; + // const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + + // const fetchTimePerRole = async (): Promise => { + // const response = await api.getTimePerRole( + // username, + // projectName ?? "", + // token, + // ); + // { + // if (response.success) { + // const report: TimePerRole = response.data ?? { + // PManagerTime: 0, + // SManagerTime: 0, + // DeveloperTime: 0, + // TesterTime: 0, + // }; + // } else { + // console.error("Failed to fetch weekly report:", response.message); + // } + // } + + interface TimePerRole { + PManager: number; + SManager: number; + Developer: number; + Tester: number; + } + + const fetchTimePerRole = async (): Promise => { + // Use mock data + const report: TimePerRole = { + PManager: 120, + SManager: 80, + Developer: 200, + Tester: 150, + }; + + // Set the state with the mock data + setPManagerTime(report.PManager); + setSManagerTime(report.SManager); + setDeveloperTime(report.Developer); + setTesterTime(report.Tester); + + await Promise.resolve(); + }; + + useEffect(() => { + void fetchTimePerRole(); + }); + + return ( + <> +

    + Total Time Per Role In: {projectName}{" "} +

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    Role + Total Time (min) +
    Project Manager + { + event.preventDefault(); + }} + /> +
    System Manager + { + event.preventDefault(); + }} + /> +
    Administration + { + event.preventDefault(); + }} + /> +
    Own Work + { + event.preventDefault(); + }} + /> +
    +
    +
    + + ); +} diff --git a/frontend/src/Components/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index a22ef01..9695899 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: @@ -34,7 +49,13 @@ function UserInfoModal(props: {

    + +
    + + ); +} 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..893bdad 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx @@ -1,27 +1,10 @@ +import AddUserToProject from "../../Components/AddUserToProject"; import BasicWindow from "../../Components/BasicWindow"; -import Button from "../../Components/Button"; function AdminProjectAddMember(): JSX.Element { - const content = <>; + const content = ; - const buttons = ( - <> -