diff --git a/backend/config.json b/backend/config.json index 4b6c95c6..65c49fba 100644 --- a/backend/config.json +++ b/backend/config.json @@ -1,7 +1,7 @@ { "apiErrors": { "invalidID": { - "http": 204, + "http": 423, "reason": "This endpoint requires an ID. The ID you provided was invalid." }, "argumentError": { @@ -95,7 +95,7 @@ ], "preferred": "/api-osoc", "authScheme": "auth/osoc2", - "defaultUserId": 3, + "defaultUserId": 1, "port": 4096, "pageSize": 10 }, diff --git a/backend/orm_functions/project_role.ts b/backend/orm_functions/project_role.ts index 44b659d3..307a4c3f 100644 --- a/backend/orm_functions/project_role.ts +++ b/backend/orm_functions/project_role.ts @@ -87,7 +87,7 @@ export async function updateProjectRole(projectRole: UpdateProjectRole) { * * @param projectRoleId the projectRole we are deleting from the project * role-table - * @returns TODO: what does this return? + * @returns a promise with the deleted record */ export async function deleteProjectRole(projectRoleId: number) { const result = await prisma.project_role.delete({ diff --git a/backend/orm_functions/student.ts b/backend/orm_functions/student.ts index 4731c21d..f8f211ae 100644 --- a/backend/orm_functions/student.ts +++ b/backend/orm_functions/student.ts @@ -12,6 +12,7 @@ import { } from "./orm_types"; import { getOsocYearsForLoginUser } from "./login_user"; import { deletePersonFromDB } from "./person"; +import { Decision } from "../types"; /** * @@ -179,7 +180,13 @@ export async function filterStudents( // manually create filter object for evaluation because evaluation doesn't need to exist // and then the whole object needs to be undefined let evaluationFilter; - if (statusFilter) { + if ((statusFilter as Decision) === Decision.NONE) { + evaluationFilter = { + none: { + is_final: true, + }, + }; + } else if (statusFilter) { evaluationFilter = { some: { decision: statusFilter, diff --git a/backend/routes/followup.ts b/backend/routes/followup.ts index 72a0f101..1ee857dd 100644 --- a/backend/routes/followup.ts +++ b/backend/routes/followup.ts @@ -4,7 +4,7 @@ import * as ormJA from "../orm_functions/job_application"; import * as rq from "../request"; import { Responses } from "../types"; import * as util from "../utility"; -import { checkYearPermissionStudent, errors } from "../utility"; +import { checkYearPermissionsFollowup, errors } from "../utility"; import { getOsocYearsForLoginUser } from "../orm_functions/login_user"; import { getLatestOsoc, getOsocById } from "../orm_functions/osoc"; import { getJobApplication } from "../orm_functions/job_application"; @@ -21,6 +21,7 @@ export async function getFollowup( return rq .parseGetFollowupStudentRequest(req) .then((parsed) => util.checkSessionKey(parsed)) + .then(checkYearPermissionsFollowup) .then((checked) => ormJA .getJobApplication(checked.data.id) @@ -59,7 +60,7 @@ export async function updateFollowup( return rq .parseSetFollowupStudentRequest(req) .then((parsed) => util.checkSessionKey(parsed)) - .then(checkYearPermissionStudent) + .then(checkYearPermissionsFollowup) .then(async (checked) => { // modifications to a job application is only allowed if the job application is of the most recent osoc year const [jobApplication, latestOsoc] = await Promise.all([ diff --git a/backend/routes/project.ts b/backend/routes/project.ts index 06f17961..ab3a77b0 100644 --- a/backend/routes/project.ts +++ b/backend/routes/project.ts @@ -203,7 +203,7 @@ export async function getProject( ): Promise { const parsedRequest = await rq.parseSingleProjectRequest(req); const checkedId = await util - .isAdmin(parsedRequest) + .checkSessionKey(parsedRequest) .then(checkYearPermissionProject) .then((v) => util.isValidID(v.data, "project")); @@ -506,7 +506,7 @@ export async function unAssignCoach( ): Promise { return rq .parseRemoveCoachRequest(req) - .then((parsed) => util.checkSessionKey(parsed)) + .then((parsed) => util.isAdmin(parsed)) .then(async (checked) => { const project = await ormPr.getProjectById(checked.data.id); @@ -555,7 +555,7 @@ export async function assignCoach( ): Promise { return rq .parseAssignCoachRequest(req) - .then((parsed) => util.checkSessionKey(parsed)) + .then((parsed) => util.isAdmin(parsed)) .then(async (checked) => { const project = await ormPr.getProjectById(checked.data.id); @@ -701,7 +701,7 @@ export async function assignStudent( // authenticate, parse, ... const checked = await rq .parseDraftStudentRequest(req) - .then((parsed) => util.isAdmin(parsed)); + .then((parsed) => util.checkSessionKey(parsed)); // check if edition is ready const latestOsoc = await ormOsoc @@ -900,7 +900,6 @@ export function getRouter(): express.Router { util.route(router, "delete", "/:id/coach", unAssignCoach); util.route(router, "post", "/:id/coach", assignCoach); - // TODO add project conflicts util.addAllInvalidVerbs(router, [ "/", "/all", diff --git a/backend/tests/request.test.ts b/backend/tests/request.test.ts index 633d541c..ebc0c396 100644 --- a/backend/tests/request.test.ts +++ b/backend/tests/request.test.ts @@ -264,7 +264,6 @@ test("Can parse login request", () => { unvalid.body.pass = "Pass #2"; unvalid.body.name = "Name.email.be"; - // TODO return Promise.all([ expect(Rq.parseLoginRequest(valid)).resolves.toStrictEqual({ name: "alice.student@hotmail.be", diff --git a/backend/tests/routes_unit/followup.test.ts b/backend/tests/routes_unit/followup.test.ts index a2c1c19f..2915da9f 100644 --- a/backend/tests/routes_unit/followup.test.ts +++ b/backend/tests/routes_unit/followup.test.ts @@ -30,7 +30,8 @@ jest.mock("../../utility", () => { checkSessionKey: jest.fn(), isAdmin: jest.fn(), checkYearPermissionStudent: jest.fn(), - }; // we want to only mock checkSessionKey, isAdmin and checkYearPermissionStudent + checkYearPermissionsFollowup: jest.fn(), + }; // we want to only mock checkSessionKey, isAdmin, checkYearPermissionStudent and checkYearPermissionsFollowup }); export const utilMock = util as jest.Mocked; @@ -179,6 +180,9 @@ beforeEach(() => { utilMock.checkYearPermissionStudent.mockImplementation((v) => Promise.resolve(v) ); + utilMock.checkYearPermissionsFollowup.mockImplementation((v) => + Promise.resolve(v) + ); osocMock.getLatestOsoc.mockResolvedValue(osocdat); osocMock.getOsocById.mockResolvedValue(osocdat); @@ -210,6 +214,7 @@ afterEach(() => { utilMock.checkSessionKey.mockReset(); utilMock.isAdmin.mockReset(); utilMock.checkYearPermissionStudent.mockReset(); + utilMock.checkYearPermissionsFollowup.mockReset(); osocMock.getLatestOsoc.mockReset(); osocMock.getOsocById.mockReset(); diff --git a/backend/tests/routes_unit/project.test.ts b/backend/tests/routes_unit/project.test.ts index 7317500e..531ae9f9 100644 --- a/backend/tests/routes_unit/project.test.ts +++ b/backend/tests/routes_unit/project.test.ts @@ -933,7 +933,7 @@ test("Can get single project", async () => { await expect(project.getProject(req)).resolves.toStrictEqual(res); expectCall(reqMock.parseSingleProjectRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(utilMock.isValidID).toHaveBeenCalledTimes(1); expectCall(ormPrMock.getProjectById, 0); expectCall(ormCMock.contractsByProject, 0); @@ -956,7 +956,7 @@ test("Can't get single project (role error)", async () => { await expect(project.getProject(req)).rejects.toStrictEqual(undefined); expectCall(reqMock.parseSingleProjectRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(utilMock.isValidID).toHaveBeenCalledTimes(1); expectCall(ormPrMock.getProjectById, 0); expectCall(ormCMock.contractsByProject, 0); @@ -978,7 +978,7 @@ test("Can't get single project (ID error)", async () => { await expect(project.getProject(req)).rejects.toStrictEqual(undefined); expectCall(reqMock.parseSingleProjectRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(utilMock.isValidID).toHaveBeenCalledTimes(1); expectCall(ormPrMock.getProjectById, 0); expect(ormCMock.contractsByProject).not.toHaveBeenCalled(); @@ -1242,7 +1242,7 @@ test("Can un-assign coaches", async () => { // await project.unAssignCoach(req); await expect(project.unAssignCoach(req)).resolves.toStrictEqual({}); expectCall(reqMock.parseRemoveCoachRequest, req); - expectCall(utilMock.checkSessionKey, req.body); + expectCall(utilMock.isAdmin, req.body); expectCall(ormPUMock.getUsersFor, 0); expectCall(ormPUMock.deleteProjectUser, { loginUserId: req.body.loginUserId, @@ -1263,7 +1263,7 @@ test("Can't un-assign coaches (invalid id)", async () => { reason: "The coach with ID 7 is not assigned to project 0", }); expectCall(reqMock.parseRemoveCoachRequest, req); - expectCall(utilMock.checkSessionKey, req.body); + expectCall(utilMock.isAdmin, req.body); expectCall(ormPUMock.getUsersFor, 0); expect(ormPUMock.deleteProjectUser).not.toHaveBeenCalled(); }); @@ -1282,7 +1282,7 @@ test("Can assign coaches", async () => { project_id: 0, }); expectCall(reqMock.parseAssignCoachRequest, req); - expectCall(utilMock.checkSessionKey, req.body); + expectCall(utilMock.isAdmin, req.body); expectCall(ormPUMock.getUsersFor, 0); expectCall(ormPUMock.createProjectUser, { projectId: 0, loginUserId: 7 }); }); @@ -1300,7 +1300,7 @@ test("Can't assign coaches (already assigned)", async () => { reason: "The coach with ID 0 is already assigned to project 0", }); expectCall(reqMock.parseAssignCoachRequest, req); - expectCall(utilMock.checkSessionKey, req.body); + expectCall(utilMock.isAdmin, req.body); expectCall(ormPUMock.getUsersFor, 0); expect(ormPUMock.createProjectUser).not.toHaveBeenCalled(); }); @@ -1401,7 +1401,7 @@ test("Can assign students", async () => { role: "dev", }); expectCall(reqMock.parseDraftStudentRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(ormOMock.getLatestOsoc).toHaveBeenCalledTimes(1); expectCall(ormCMock.contractsForStudent, req.body.studentId); expectCall(ormCMock.createContract, { @@ -1460,7 +1460,7 @@ test("Can't assign students (no places)", async () => { reason: "There are no more free spaces for that role", }); expectCall(reqMock.parseDraftStudentRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(ormOMock.getLatestOsoc).toHaveBeenCalledTimes(1); expectCall(ormCMock.contractsForStudent, req.body.studentId); expect(ormCMock.createContract).not.toHaveBeenCalled(); @@ -1514,7 +1514,7 @@ test("Can't assign students (no such role)", async () => { reason: "That role doesn't exist", }); expectCall(reqMock.parseDraftStudentRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(ormOMock.getLatestOsoc).toHaveBeenCalledTimes(1); expectCall(ormCMock.contractsForStudent, req.body.studentId); expect(ormCMock.createContract).not.toHaveBeenCalled(); @@ -1571,7 +1571,7 @@ test("Can't assign students (already used)", async () => { reason: "This student does already have a contract", }); expectCall(reqMock.parseDraftStudentRequest, req); - expectCall(utilMock.isAdmin, req.body); + expectCall(utilMock.checkSessionKey, req.body); expect(ormOMock.getLatestOsoc).toHaveBeenCalledTimes(1); expectCall(ormCMock.contractsForStudent, req.body.studentId); expect(ormCMock.createContract).not.toHaveBeenCalled(); diff --git a/backend/types.ts b/backend/types.ts index 4402932e..a83b982e 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -1527,6 +1527,7 @@ export interface ServerToClientEvents { projectWasCreatedOrDeleted: () => void; projectWasModified: (projectId: number) => void; osocWasCreatedOrDeleted: () => void; + yearPermissionUpdated: (loginUserId: number) => void; } /** @@ -1547,6 +1548,7 @@ export interface ClientToServerEvents { projectDeleted: () => void; osocDeleted: () => void; osocCreated: () => void; + yearPermissionUpdate: (loginUserId: number) => void; } /** @@ -1567,6 +1569,7 @@ export enum Decision { YES = "YES", MAYBE = "MAYBE", NO = "NO", + NONE = "NONE", } export enum AccountStatus { diff --git a/backend/utility.ts b/backend/utility.ts index 11725757..14f3bfee 100644 --- a/backend/utility.ts +++ b/backend/utility.ts @@ -24,6 +24,7 @@ import { getAppliedYearsForStudent } from "./orm_functions/student"; import IdRequest = Requests.IdRequest; import { getProjectYear } from "./orm_functions/project"; import { getOsocById } from "./orm_functions/osoc"; +import { getJobApplication } from "./orm_functions/job_application"; /** * The API error cooking functions. HTTP error codes are loaded from @@ -416,6 +417,29 @@ export async function checkYearPermissionOsoc( return Promise.reject(errors.cookInsufficientRights()); } +/** + * returns the userData object if the user is allowed to see the followup + * Otherwise it returns an insufficient rights error. + * @param userData: object with the userId and osocID + */ +export async function checkYearPermissionsFollowup( + userData: WithUserID +): Promise> { + // get the years that are visible for the loginUser + const visibleYears = await getOsocYearsForLoginUser(userData.userId); + // get the year that the application belongs to + const job_application = await getJobApplication(userData.data.id); + + // check if the project year is inside the visible years for the user + if ( + job_application !== null && + visibleYears.includes(job_application.osoc.year) + ) { + return userData; + } + return Promise.reject(errors.cookInsufficientRights()); +} + /** * Generates a new session key. * @returns The newly generated session key. diff --git a/backend/websocket_events/osoc.ts b/backend/websocket_events/osoc.ts index 88d5223c..8e695645 100644 --- a/backend/websocket_events/osoc.ts +++ b/backend/websocket_events/osoc.ts @@ -29,7 +29,11 @@ export function registerOsocHandlers( const OsocCreatedOrDeleted = () => { socket.broadcast.emit("osocWasCreatedOrDeleted"); }; + const yearPermissionUpdated = (loginUserId: number) => { + socket.broadcast.emit("yearPermissionUpdated", loginUserId); + }; socket.on("osocDeleted", OsocCreatedOrDeleted); socket.on("osocCreated", OsocCreatedOrDeleted); + socket.on("yearPermissionUpdate", yearPermissionUpdated); } diff --git a/database/startupScripts/generate_data.sql b/database/startupScripts/generate_data.sql index 9cf40de7..3863ba12 100644 --- a/database/startupScripts/generate_data.sql +++ b/database/startupScripts/generate_data.sql @@ -1,84 +1,5 @@ -/* Insert data into person table */ INSERT INTO person(email, "name") -VALUES('alicestudent@gmail.com', 'Alice Smith'), -('bob.admin@osoc.com', 'Bob Jones'), ('Trudycoach@gmail.com', 'Trudy Taylor'), -('osoc2@mail.com', 'Osoc TeamTwo'); +VALUES('osoc2@mail.com', 'Osoc TeamTwo'); -/* Insert data into student table */ -INSERT INTO student(person_id, gender, phone_number, nickname, alumni) -VALUES((SELECT person_id FROM person WHERE "name" LIKE 'Alice%'), -'Female', '0032476553498', 'Unicorn', TRUE); - -/* Insert data into login_user table */ INSERT INTO login_user(person_id, password, is_admin, is_coach, account_status) -VALUES((SELECT person_id FROM person WHERE "name" LIKE 'Bob%'), '$2b$08$ffQO2UCEFHUHcn9d.XHHg.hFKn7oF5AOW82J.hsOqq8gV0TzMEuzq', TRUE, FALSE , 'ACTIVATED'), -((SELECT person_id FROM person WHERE "name" LIKE 'Trudy%'), '$2b$08$XDDmyKZnWsai9wVAW7r.GOOv7pGKa7oHLlBhVAqTmPgiscMzynpVq', FALSE, TRUE, 'PENDING'), -((SELECT person_id FROM person WHERE email = 'osoc2@mail.com'), '$2b$08$MCblaKGOOBV7NpiW62GEc.km732o6XWDJxU6SfU3NMENxMuCWFlJq', TRUE, FALSE, 'ACTIVATED'); - -/* Insert data into osoc table */ -INSERT INTO osoc(year)VALUES(2022); - -/* Insert data into job_application table */ -INSERT INTO job_application(student_id, osoc_id, student_volunteer_info, responsibilities, fun_fact, student_coach, - edus, edu_level, edu_duration, edu_year, edu_institute, email_status, created_at)VALUES - ((SELECT student_id FROM student WHERE phone_number = '0032476553498'), (SELECT osoc_id FROM osoc WHERE year = 2022), - 'Yes, I can work with a student employment agreement in Belgium', 'Very responsible', 'I am a very funny fact', TRUE, '{"Informatics"}', - 'Universitarian', 3, '2022', 'Ghent University', 'APPLIED', '2022-03-14 23:10:00+01'); - - /* Insert data into evaluation table */ - INSERT INTO evaluation(login_user_id, job_application_id, decision, motivation, is_final)VALUES - ((SELECT login_user_id FROM login_user WHERE is_admin = TRUE AND person_id = 2), (SELECT job_application_id FROM job_application), - 'YES', 'Simply the best', TRUE); - -/* Insert data into role table */ -INSERT INTO role(name)VALUES('Developer'); - - /* Insert data into project table */ - INSERT INTO project(name, osoc_id, partner, start_date, end_date)VALUES('OSOC Platform', - (SELECT osoc_id FROM osoc WHERE year = 2022), 'UGent', DATE '2022-07-01', DATE '2022-08-15'); - -/* Insert data into project_user table */ -INSERT INTO project_user(login_user_id, project_id)VALUES((SELECT login_user_id FROM login_user WHERE is_admin = TRUE AND person_id = 2), -(SELECT project_id FROM project WHERE name = 'OSOC Platform')); - -/* Insert data into project_role table */ -INSERT INTO project_role(project_id, role_id, positions) VALUES((SELECT project_id FROM project WHERE name = 'OSOC Platform'), -(SELECT role_id FROM role WHERE name = 'Developer'), 2); - -/* Insert data into contract table */ -INSERT INTO contract(student_id, project_role_id, information, created_by_login_user_id, contract_status) VALUES -((SELECT student_id FROM student WHERE phone_number = '0032476553498'), -(SELECT project_role_id FROM project_role WHERE positions = 2), 'Developer contract for osoc platform', -(SELECT login_user_id FROM login_user WHERE is_admin = TRUE AND person_id = 2), 'DRAFT'); - -/* Insert data into applied_role table */ -INSERT INTO applied_role(job_application_id, role_id)VALUES -((SELECT job_application_id from job_application WHERE fun_fact = 'I am a very funny fact'), -(SELECT role_id FROM role WHERE name = 'Developer')); - -/* Insert data into language table */ -INSERT INTO language(name)VALUES('Dutch'); - -/* Insert data into job_application_skill table */ -INSERT INTO job_application_skill(job_application_id, skill, language_id, level, is_preferred, is_best) VALUES -((SELECT job_application_id from job_application WHERE fun_fact = 'I am a very funny fact'), 'Typing', -(SELECT language_id FROM language WHERE name = 'Dutch'), 2, TRUE, TRUE); - -/* Insert data into attachment table */ -INSERT INTO attachment(job_application_id, data, type)VALUES -((SELECT job_application_id from job_application WHERE fun_fact = 'I am a very funny fact'), -'{https://github.com/SELab-2/OSOC-2}', '{CV_URL}'); - -INSERT INTO attachment(job_application_id, data, type)VALUES -((SELECT job_application_id from job_application WHERE fun_fact = 'I am a very funny fact'), -'{I really need the money}', '{MOTIVATION_STRING}'); - -/* Insert into the login_user_osoc table */ -INSERT INTO login_user_osoc(login_user_id, osoc_id) VALUES ((SELECT login_user_id FROM login_user WHERE is_admin = TRUE AND person_id = 4), -(SELECT osoc_id FROM osoc WHERE year = 2022)); - -/* Insert data into template table */ -INSERT INTO template_email(owner_id, name, content)VALUES -((SELECT login_user_id FROM login_user WHERE is_admin = TRUE AND person_id = 2), 'Some Template', '

I am a template

'); -INSERT INTO template_email(owner_id, name, content, cc, subject)VALUES -((SELECT login_user_id FROM login_user WHERE is_admin = TRUE AND person_id = 2), 'Some Advanced Template', '

I am an advanced template

', 'nobody@me.com', 'A non-suspicious email!'); +VALUES((SELECT person_id FROM person WHERE email = 'osoc2@mail.com'), '$2b$08$MCblaKGOOBV7NpiW62GEc.km732o6XWDJxU6SfU3NMENxMuCWFlJq', TRUE, FALSE, 'ACTIVATED'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 86b2a1d6..5ebad4ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - '4096:4096' # 4096 is the port used by express, we map this to 4096 depends_on: - db - deploy: # TODO: maybe change the restart policy later? + deploy: restart_policy: condition: on-failure delay: 5s @@ -19,7 +19,7 @@ services: - '3000:3000' # 3000 is used by next, we keep using this one depends_on: - backend - deploy: # TODO: maybe change the restart policy later? + deploy: restart_policy: condition: on-failure delay: 5s @@ -31,7 +31,7 @@ services: - data:/var/lib/postgresql/data ports: - '5432:5432' - deploy: # TODO: maybe change the restart policy later? + deploy: restart_policy: condition: on-failure delay: 5s diff --git a/docs/User Manual.pdf b/docs/User Manual.pdf index 00a3d5c7..249e0203 100644 Binary files a/docs/User Manual.pdf and b/docs/User Manual.pdf differ diff --git a/frontend/__tests__/createProject.test.tsx b/frontend/__tests__/createProject.test.tsx new file mode 100644 index 00000000..b69d1768 --- /dev/null +++ b/frontend/__tests__/createProject.test.tsx @@ -0,0 +1,60 @@ +import fetchMock from "jest-fetch-mock"; +import { act, render, screen } from "@testing-library/react"; +import Create from "../pages/projects/create"; +import fireEvent from "@testing-library/user-event"; + +jest.mock("next/router", () => require("next-router-mock")); + +fetchMock.enableMocks(); +jest.mock("next/router"); + +const response = JSON.stringify({ + success: true, + data: [], + pagination: { count: 0 }, +}); + +describe("project create test", () => { + beforeEach(async () => { + fetchMock.resetMocks(); + fetchMock.mockOnce(response); + fetchMock.mockOnce(response); + fetchMock.mockOnce(response); + await act(() => { + render(); + }); + }); + + const testInput = async (inputName: string, inputVal: string) => { + await act(async () => { + await fireEvent.type(screen.getByTestId(inputName), inputVal); + }); + }; + const formatDate = () => { + const date = new Date(); + return [ + date.getFullYear(), + padTo2Digits(date.getMonth() + 1), + padTo2Digits(date.getDate()), + ].join("-"); + }; + const padTo2Digits = (num: number) => { + return num.toString().padStart(2, "0"); + }; + test("test inputs", async () => { + const input = "aaa"; + const date1 = formatDate(); + await testInput("nameInput", input); + await testInput("partnerInput", input); + await testInput("descriptionInput", input); + await act(async () => { + fetchMock.mockOnce(response); + await screen.getByTestId("confirmButton").click(); + }); + const lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe(`undefined/project`); + expect(fetchMock.mock.calls[lastLength][1]?.body).toBe( + `{"name":"${input}","partner":"${input}","start":"${date1}","end":"${date1}","osocId":0,"positions":0,"roles":{"roles":[]},"description":"${input}","coaches":{"coaches":[]}}` + ); + }); +}); diff --git a/frontend/__tests__/project.test.tsx b/frontend/__tests__/project.test.tsx new file mode 100644 index 00000000..82d97098 --- /dev/null +++ b/frontend/__tests__/project.test.tsx @@ -0,0 +1,166 @@ +import "@testing-library/jest-dom"; +import fetchMock from "jest-fetch-mock"; +import { act, render, screen } from "@testing-library/react"; +import fireEvent from "@testing-library/user-event"; +import Projects from "../pages/projects"; +import { defaultUser } from "../defaultUser"; +import { Project } from "../types"; +import { wrapWithTestBackend } from "react-dnd-test-utils"; + +jest.mock("next/router", () => require("next-router-mock")); + +fetchMock.enableMocks(); +jest.mock("next/router"); +const student = defaultUser; +const defaultProject: Project = { + coaches: [], + end_date: "", + id: -1, + name: "", + osoc_id: -1, + partner: "", + positions: -1, + start_date: "", + description: "", + contracts: [], + roles: [ + { + name: "DEV", + positions: 1, + }, + ], +}; + +const responseStudents = JSON.stringify({ + success: true, + data: [student], + pagination: { count: 0 }, +}); + +const responseProject = JSON.stringify({ + success: true, + data: [defaultProject], + pagination: { count: 0 }, +}); + +const response = JSON.stringify({ + success: true, + data: [], + pagination: { count: 0 }, +}); + +const pageSize = 5; +describe("project filter tests", () => { + beforeEach(async () => { + fetchMock.resetMocks(); + fetchMock.mockOnce(responseStudents); + fetchMock.mockOnce(response); + fetchMock.mockOnce(responseProject); + const [BoxContext] = wrapWithTestBackend(Projects); + + await act(() => { + render(); + }); + }); + + test("test filter inputs presents", async () => { + expect(screen.getByTestId("nameSort")).toBeInTheDocument(); + expect(screen.getByTestId("nameInput")).toBeInTheDocument(); + expect(screen.getByTestId("clientSort")).toBeInTheDocument(); + expect(screen.getByTestId("clientInput")).toBeInTheDocument(); + expect(screen.getByTestId("osocInput")).toBeInTheDocument(); + expect(screen.getByTestId("assignedButton")).toBeInTheDocument(); + expect( + screen.getByTestId("searchButtonProjectFilter") + ).toBeInTheDocument(); + }); + + const testSortAndInput = async ( + sort: string, + input: string, + sortReqVal: string, + inputReqVal: string + ) => { + await act(async () => { + fetchMock.mockOnce(response); + screen.getByTestId(sort).click(); + }); + let lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?${sortReqVal}=asc¤tPage=0&pageSize=${pageSize}` + ); + await act(async () => { + fetchMock.mockOnce(response); + screen.getByTestId(sort).click(); + }); + lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?${sortReqVal}=desc¤tPage=0&pageSize=${pageSize}` + ); + await act(async () => { + fetchMock.mockOnce(response); + screen.getByTestId(sort).click(); + }); + lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?currentPage=0&pageSize=${pageSize}` + ); + await testInput(input, inputReqVal); + }; + const testInput = async (input: string, inputReqVal: string) => { + let lastLength: number; + + const test_val = "testvalue"; + fetchMock.resetMocks(); + await act(async () => { + await fireEvent.type(screen.getByTestId(input), test_val); + fetchMock.mockOnce(response); + screen.getByTestId("searchButtonProjectFilter").click(); + }); + lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?${inputReqVal}=${test_val}¤tPage=0&pageSize=${pageSize}` + ); + await act(async () => { + await fireEvent.clear(screen.getByTestId(input)); + fetchMock.mockOnce(response); + screen.getByTestId("searchButtonProjectFilter").click(); + }); + lastLength = fetchMock.mock.calls.length - 1; + console.log(fetchMock.mock.calls); + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?currentPage=0&pageSize=${pageSize}` + ); + }; + test("test filters functionality", async () => { + await testSortAndInput( + "nameSort", + "nameInput", + "projectNameSort", + "projectNameFilter" + ); + await testSortAndInput( + "clientSort", + "clientInput", + "clientNameSort", + "clientNameFilter" + ); + await testInput("osocInput", "osocYear"); + await act(async () => { + await fireEvent.click(screen.getByTestId("assignedButton")); + fetchMock.mockOnce(response); + }); + let lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?fullyAssignedFilter=true¤tPage=0&pageSize=${pageSize}` + ); + await act(async () => { + await fireEvent.click(screen.getByTestId("assignedButton")); + fetchMock.mockOnce(response); + }); + lastLength = fetchMock.mock.calls.length - 1; + expect(fetchMock.mock.calls[lastLength][0]).toBe( + `undefined/project/filter?currentPage=0&pageSize=${pageSize}` + ); + }); +}); diff --git a/frontend/components/Filters/OsocFilter.tsx b/frontend/components/Filters/OsocFilter.tsx index cd8c0b02..5a013e3c 100644 --- a/frontend/components/Filters/OsocFilter.tsx +++ b/frontend/components/Filters/OsocFilter.tsx @@ -94,7 +94,25 @@ export const OsocCreateFilter: React.FC<{ console.log(err); }); if (response && response.success) { + // get the current userId. This is needed to notify the other users on the manage user screen. + // this loginUser id is used to notify which user year permissions need to be refetched there. + const currentUser = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/user/self`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `auth/osoc2 ${sessionKey}`, + }, + } + ) + .then((res) => res.json()) + .catch((err) => { + console.log(err); + }); socket.emit("osocCreated"); + socket.emit("yearPermissionUpdate", currentUser.login_user_id); const params: OsocFilterParams = { yearFilter: yearFilter, yearSort: yearSort, diff --git a/frontend/components/Filters/ProjectFilter.tsx b/frontend/components/Filters/ProjectFilter.tsx index eceeeb68..4c739b03 100644 --- a/frontend/components/Filters/ProjectFilter.tsx +++ b/frontend/components/Filters/ProjectFilter.tsx @@ -133,7 +133,7 @@ export const ProjectFilter: React.FC<{ return (
-
+
Project Name
-
+
Client
- +
); }; diff --git a/frontend/components/Filters/StudentFilter.tsx b/frontend/components/Filters/StudentFilter.tsx index 034bfe80..8eae12a6 100644 --- a/frontend/components/Filters/StudentFilter.tsx +++ b/frontend/components/Filters/StudentFilter.tsx @@ -18,6 +18,8 @@ import ExclamationIconColor from "../../public/images/exclamation_mark_color.png import ExclamationIcon from "../../public/images/exclamation_mark.png"; import ForbiddenIconColor from "../../public/images/forbidden_icon_color.png"; import ForbiddenIcon from "../../public/images/forbidden_icon.png"; +import CrossIconColor from "../../public/images/close_icon_red.png"; +import CrossIcon from "../../public/images/close_icon.png"; import { NotificationContext } from "../../contexts/notificationProvider"; export const StudentFilter: React.FC<{ @@ -287,6 +289,33 @@ export const StudentFilter: React.FC<{ searchManual(params); }; + const toggleFilterNone = async (e: SyntheticEvent) => { + e.preventDefault(); + let newVal; + if (statusFilter !== StudentStatus.NONE) { + newVal = StudentStatus.NONE; + } else { + newVal = StudentStatus.EMPTY; + } + setStatusFilter(newVal); + + setEmailStatusActive(false); + setRolesActive(false); + const params: StudentFilterParams = { + nameFilter: nameFilter, + emailFilter: emailFilter, + nameSort: nameSort, + emailSort: emailSort, + alumni: alumni, + studentCoach: studentCoach, + statusFilter: newVal, + osocYear: osocYear, + emailStatus: emailStatus, + selectedRoles: selectedRoles, + }; + searchManual(params); + }; + const toggleStatusApplied = async (e: SyntheticEvent) => { e.preventDefault(); @@ -806,9 +835,9 @@ export const StudentFilter: React.FC<{ ? CheckIconColor : CheckIcon } + alt="YesDecision" width={30} height={30} - alt={"Disabled"} onClick={toggleFilterYes} />

YES

@@ -823,9 +852,9 @@ export const StudentFilter: React.FC<{ ? ExclamationIconColor : ExclamationIcon } + alt="MaybeDecision" width={30} height={30} - alt={"Disabled"} onClick={toggleFilterMaybe} />

MAYBE

@@ -840,13 +869,29 @@ export const StudentFilter: React.FC<{ ? ForbiddenIconColor : ForbiddenIcon } + alt="NoDecision" width={30} height={30} - alt={"Disabled"} onClick={toggleFilterNo} />

NO

+ +
+ NoneDecision +

NONE

+
-
- - -
+ {isAdmin ? ( +
+ + +
+ ) : null}
); }; diff --git a/frontend/components/Projects/Projects.tsx b/frontend/components/Projects/Projects.tsx index 97851f3d..1051df9d 100644 --- a/frontend/components/Projects/Projects.tsx +++ b/frontend/components/Projects/Projects.tsx @@ -24,6 +24,7 @@ export const Projects: React.FC = () => { page: 0, count: 0, }); + const { isAdmin } = useContext(SessionContext); // 5 projects per page const pageSize = 5; @@ -107,7 +108,7 @@ export const Projects: React.FC = () => { : 0; setPagination({ page: currentPageInt, - count: 0, //TODO: what value should this be? I thought this would have to be currentPageInt * pageSize + 1 + count: 0, }); search(params, currentPageInt).then(); }; @@ -220,19 +221,22 @@ export const Projects: React.FC = () => { const navigator = (page: number) => { if (params !== undefined) { + window.scrollTo(0, 0); search(params, page).then(); } }; return (
- + {isAdmin ? ( + + ) : null} = {}; const navigator = (page: number) => { + setSelectedStudent(-1); + // get the current url and delete the id field + const paramsQuery = new URLSearchParams(window.location.search); + paramsQuery.delete("id"); + // push the url + router + .push(`${window.location.pathname}?${paramsQuery.toString()}`) + .then(); + if (params !== undefined) { + window.scrollTo(0, 0); search(params, page).then(); } }; @@ -269,7 +279,7 @@ export const Students: React.FC<{ : 0; setPagination({ page: currentPageInt, - count: 0, //TODO: what value should this be? I thought this would have to be currentPageInt * pageSize + 1 + count: 0, }); search(params, currentPageInt).then(); }; diff --git a/frontend/components/User/User.tsx b/frontend/components/User/User.tsx index 71ec5737..9ca7c4fc 100644 --- a/frontend/components/User/User.tsx +++ b/frontend/components/User/User.tsx @@ -5,7 +5,13 @@ import CoachIconColor from "../../public/images/coach_icon_color.png"; import CoachIcon from "../../public/images/coach_icon.png"; import ForbiddenIconColor from "../../public/images/forbidden_icon_color.png"; import ForbiddenIcon from "../../public/images/forbidden_icon.png"; -import React, { SyntheticEvent, useContext, useEffect, useState } from "react"; +import React, { + SyntheticEvent, + useCallback, + useContext, + useEffect, + useState, +} from "react"; import Image from "next/image"; import SessionContext from "../../contexts/sessionProvider"; import { @@ -38,7 +44,7 @@ export const User: React.FC<{ const userId = user.login_user_id; const [showDeleteModal, setShowDeleteModal] = useState(false); const { notify } = useContext(NotificationContext); - + const [hasYearPermListener, setYearPermListener] = useState(false); // a set of edition ids the user is allowed to see const [userEditions, setUserEditions] = useState>(new Set()); // dropdown open or closed @@ -51,6 +57,16 @@ export const User: React.FC<{ setStatus(user.account_status); }, [user]); + /** + * remove the listeners when dismounting the component + */ + useEffect(() => { + return () => { + socket.off("yearPermissionUpdated"); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { if (!isAdmin && !isCoach) { setStatus(() => AccountStatus.DISABLED); @@ -63,7 +79,7 @@ export const User: React.FC<{ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const fetchUserEditions = async () => { + const fetchUserEditions = useCallback(async () => { const { sessionKey } = getSession ? await getSession() : { sessionKey: "" }; @@ -85,7 +101,24 @@ export const User: React.FC<{ } setUserEditions(new Set(ids)); } - }; + }, [getSession, userId]); + + /** + * websocket listeners that update the visible years for a loginUser + * we use the state to check if there is already a listener in this User + * If there is no listener we register a new one + */ + useEffect(() => { + if (hasYearPermListener) { + return; + } + setYearPermListener(true); + socket.on("yearPermissionUpdated", (loginUserId: number) => { + if (user.login_user_id === loginUserId) { + fetchUserEditions().then(); + } + }); + }, [user, socket, fetchUserEditions, hasYearPermListener]); const setUserRole = async ( route: string, @@ -133,7 +166,7 @@ export const User: React.FC<{ socket.emit("updateRoleUser"); if (notify) { notify( - `Successfully updated ${name} authorities`, + `Successfully updated the permissions of ${name}`, NotificationType.SUCCESS, 2000 ); @@ -304,15 +337,19 @@ export const User: React.FC<{ Accept: "application/json", Authorization: `auth/osoc2 ${sessionKey}`, }, - }).catch((reason) => { - console.log(reason); - if (method === "POST") { - userEditions.delete(id); - } else { - userEditions.add(id); - } - setUserEditions(new Set(userEditions)); - }); + }) + .then(() => { + socket.emit("yearPermissionUpdate", user.login_user_id); + }) + .catch((reason) => { + console.log(reason); + if (method === "POST") { + userEditions.delete(id); + } else { + userEditions.add(id); + } + setUserEditions(new Set(userEditions)); + }); }; return ( diff --git a/frontend/contexts/notificationProvider.module.scss b/frontend/contexts/notificationProvider.module.scss index 43034209..d04a59a3 100644 --- a/frontend/contexts/notificationProvider.module.scss +++ b/frontend/contexts/notificationProvider.module.scss @@ -1,6 +1,6 @@ .notifications { position: fixed; - z-index: 1; + z-index: 2; bottom: 2%; right: 1rem; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 14628a16..844e24c1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "react": "18.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", + "react-dnd-test-utils": "^16.0.1", "react-dom": "18.1.0", "socket.io-client": "^4.5.1", "validator": "^13.7.0" @@ -35,6 +36,7 @@ "eslint-config-next": "12.1.6", "jest": "^28.1.0", "react-dnd-test-backend": "^16.0.1", + "react-dnd-test-utils": "^16.0.1", "sass": "^1.52.0", "typedoc": "^0.22.15", "typescript": "4.6.4" @@ -6439,6 +6441,40 @@ "dnd-core": "^16.0.1" } }, + "node_modules/react-dnd-test-utils": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-test-utils/-/react-dnd-test-utils-16.0.1.tgz", + "integrity": "sha512-hKt6HYUAuN1oVtpVIpHwxeZLqtjwfzK2wFyfQUs7Pbi6muxXK+ZDOHEUmaI3WcoWb0nhZOyX3+FwmBsrA288Eg==", + "dev": true, + "peerDependencies": { + "@testing-library/react": ">= 11", + "@types/node": "*", + "@types/react": ">= 16", + "@types/react-dom": ">= 16", + "react": ">= 16.14", + "react-dnd": ">= 11.1.3", + "react-dnd-html5-backend": ">= 11.1.3", + "react-dnd-test-backend": ">= 10.0.0", + "react-dom": ">= 16.14" + }, + "peerDependenciesMeta": { + "@testing-library/react": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dnd-html5-backend": { + "optional": true + }, + "react-dnd-test-backend": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", @@ -12346,6 +12382,13 @@ "dnd-core": "^16.0.1" } }, + "react-dnd-test-utils": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-test-utils/-/react-dnd-test-utils-16.0.1.tgz", + "integrity": "sha512-hKt6HYUAuN1oVtpVIpHwxeZLqtjwfzK2wFyfQUs7Pbi6muxXK+ZDOHEUmaI3WcoWb0nhZOyX3+FwmBsrA288Eg==", + "dev": true, + "requires": {} + }, "react-dom": { "version": "18.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b817ebcf..15b22236 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "react": "18.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", + "react-dnd-test-utils": "^16.0.1", "react-dom": "18.1.0", "socket.io-client": "^4.5.1", "validator": "^13.7.0" @@ -28,6 +29,7 @@ "devDependencies": { "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", + "react-dnd-test-utils": "^16.0.1", "@types/node": "17.0.35", "@types/react": "18.0.9", "@types/react-beautiful-dnd": "^13.1.2", diff --git a/frontend/pages/login/index.tsx b/frontend/pages/login/index.tsx index 56086beb..e6d69e76 100644 --- a/frontend/pages/login/index.tsx +++ b/frontend/pages/login/index.tsx @@ -205,6 +205,7 @@ const Index: NextPage = () => { error = true; } else if (!validator.default.isEmail(registerEmail)) { setRegisterEmailError("No valid email address"); + error = true; } else { setRegisterEmailError(""); } diff --git a/frontend/pages/osocs.tsx b/frontend/pages/osocs.tsx index 4f1f4f16..da290083 100644 --- a/frontend/pages/osocs.tsx +++ b/frontend/pages/osocs.tsx @@ -6,7 +6,7 @@ import { Sort, } from "../types"; import { NextPage } from "next"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useState } from "react"; import styles from "../styles/users.module.css"; import { OsocCreateFilter } from "../components/Filters/OsocFilter"; import SessionContext from "../contexts/sessionProvider"; @@ -44,20 +44,6 @@ const Osocs: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - socket.off("osocWasCreatedOrDeleted"); // remove old listener - - // add new listener - socket.on("osocWasCreatedOrDeleted", () => { - if (params !== undefined) { - const scrollPosition = window.scrollY; - search(params, pagination.page).then(() => - window.scrollTo(0, scrollPosition) - ); - } - }); - }); - const removeOsoc = (osoc: OsocEdition) => { if (osocEditions !== undefined) { const index = osocEditions.indexOf(osoc, 0); @@ -88,54 +74,71 @@ const Osocs: NextPage = () => { * @param params * @param page */ - const search = async (params: OsocFilterParams, page: number) => { - if (loading) return; - isLoading(true); - const filters = []; + const search = useCallback( + async (params: OsocFilterParams, page: number) => { + if (loading) return; + isLoading(true); + const filters = []; + + if (params.yearFilter !== "") { + filters.push(`yearFilter=${params.yearFilter}`); + } - if (params.yearFilter !== "") { - filters.push(`yearFilter=${params.yearFilter}`); - } + if (params.yearSort !== Sort.NONE) { + filters.push(`yearSort=${params.yearSort}`); + } - if (params.yearSort !== Sort.NONE) { - filters.push(`yearSort=${params.yearSort}`); - } + filters.push(`currentPage=${page}`); + filters.push(`pageSize=${pageSize}`); + + const query = filters.length > 0 ? `?${filters.join("&")}` : ""; + + const { sessionKey } = getSession + ? await getSession() + : { sessionKey: "" }; + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/osoc/filter` + query, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `auth/osoc2 ${sessionKey}`, + }, + } + ) + .then((response) => response.json()) + .catch((err) => { + console.log(err); + }); + if (response.data && response.pagination) { + setEditions(response.data); + setPagination(response.pagination); + } else if (!response.success && notify) { + notify( + "Something went wrong:" + response.reason, + NotificationType.ERROR, + 2000 + ); + } + isLoading(false); + }, + [getSession, loading, notify] + ); - filters.push(`currentPage=${page}`); - filters.push(`pageSize=${pageSize}`); - - const query = filters.length > 0 ? `?${filters.join("&")}` : ""; - - const { sessionKey } = getSession - ? await getSession() - : { sessionKey: "" }; - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/osoc/filter` + query, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `auth/osoc2 ${sessionKey}`, - }, + useEffect(() => { + socket.off("osocWasCreatedOrDeleted"); // remove old listener + + // add new listener + socket.on("osocWasCreatedOrDeleted", () => { + if (params !== undefined) { + const scrollPosition = window.scrollY; + search(params, pagination.page).then(() => + window.scrollTo(0, scrollPosition) + ); } - ) - .then((response) => response.json()) - .catch((err) => { - console.log(err); - }); - if (response.data && response.pagination) { - setEditions(response.data); - setPagination(response.pagination); - } else if (!response.success && notify) { - notify( - "Something went wrong:" + response.reason, - NotificationType.ERROR, - 2000 - ); - } - isLoading(false); - }; + }); + }, [params, getSession, socket, search, pagination.page]); return (
diff --git a/frontend/pages/projects/create.tsx b/frontend/pages/projects/create.tsx index 80c088dd..96db19f9 100644 --- a/frontend/pages/projects/create.tsx +++ b/frontend/pages/projects/create.tsx @@ -137,50 +137,48 @@ const Create: NextPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleConfirm = () => { - if (getSession !== undefined) { - getSession().then(async ({ sessionKey }) => { - if (sessionKey != "") { - const response = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/project`, - { - method: "POST", - headers: { - Authorization: `auth/osoc2 ${sessionKey}`, - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - name: projectName, - partner: partner, - start: startDate, - end: endDate, - osocId: osocId, - positions: getTotalPositions(), - roles: { roles: getRoleList() }, - description: description, - coaches: { coaches: selectedCoaches }, - }), - } - ) - .then((response) => response.json()) - .catch((error) => console.log(error)); - if (response !== undefined && response.success) { - if (notify) { - notify( - "Project succesfully created!", - NotificationType.SUCCESS, - 2000 - ); - } - socket.emit("projectCreated"); - } else if (notify && response !== null) { - notify(response.reason, NotificationType.ERROR, 5000); - } - router.push("/projects").then(); - } - }); + const handleConfirm = async () => { + const { sessionKey } = getSession + ? await getSession() + : { sessionKey: "" }; + + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/project`, + { + method: "POST", + headers: { + Authorization: `auth/osoc2 ${sessionKey}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + name: projectName, + partner: partner, + start: startDate, + end: endDate, + osocId: osocId, + positions: getTotalPositions(), + roles: { roles: getRoleList() }, + description: description, + coaches: { coaches: selectedCoaches }, + }), + } + ) + .then((response) => response.json()) + .catch((error) => console.log(error)); + if (response !== undefined && response.success) { + if (notify) { + notify( + "Project succesfully created!", + NotificationType.SUCCESS, + 2000 + ); + } + socket.emit("projectCreated"); + } else if (notify && response !== null) { + notify(response.reason, NotificationType.ERROR, 5000); } + router.push("/projects").then(); }; const getTotalPositions = () => { @@ -315,6 +313,7 @@ const Create: NextPage = () => {

- +
); }; diff --git a/frontend/pages/reset/[pid].tsx b/frontend/pages/reset/[pid].tsx index 2fda1bf0..61c52d7e 100644 --- a/frontend/pages/reset/[pid].tsx +++ b/frontend/pages/reset/[pid].tsx @@ -123,7 +123,6 @@ const Pid: NextPage = () => { .catch((error) => console.log(error)); if (response.success) { setBackendError(""); - // TODO -- Notification router.push("/login").then(); } else { setBackendError(response.reason); diff --git a/frontend/pages/users.tsx b/frontend/pages/users.tsx index e33e4aae..1a8886a8 100644 --- a/frontend/pages/users.tsx +++ b/frontend/pages/users.tsx @@ -25,7 +25,7 @@ import { NotificationContext } from "../contexts/notificationProvider"; const Users: NextPage = () => { const [users, setUsers] = useState>(); const [loading, isLoading] = useState(false); // Check if we are executing a request - const { getSession } = useContext(SessionContext); + const { getSession, sessionKey } = useContext(SessionContext); const router = useRouter(); const [pagination, setPagination] = useState({ page: 0, @@ -48,11 +48,15 @@ const Users: NextPage = () => { const { socket } = useSockets(); const { notify } = useContext(NotificationContext); + /** + * remove the listeners when dismounting the component + */ useEffect(() => { return () => { socket.off("loginUserUpdated"); socket.off("registrationReceived"); - }; // disconnect from the socket on dismount + socket.off("osocWasCreatedOrDeleted"); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -77,6 +81,7 @@ const Users: NextPage = () => { useEffect(() => { socket.off("loginUserUpdated"); // remove the earlier added listeners socket.off("registrationReceived"); + socket.off("osocWasCreatedOrDeleted"); // add new listener socket.on("loginUserUpdated", () => { const scrollPosition = window.scrollY; @@ -109,8 +114,15 @@ const Users: NextPage = () => { pagination.page ).then(() => window.scrollTo(0, scrollPosition)); }); + // when an osoc edition is deleted or removed this should be updated in the dropdown + socket.on("osocWasCreatedOrDeleted", () => { + const scrollPosition = window.scrollY; + fetchAllOsocEditions(sessionKey).then(() => + window.scrollTo(0, scrollPosition) + ); + }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [socket, searchParams]); + }, [socket, searchParams, pagination]); /** * Gets all osoc editions from the backend diff --git a/frontend/public/images/close_icon.png b/frontend/public/images/close_icon.png new file mode 100644 index 00000000..f477bbcd Binary files /dev/null and b/frontend/public/images/close_icon.png differ diff --git a/frontend/public/images/close_icon_red.png b/frontend/public/images/close_icon_red.png new file mode 100644 index 00000000..178f9eca Binary files /dev/null and b/frontend/public/images/close_icon_red.png differ diff --git a/frontend/types.ts b/frontend/types.ts index 5a80b950..737fa998 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -34,6 +34,7 @@ export enum StudentStatus { YES = "YES", MAYBE = "MAYBE", NO = "NO", + NONE = "NONE", } export enum ContractStatus { @@ -238,6 +239,7 @@ export interface ServerToClientEvents { projectWasCreatedOrDeleted: () => void; projectWasModified: (projectId: number) => void; osocWasCreatedOrDeleted: () => void; + yearPermissionUpdated: (loginUserId: number) => void; } /** @@ -256,6 +258,7 @@ export interface ClientToServerEvents { projectModified: (projectId: number) => void; osocDeleted: () => void; osocCreated: () => void; + yearPermissionUpdate: (loginUserId: number) => void; } export interface ProjectPerson { @@ -294,7 +297,7 @@ export interface Contract { } export interface Project { - coaches: [ProjectLoginUser]; + coaches: [ProjectLoginUser] | []; end_date: string; id: number; name: string; @@ -303,7 +306,7 @@ export interface Project { positions: number; start_date: string; description: string | null; - contracts: [Contract]; + contracts: [Contract] | []; roles: [ { name: string;