diff --git a/.env.example b/.env.example index 6e76e1d..d0facf6 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,13 @@ -DB_HOST="" -DB_PORT="" -DB_NAME="" -DB_USERNAME="" -DB_PASSWORD="" +DB_HOST="localhost" +DB_PORT="5432" +DB_NAME="study" +DB_USERNAME="postgres" +DB_PASSWORD="admin" -ACCESS_TOKEN_SECRET="" -PUBLIC_APP_URL="" +ACCESS_TOKEN_SECRET="arbitrary" +PUBLIC_APP_URL="http://localhost:3000" -NEXT_PUBLIC_SENTRY_DSN="" \ No newline at end of file +NEXT_PUBLIC_SENTRY_DSN="" + +#cid from app.code.berlin +LP_ACCESS_TOKEN="" diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index c61631a..f2bc256 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -3,9 +3,11 @@ import dayjs from "dayjs"; import { NextRequest, NextResponse } from "next/server"; import { AppDataSource, connectToDatabase } from "@/backend/datasource"; +import { CollaboratorRole } from "@/backend/entities/enums"; import { ModuleHandbook } from "@/backend/entities/moduleHandbook.entity"; import { Semester } from "@/backend/entities/semester.entity"; -import { StudyPlan } from "@/backend/entities/studyPlan.entity"; +import { StudyPlan, StudyPlanScope } from "@/backend/entities/studyPlan.entity"; +import { StudyPlanCollaborator } from "@/backend/entities/studyPlanCollaborator.entity"; import { User } from "@/backend/entities/user.entity"; import { issueAccessToken } from "@/backend/jwt"; import { isDefined } from "@/services/learningPlatform/util/isDefined"; @@ -75,20 +77,27 @@ export async function POST(req: NextRequest) { if (isSignup) { await AppDataSource.transaction(async (transaction) => { newUser = new User(); - newUser.lpId = learningPlatformUser.me.id; - const studyPlan = new StudyPlan(); + await transaction.getRepository(User).save(newUser); + const studyPlan = new StudyPlan(); studyPlan.moduleHandbookId = moduleHandbook.id; + studyPlan.subjectId = newUser.id; + studyPlan.scope = StudyPlanScope.Private; const newStudyPlan = await transaction .getRepository(StudyPlan) .save(studyPlan); - newUser.studyPlanId = newStudyPlan.id; + const studyPlanCollaborator = new StudyPlanCollaborator(); + studyPlanCollaborator.role = CollaboratorRole.Owner; + studyPlanCollaborator.studyPlanId = newStudyPlan.id; + studyPlanCollaborator.userId = newUser.id; - await transaction.getRepository(User).save(newUser); + await transaction + .getRepository(StudyPlanCollaborator) + .save(studyPlanCollaborator); const myStudiesData = await learningPlatform.raw.query( `query myStudies($filter: ModuleFilter) { diff --git a/app/api/learning-platform-proxy/route.ts b/app/api/learning-platform-proxy/route.ts index cb5ca70..5e7352a 100644 --- a/app/api/learning-platform-proxy/route.ts +++ b/app/api/learning-platform-proxy/route.ts @@ -34,9 +34,9 @@ export async function POST(req: NextRequest) { if (init?.body) { // @ts-ignore - console.log("fetched learning platform:", JSON.parse(init.body), data); + //console.log("fetched learning platform:", JSON.parse(init.body), data); } else { - console.log("fetched learning platform:", data); + //console.log("fetched learning platform:", data); } const res = NextResponse.json(data, { diff --git a/app/api/study-plan/[id]/collaborators/[collabId]/route.ts b/app/api/study-plan/[id]/collaborators/[collabId]/route.ts new file mode 100644 index 0000000..17ef97d --- /dev/null +++ b/app/api/study-plan/[id]/collaborators/[collabId]/route.ts @@ -0,0 +1,50 @@ +import { NextRequest } from "next/server"; + +import { + internalServerErrorResponse, + successResponse, + unauthorizedResponse, +} from "@/app/api/utils"; +import { StudyPlanCollaboratorPutDTO } from "@/backend/dtos/study-plan-collaborator.dto"; +import { + deleteCollaboratorById, + getCollaborator, + updateCollaboratorById, +} from "@/backend/queries/study-plan-collaborator.query"; + +export type CollaborationParams = { + params: { + id: string; + collabId: string; + }; +}; + +export async function DELETE( + req: NextRequest, + { params: { id: studyPlanId, collabId } }: CollaborationParams, +) { + const collaborator = await getCollaborator(req, studyPlanId); + + if (!collaborator?.canManageCollaborators) return unauthorizedResponse(); + + const deleteCollaborator = await deleteCollaboratorById(collabId); + if (!deleteCollaborator) return internalServerErrorResponse(); + + return successResponse(); +} + +export async function PUT( + req: NextRequest, + { params: { id: studyPlanId, collabId } }: CollaborationParams, +) { + const collaborator = await getCollaborator(req, studyPlanId); + + if (!collaborator?.canManageCollaborators) return unauthorizedResponse(); + + const body: StudyPlanCollaboratorPutDTO = await req.json(); + const updatedCollaborator = await updateCollaboratorById(collabId, body); + + if (!updatedCollaborator) return internalServerErrorResponse(); + + return successResponse(); +} diff --git a/app/api/study-plan/[id]/collaborators/invites/[inviteId]/accept/route.ts b/app/api/study-plan/[id]/collaborators/invites/[inviteId]/accept/route.ts new file mode 100644 index 0000000..ec96dc2 --- /dev/null +++ b/app/api/study-plan/[id]/collaborators/invites/[inviteId]/accept/route.ts @@ -0,0 +1,44 @@ +import { NextRequest } from "next/server"; + +import { + badRequestResponse, + successResponse, + unauthorizedResponse, +} from "@/app/api/utils"; +import { InviteStatus } from "@/backend/entities/invite.entity"; +import { getUser } from "@/backend/getUser"; +import { + getInviteById, + updateInviteStatus, +} from "@/backend/queries/invite.query"; +import { createStudyPlanCollaborator } from "@/backend/queries/study-plan-collaborator.query"; + +import { CollabParams } from "../utils"; + +export async function PUT( + req: NextRequest, + { params: { id, inviteId } }: CollabParams, +) { + const user = await getUser(req); + if (!user) return unauthorizedResponse(); + + const invite = await getInviteById(inviteId); + if (!invite) return badRequestResponse(); + + if (user.lpId !== invite.inviteeLpId) return unauthorizedResponse(); + + const updatedInvite = await updateInviteStatus( + inviteId, + InviteStatus.Accepted, + ); + if (!updatedInvite) return badRequestResponse(); + + const studyPlanCollaborator = await createStudyPlanCollaborator( + user.id, + id, + updatedInvite.role, + ); + if (!studyPlanCollaborator) return badRequestResponse(); + + return successResponse(); +} diff --git a/app/api/study-plan/[id]/collaborators/invites/[inviteId]/decline/route.ts b/app/api/study-plan/[id]/collaborators/invites/[inviteId]/decline/route.ts new file mode 100644 index 0000000..83b312c --- /dev/null +++ b/app/api/study-plan/[id]/collaborators/invites/[inviteId]/decline/route.ts @@ -0,0 +1,38 @@ +import { NextRequest } from "next/server"; + +import { + badRequestResponse, + successResponse, + unauthorizedResponse, +} from "@/app/api/utils"; +import { InviteStatus } from "@/backend/entities/invite.entity"; +import { getUser } from "@/backend/getUser"; +import { + getInviteById, + updateInviteStatus, +} from "@/backend/queries/invite.query"; + +import { CollabParams } from "../utils"; + +export async function PUT( + req: NextRequest, + { params: { inviteId } }: CollabParams, +) { + const user = await getUser(req); + + if (!user) return unauthorizedResponse(); + + const invite = await getInviteById(inviteId); + + if (!invite) return badRequestResponse(); + + if (user.lpId !== invite.inviteeLpId) return unauthorizedResponse(); + + const updatedInvite = await updateInviteStatus( + inviteId, + InviteStatus.Declined, + ); + if (!updatedInvite) return badRequestResponse(); + + return successResponse(); +} diff --git a/app/api/study-plan/[id]/collaborators/invites/[inviteId]/utils.ts b/app/api/study-plan/[id]/collaborators/invites/[inviteId]/utils.ts new file mode 100644 index 0000000..b8ea095 --- /dev/null +++ b/app/api/study-plan/[id]/collaborators/invites/[inviteId]/utils.ts @@ -0,0 +1,6 @@ +export type CollabParams = { + params: { + id: string; + inviteId: string; + }; +}; diff --git a/app/api/study-plan/[id]/collaborators/invites/route.ts b/app/api/study-plan/[id]/collaborators/invites/route.ts new file mode 100644 index 0000000..89d8ff0 --- /dev/null +++ b/app/api/study-plan/[id]/collaborators/invites/route.ts @@ -0,0 +1,30 @@ +import { NextRequest } from "next/server"; + +import { + badRequestResponse, + StudyPlanParams, + successResponse, + unauthorizedResponse, +} from "@/app/api/utils"; +import { InvitePostDTO } from "@/backend/dtos/invite.dto"; +import { createInvite } from "@/backend/queries/invite.query"; +import { getCollaborator } from "@/backend/queries/study-plan-collaborator.query"; + +export async function POST(req: NextRequest, { params }: StudyPlanParams) { + const collaborator = await getCollaborator(req, params.id); + + if (!collaborator?.canManageCollaborators) return unauthorizedResponse(); + + const { inviteeLpId, role }: InvitePostDTO = await req.json(); + + const invite = await createInvite({ + invitedById: collaborator.id, + studyPlanId: params.id, + inviteeLpId, + role, + }); + + if (!invite) return badRequestResponse(); + + return successResponse(); +} diff --git a/app/api/study-plan/[id]/collaborators/route.ts b/app/api/study-plan/[id]/collaborators/route.ts new file mode 100644 index 0000000..e17c006 --- /dev/null +++ b/app/api/study-plan/[id]/collaborators/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { StudyPlanParams, unauthorizedResponse } from "@/app/api/utils"; +import { StudyPlanCollaboratorDTO } from "@/backend/dtos/study-plan-collaborator.dto"; +import { + getAllCollaboratorsByStudyPlanId, + getCollaborator, +} from "@/backend/queries/study-plan-collaborator.query"; + +export async function DELETE(req: NextRequest) {} +export async function GET(req: NextRequest, { params }: StudyPlanParams) { + const collaborator = await getCollaborator(req, params.id); + + const studyPlanCollaborators = await getAllCollaboratorsByStudyPlanId( + params.id, + ); + if (!collaborator?.canViewCollaborators) return unauthorizedResponse(); + + const collabs: StudyPlanCollaboratorDTO[] = studyPlanCollaborators; + + return NextResponse.json(collabs); +} diff --git a/app/api/study-plan/[id]/route.ts b/app/api/study-plan/[id]/route.ts new file mode 100644 index 0000000..dd9ff31 --- /dev/null +++ b/app/api/study-plan/[id]/route.ts @@ -0,0 +1,107 @@ +import dayjs from "dayjs"; +import { NextRequest, NextResponse } from "next/server"; + +import { SemesterDTO } from "@/backend/dtos/semester.dto"; +import { StudyPlanDTO, StudyPlanPutDTO } from "@/backend/dtos/study-plan.dto"; +import { Semester } from "@/backend/entities/semester.entity"; +import { SemesterModule } from "@/backend/entities/semesterModule.entity"; +import { getSemesterByStudyPlanId } from "@/backend/queries/semester.query"; +import { getCollaborator } from "@/backend/queries/study-plan-collaborator.query"; +import { + getStudyPlanByCollaboratorId, + updateStudyPlanScopeByCollabId, +} from "@/backend/queries/study-plan.query"; + +import { + internalServerErrorResponse, + StudyPlanParams, + successResponse, + unauthorizedResponse, +} from "../../utils"; + +const byIndex = (a: SemesterModule, b: SemesterModule) => a.index - b.index; + +const toModule = (module: SemesterModule) => ({ moduleId: module.module.lpId }); + +const mapSemster = (semesters: Semester[]): SemesterDTO[] => { + return semesters + .toSorted((a, b) => dayjs(a.startDate).unix() - dayjs(b.startDate).unix()) + .map((semester) => { + return { + id: semester.id, + lpId: semester.lpId, + startDate: semester.startDate, + modules: { + earlyAssessments: semester.semesterModules + .filter((module) => module.assessmentType === "earlyAssessments") + .sort(byIndex) + .map(toModule), + standardAssessments: semester.semesterModules + .filter((module) => module.assessmentType === "standardAssessments") + .sort(byIndex) + .map(toModule), + alternativeAssessments: semester.semesterModules + .filter( + (module) => module.assessmentType === "alternativeAssessments", + ) + .sort(byIndex) + .map(toModule), + reassessments: semester.semesterModules + .filter((module) => module.assessmentType === "reassessments") + .sort(byIndex) + .map(toModule), + }, + }; + }); +}; + +export async function GET(req: NextRequest, { params }: StudyPlanParams) { + const collaborator = await getCollaborator(req, params.id); + + if (!collaborator?.canViewStudyPlan) { + return unauthorizedResponse(); + } + + const currentStudyPlan = await getStudyPlanByCollaboratorId(collaborator.id); + + if (!currentStudyPlan) { + return unauthorizedResponse(); + } + + /* + TODO: maybe add error if list is empty or return something currently if find function errors returns empty list + Prev. on error it would automatically return response 500 rn this wouldnt happen + */ + const semesters = await getSemesterByStudyPlanId(currentStudyPlan.id); + + const mappedSemesters = mapSemster(semesters); + + const studyPlan: StudyPlanDTO = { + scope: currentStudyPlan.scope, + studyPlanCollaborators: currentStudyPlan.studyPlanCollaborators, + semesters: mappedSemesters, + }; + + return NextResponse.json(studyPlan); +} + +/** + * Sucessfull response for PUT/POST/DELETE is {ok: true} + */ +export async function PUT(req: NextRequest, { params }: StudyPlanParams) { + const collaborator = await getCollaborator(req, params.id); + + if (!collaborator?.canChangeStudyPlanScope) return unauthorizedResponse(); + + const body: StudyPlanPutDTO = await req.json(); + + const updatePlan = await updateStudyPlanScopeByCollabId( + collaborator.id, + body, + ); + + // TODO: Think about better error handling lol + if (!updatePlan) return internalServerErrorResponse(); + + return successResponse(); +} diff --git a/app/api/study-plan/semester-modules/batch-update/route.ts b/app/api/study-plan/[id]/semester-modules/batch-update/route.ts similarity index 86% rename from app/api/study-plan/semester-modules/batch-update/route.ts rename to app/api/study-plan/[id]/semester-modules/batch-update/route.ts index cb5233e..6d0f37f 100644 --- a/app/api/study-plan/semester-modules/batch-update/route.ts +++ b/app/api/study-plan/[id]/semester-modules/batch-update/route.ts @@ -1,6 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; +import { + badRequestResponse, + internalServerErrorResponse, + StudyPlanParams, + successResponse, + unauthorizedResponse, +} from "@/app/api/utils"; import { AppDataSource, connectToDatabase } from "@/backend/datasource"; import { Module } from "@/backend/entities/module.entity"; import { Semester } from "@/backend/entities/semester.entity"; @@ -8,16 +15,18 @@ import { AssessmentType, SemesterModule, } from "@/backend/entities/semesterModule.entity"; -import { getUser } from "@/backend/getUser"; +import { getCollaborator } from "@/backend/queries/study-plan-collaborator.query"; /** * note: both `semesterId` and `moduleId` are learning platform ids, not the ids in our database */ -export async function PUT(req: NextRequest) { - const user = await getUser(req); +export async function PUT(req: NextRequest, { params }: StudyPlanParams) { + console.log("params", params); + + const collaborator = await getCollaborator(req, params.id); - if (!user) { - return NextResponse.json({}, { status: 401 }); + if (!collaborator?.canModifyStudyPlan) { + return unauthorizedResponse(); } const moduleSchema = z.object({ @@ -35,7 +44,7 @@ export async function PUT(req: NextRequest) { try { body = bodySchema.parse(await req.json()); } catch (err) { - return NextResponse.json({}, { status: 400 }); + return badRequestResponse(); } try { await connectToDatabase(); @@ -48,7 +57,7 @@ export async function PUT(req: NextRequest) { "semester.id IN (:...semesterIds) and semester.studyPlanId = :studyPlanId", { semesterIds, - studyPlanId: user.studyPlanId, + studyPlanId: collaborator.studyPlanId, }, ) .getCount(); @@ -71,7 +80,7 @@ export async function PUT(req: NextRequest) { .where( "semester.studyPlanId = :studyPlanId AND semesterModule.semesterId IN (:...semesterIds)", { - studyPlanId: user.studyPlanId, + studyPlanId: collaborator.studyPlanId, semesterIds, }, ) @@ -144,9 +153,7 @@ export async function PUT(req: NextRequest) { } }); - const res = NextResponse.json({}); - - return res; + return successResponse(); } catch (err) { const isTypeormConstraintViolation = (err as Record).code === "23505"; @@ -162,6 +169,6 @@ export async function PUT(req: NextRequest) { console.error(err); - return NextResponse.json({}, { status: 500 }); + return internalServerErrorResponse(); } } diff --git a/app/api/study-plan/route.ts b/app/api/study-plan/route.ts deleted file mode 100644 index f65f4f3..0000000 --- a/app/api/study-plan/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import dayjs from "dayjs"; -import { NextRequest, NextResponse } from "next/server"; - -import { AppDataSource } from "@/backend/datasource"; -import { Semester } from "@/backend/entities/semester.entity"; -import { SemesterModule } from "@/backend/entities/semesterModule.entity"; -import { getUser } from "@/backend/getUser"; - -const byIndex = (a: SemesterModule, b: SemesterModule) => a.index - b.index; - -const toModule = (module: SemesterModule) => ({ moduleId: module.module.lpId }); - -export async function GET(req: NextRequest) { - const user = await getUser(req); - - if (!user) { - return NextResponse.json({}, { status: 401 }); - } - - const semesterRepository = AppDataSource.getRepository(Semester); - - const semesters = await semesterRepository.find({ - where: { - studyPlan: { - user: { - id: user.id, - }, - }, - }, - relations: ["semesterModules", "semesterModules.module"], - }); - - const mappedSemesters = semesters - .toSorted((a, b) => dayjs(a.startDate).unix() - dayjs(b.startDate).unix()) - .map((semester) => { - return { - id: semester.id, - lpId: semester.lpId, - startDate: semester.startDate, - modules: { - earlyAssessments: semester.semesterModules - .filter((module) => module.assessmentType === "earlyAssessments") - .sort(byIndex) - .map(toModule), - standardAssessments: semester.semesterModules - .filter((module) => module.assessmentType === "standardAssessments") - .sort(byIndex) - .map(toModule), - alternativeAssessments: semester.semesterModules - .filter( - (module) => module.assessmentType === "alternativeAssessments", - ) - .sort(byIndex) - .map(toModule), - reassessments: semester.semesterModules - .filter((module) => module.assessmentType === "reassessments") - .sort(byIndex) - .map(toModule), - }, - }; - }); - - const res = NextResponse.json({ - semesters: mappedSemesters, - }); - return res; -} diff --git a/app/api/utils.ts b/app/api/utils.ts new file mode 100644 index 0000000..ea20e50 --- /dev/null +++ b/app/api/utils.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; + +export const successResponse = () => NextResponse.json({ ok: true }); +export const badRequestResponse = () => + NextResponse.json({ ok: false }, { status: 400 }); +export const unauthorizedResponse = () => + NextResponse.json({ ok: false }, { status: 401 }); +export const internalServerErrorResponse = () => + NextResponse.json({ ok: false }, { status: 500 }); + +export type StudyPlanParams = { + params: { + id: string; + }; +}; diff --git a/backend/dtos/compulsory-elective-pairing.dto.ts b/backend/dtos/compulsory-elective-pairing.dto.ts new file mode 100644 index 0000000..9645b80 --- /dev/null +++ b/backend/dtos/compulsory-elective-pairing.dto.ts @@ -0,0 +1,10 @@ +import { CompulsoryElectivePairing } from "../entities/compulsoryElectivePairing.entity"; +import { ModuleDTO } from "./module.dto"; + +export interface CompulsoryElectivePairingDTO + extends Omit< + CompulsoryElectivePairing, + "createdAt" | "updatedAt" | "moduleHandbook" + > { + Modules: ModuleDTO[]; +} diff --git a/backend/dtos/invite.dto.ts b/backend/dtos/invite.dto.ts new file mode 100644 index 0000000..0878baa --- /dev/null +++ b/backend/dtos/invite.dto.ts @@ -0,0 +1,10 @@ +import { CollaboratorRole } from "../entities/enums"; +import { Invite } from "../entities/invite.entity"; + +export interface InviteDTO + extends Omit {} + +export type InvitePostDTO = { + inviteeLpId: string; + role: CollaboratorRole; +}; diff --git a/backend/dtos/module.dto.ts b/backend/dtos/module.dto.ts new file mode 100644 index 0000000..2cde3e4 --- /dev/null +++ b/backend/dtos/module.dto.ts @@ -0,0 +1,7 @@ +import { Module } from "../entities/module.entity"; + +export interface ModuleDTO + extends Omit< + Module, + "createdAt" | "updatedAt" | "compulsoryElectivePairings" + > {} diff --git a/backend/dtos/semester-module.dto.ts b/backend/dtos/semester-module.dto.ts new file mode 100644 index 0000000..eafa965 --- /dev/null +++ b/backend/dtos/semester-module.dto.ts @@ -0,0 +1,17 @@ +import { SemesterModule } from "../entities/semesterModule.entity"; +import { ModulesRecordDTO } from "./semester.dto"; + +export interface SemesterModuleDTO + extends Omit< + SemesterModule, + | "id" + | "createdAt" + | "updatedAt" + | "semester" + | "semesterId" + | "module" + | "assessmentType" + | "index" + > {} + +export type SemesterModulePutDTO = Record; diff --git a/backend/dtos/semester.dto.ts b/backend/dtos/semester.dto.ts new file mode 100644 index 0000000..adaec93 --- /dev/null +++ b/backend/dtos/semester.dto.ts @@ -0,0 +1,23 @@ +import { Semester } from "../entities/semester.entity"; +import { SemesterModuleDTO } from "./semester-module.dto"; + +export interface SemesterDTO + extends Omit< + Semester, + | "createdAt" + | "updatedAt" + | "studyPlan" + | "studyPlanId" + | "semesterModules" + | "lpId" + > { + modules: ModulesRecordDTO; + lpId: string | null; +} + +export interface ModulesRecordDTO { + earlyAssessments: SemesterModuleDTO[]; + standardAssessments: SemesterModuleDTO[]; + alternativeAssessments: SemesterModuleDTO[]; + reassessments: SemesterModuleDTO[]; +} diff --git a/backend/dtos/study-plan-collaborator.dto.ts b/backend/dtos/study-plan-collaborator.dto.ts new file mode 100644 index 0000000..1da9992 --- /dev/null +++ b/backend/dtos/study-plan-collaborator.dto.ts @@ -0,0 +1,13 @@ +import { CollaboratorRole } from "../entities/enums"; +import { StudyPlanCollaborator } from "../entities/studyPlanCollaborator.entity"; +import { UserDTO } from "./user.dto"; + +export interface StudyPlanCollaboratorDTO + extends Omit< + StudyPlanCollaborator, + "createdAt" | "updatedAt" | "studyPlan" | "user" + > {} + +export type StudyPlanCollaboratorPutDTO = { + role: CollaboratorRole; +}; diff --git a/backend/dtos/study-plan.dto.ts b/backend/dtos/study-plan.dto.ts new file mode 100644 index 0000000..c5b2262 --- /dev/null +++ b/backend/dtos/study-plan.dto.ts @@ -0,0 +1,24 @@ +import { StudyPlan, StudyPlanScope } from "../entities/studyPlan.entity"; +import { SemesterDTO } from "./semester.dto"; +import { StudyPlanCollaboratorDTO } from "./study-plan-collaborator.dto"; + +export interface StudyPlanDTO + extends Omit< + StudyPlan, + | "id" + | "createdAt" + | "updatedAt" + | "moduleHandbook" + | "moduleHandbookId" + | "semesters" + | "studyPlanCollaborators" + | "subject" + | "subjectId" + > { + semesters: SemesterDTO[]; + studyPlanCollaborators: StudyPlanCollaboratorDTO[]; +} + +export type StudyPlanPutDTO = { + scope: StudyPlanScope; +}; diff --git a/backend/dtos/user.dto.ts b/backend/dtos/user.dto.ts new file mode 100644 index 0000000..f75bde9 --- /dev/null +++ b/backend/dtos/user.dto.ts @@ -0,0 +1,18 @@ +import { User } from "../entities/user.entity"; +import { InviteDTO } from "./invite.dto"; +import { StudyPlanCollaboratorDTO } from "./study-plan-collaborator.dto"; +import { StudyPlanDTO } from "./study-plan.dto"; + +export interface UserDTO + extends Omit< + User, + | "createdAt" + | "updatedAt" + | "studyPlanCollaborators" + | "studyPlans" + | "invites" + > { + studyPlanCollaborators: StudyPlanCollaboratorDTO[]; + studyPlans: StudyPlanDTO[]; + invites: InviteDTO[]; +} diff --git a/backend/entities/enums.ts b/backend/entities/enums.ts new file mode 100644 index 0000000..b7698c2 --- /dev/null +++ b/backend/entities/enums.ts @@ -0,0 +1,6 @@ +export enum CollaboratorRole { + Viewer = "viewer", // can't edit, can't invite and remove collaborators + Editor = "editor", // can edit, can't invite and remove collaborators + Admin = "amdin", // can edit, can invite and remove collaborators + Owner = "owner", // can edit, can invite and remove collaborators, can't be removed, this role can't be assigend, only one who can delete the plan +} diff --git a/backend/entities/invite.entity.ts b/backend/entities/invite.entity.ts new file mode 100644 index 0000000..9608d7e --- /dev/null +++ b/backend/entities/invite.entity.ts @@ -0,0 +1,62 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, + UpdateDateColumn, +} from "typeorm"; + +import { CollaboratorRole } from "./enums"; +import { StudyPlan } from "./studyPlan.entity"; +import "./studyPlanCollaborator.entity"; +import { User } from "./user.entity"; + +export enum InviteStatus { + Pending = "pending", + Declined = "declined", + Accepted = "accepted", +} + +@Entity({ name: "invites" }) +export class Invite { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @CreateDateColumn({ select: false }) + createdAt!: Date; + + @UpdateDateColumn({ select: false }) + updatedAt!: Date; + + @Column({ + comment: + "the id of the corresponding resource of the CODE learning platform.", + }) + inviteeLpId!: string; + + @Column({ + type: "enum", + enum: CollaboratorRole, + }) + role!: CollaboratorRole; + + @Column({ type: "enum", enum: InviteStatus, default: InviteStatus.Pending }) + status!: InviteStatus; + + @Column() + studyPlanId!: string; + + @ManyToOne(() => StudyPlan, (studyPlan) => studyPlan.studyPlanCollaborators) + @JoinColumn({ name: "studyPlanId" }) + studyPlan!: Relation; + + @Column() + invitedById!: string; + + @ManyToOne(() => User, (user) => user.studyPlanCollaborators) + @JoinColumn({ name: "userId" }) + invitedBy!: Relation; +} diff --git a/backend/entities/studyPlan.entity.ts b/backend/entities/studyPlan.entity.ts index a6e2cf1..c0c44c6 100644 --- a/backend/entities/studyPlan.entity.ts +++ b/backend/entities/studyPlan.entity.ts @@ -5,7 +5,6 @@ import { JoinColumn, ManyToOne, OneToMany, - OneToOne, PrimaryGeneratedColumn, type Relation, UpdateDateColumn, @@ -13,8 +12,18 @@ import { import { ModuleHandbook } from "./moduleHandbook.entity"; import { Semester } from "./semester.entity"; +import { StudyPlanCollaborator } from "./studyPlanCollaborator.entity"; import { User } from "./user.entity"; +export enum StudyPlanScope { + Public = "public", + Faculty = "facultyOnly", + Private = "private", +} + +/** + * @param subjectId refers to the user which the study plan is about + */ @Entity({ name: "study_plans" }) export class StudyPlan { @PrimaryGeneratedColumn("uuid") @@ -26,18 +35,38 @@ export class StudyPlan { @UpdateDateColumn({ select: false }) updatedAt!: Date; - @OneToOne(() => User, (user) => user.studyPlan) - user!: Relation; + @Column({ + type: "enum", + enum: StudyPlanScope, + default: StudyPlanScope.Private, + }) + scope!: StudyPlanScope; @OneToMany(() => Semester, (semester) => semester.studyPlan, { cascade: ["remove"], }) semesters!: Relation[]; + @OneToMany( + () => StudyPlanCollaborator, + (studyPlanCollaborator) => studyPlanCollaborator.studyPlan, + { + cascade: ["remove"], + }, + ) + studyPlanCollaborators!: Relation[]; + @Column() moduleHandbookId!: string; @ManyToOne(() => ModuleHandbook, (handbook) => handbook.studyPlans) @JoinColumn({ name: "moduleHandbookId" }) moduleHandbook!: Relation; + + @Column() + subjectId!: string; + + @ManyToOne(() => User, (user) => user.studyPlans) + @JoinColumn({ name: "subjectId" }) + subject!: Relation; } diff --git a/backend/entities/studyPlanCollaborator.entity.ts b/backend/entities/studyPlanCollaborator.entity.ts new file mode 100644 index 0000000..47fb8ae --- /dev/null +++ b/backend/entities/studyPlanCollaborator.entity.ts @@ -0,0 +1,79 @@ +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, + Unique, + UpdateDateColumn, +} from "typeorm"; + +import { CollaboratorRole } from "./enums"; +import { StudyPlan } from "./studyPlan.entity"; +import { User } from "./user.entity"; + +@Entity({ name: "study_plan_collaborators" }) +@Unique(["studyPlanId", "userId"]) +export class StudyPlanCollaborator { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @CreateDateColumn({ select: false }) + createdAt!: Date; + + @UpdateDateColumn({ select: false }) + updatedAt!: Date; + + @Column({ + type: "enum", + enum: CollaboratorRole, + }) + role!: CollaboratorRole; + + @Column() + studyPlanId!: string; + + @ManyToOne(() => StudyPlan, (studyPlan) => studyPlan.studyPlanCollaborators) + @JoinColumn({ name: "studyPlanId" }) + studyPlan!: Relation; + + @Column() + userId!: string; + + @ManyToOne(() => User, (user) => user.studyPlanCollaborators) + @JoinColumn({ name: "userId" }) + user!: Relation; + + private get isViewer() { + return this.role === CollaboratorRole.Viewer; + } + private get isEditor() { + return this.role === CollaboratorRole.Editor; + } + private get isAdmin() { + return this.role === CollaboratorRole.Admin; + } + private get isOwner() { + return this.role === CollaboratorRole.Owner; + } + + public get canViewStudyPlan() { + return true; + } + public get canModifyStudyPlan() { + return this.isEditor || this.isAdmin || this.isOwner; + } + + public get canViewCollaborators() { + return true; + } + public get canManageCollaborators() { + return this.isAdmin || this.isOwner; + } + + public get canChangeStudyPlanScope() { + return this.isOwner; + } +} diff --git a/backend/entities/user.entity.ts b/backend/entities/user.entity.ts index 8552ffd..73a5165 100644 --- a/backend/entities/user.entity.ts +++ b/backend/entities/user.entity.ts @@ -2,16 +2,19 @@ import { Column, CreateDateColumn, Entity, - JoinColumn, - OneToOne, + OneToMany, PrimaryGeneratedColumn, type Relation, + Unique, UpdateDateColumn, } from "typeorm"; +import { Invite } from "./invite.entity"; import { StudyPlan } from "./studyPlan.entity"; +import { StudyPlanCollaborator } from "./studyPlanCollaborator.entity"; @Entity({ name: "users" }) +@Unique(["lpId"]) export class User { @PrimaryGeneratedColumn("uuid") id!: string; @@ -30,12 +33,27 @@ export class User { }) lpId!: string; - @Column() - studyPlanId!: string; - - @OneToOne(() => StudyPlan, (studyPlan) => studyPlan.user, { + @OneToMany( + () => StudyPlanCollaborator, + (studyPlanCollaborator) => studyPlanCollaborator.user, + { + cascade: ["remove"], + }, + ) + studyPlanCollaborators!: Relation[]; + + @OneToMany( + () => StudyPlan, + (studyPlan) => studyPlan.subject, + + { + cascade: ["remove"], + }, + ) + studyPlans!: Relation[]; + + @OneToMany(() => Invite, (invite) => invite.invitedBy, { cascade: ["remove"], }) - @JoinColumn({ name: "studyPlanId" }) - studyPlan!: Relation; + invites!: Relation[]; } diff --git a/backend/queries/invite.query.ts b/backend/queries/invite.query.ts new file mode 100644 index 0000000..a3351cf --- /dev/null +++ b/backend/queries/invite.query.ts @@ -0,0 +1,70 @@ +import { AppDataSource } from "../datasource"; +import { CollaboratorRole } from "../entities/enums"; +import { Invite, InviteStatus } from "../entities/invite.entity"; + +export type CreateInvite = { + invitedById: string; + studyPlanId: string; + inviteeLpId: string; + role: CollaboratorRole; +}; + +export const createInvite = async ( + createInviteBody: CreateInvite, +): Promise => { + try { + const inviteRepository = AppDataSource.getRepository(Invite); + + const invite = inviteRepository.create(createInviteBody); + return await inviteRepository.save(invite); + } catch (error) { + console.error("createInvite: ", error); + + return null; + } +}; + +export const getInviteById = async ( + inviteId: string, +): Promise => { + try { + const inviteRepository = AppDataSource.getRepository(Invite); + + return await inviteRepository.findOne({ + where: { + id: inviteId, + }, + }); + } catch (error) { + console.error("createInvite: ", error); + + return null; + } +}; + +export const updateInviteStatus = async ( + inviteId: string, + status: InviteStatus, +): Promise => { + try { + const inviteRepository = AppDataSource.getRepository(Invite); + + const invite = await inviteRepository.findOne({ + where: { + id: inviteId, + }, + }); + + if (!invite) return null; + + invite.status = status; + + await inviteRepository.save(invite); + + return invite; + } catch (error) { + console.error("createInvite: ", error); + + return null; + } +}; diff --git a/backend/queries/semester.query.ts b/backend/queries/semester.query.ts new file mode 100644 index 0000000..ce78163 --- /dev/null +++ b/backend/queries/semester.query.ts @@ -0,0 +1,21 @@ +import { AppDataSource } from "../datasource"; +import { Semester } from "../entities/semester.entity"; + +export async function getSemesterByStudyPlanId(studyPlanId: string) { + try { + const semesterRepository = AppDataSource.getRepository(Semester); + + return await semesterRepository.find({ + where: { + studyPlan: { + id: studyPlanId, + }, + }, + relations: ["semesterModules", "semesterModules.module"], + }); + } catch (error) { + console.error("GetSemesterByCollab: ", error); + + return []; + } +} diff --git a/backend/queries/study-plan-collaborator.query.ts b/backend/queries/study-plan-collaborator.query.ts new file mode 100644 index 0000000..71498d7 --- /dev/null +++ b/backend/queries/study-plan-collaborator.query.ts @@ -0,0 +1,128 @@ +import { NextRequest } from "next/server"; + +import { AppDataSource } from "../datasource"; +import { StudyPlanCollaboratorPutDTO } from "../dtos/study-plan-collaborator.dto"; +import { CollaboratorRole } from "../entities/enums"; +import { StudyPlanCollaborator } from "../entities/studyPlanCollaborator.entity"; +import { getUser } from "../getUser"; + +const WIP_EXPERIMENTAL_STUDY_PLAN_ID_ENFORCEMENT = false; + +export async function getAllCollaboratorOwnerByUserId( + userId: string, +): Promise { + try { + const collaboratorRepository = AppDataSource.getRepository( + StudyPlanCollaborator, + ); + + return await collaboratorRepository.find({ + where: { + role: CollaboratorRole.Owner, + user: { + id: userId, + }, + }, + }); + } catch (error) { + return []; + } +} + +export async function getAllCollaboratorsByStudyPlanId( + studyPlanId: string, +): Promise { + try { + const collaboratorRepository = AppDataSource.getRepository( + StudyPlanCollaborator, + ); + + return await collaboratorRepository.find({ + where: { + studyPlanId, + }, + }); + } catch (error) { + return []; + } +} + +export const createStudyPlanCollaborator = async ( + userId: string, + studyPlanId: string, + role: CollaboratorRole, +): Promise => { + // returns null because it is not allowed to set a new owner + if (role === CollaboratorRole.Owner) return null; + + try { + const collaboratorRepository = AppDataSource.getRepository( + StudyPlanCollaborator, + ); + + const collaborator = collaboratorRepository.create({ + role, + userId, + studyPlanId, + }); + return await collaboratorRepository.save(collaborator); + } catch (error) { + console.error("createStudyPlanCollaborator error: ", error); + return null; + } +}; + +export const deleteCollaboratorById = async (collabId: string) => { + try { + const collaboratorRepository = AppDataSource.getRepository( + StudyPlanCollaborator, + ); + + return await collaboratorRepository.delete({ + id: collabId, + }); + } catch (error) { + console.error("createStudyPlanCollaborator error: ", error); + return null; + } +}; + +export const updateCollaboratorById = async ( + collabId: string, + body: StudyPlanCollaboratorPutDTO, +) => { + try { + const collaboratorRepository = AppDataSource.getRepository( + StudyPlanCollaborator, + ); + + await collaboratorRepository.update({ id: collabId }, body); + + return await collaboratorRepository.findOne({ where: { id: collabId } }); + } catch (error) { + console.error("createStudyPlanCollaborator error: ", error); + return null; + } +}; + +export const getCollaborator = async ( + req: NextRequest, + studyPlanId: string, +): Promise => { + const user = await getUser(req); + + if (!user) return null; + + const studyPlanCollaborators = WIP_EXPERIMENTAL_STUDY_PLAN_ID_ENFORCEMENT + ? await getAllCollaboratorsByStudyPlanId(studyPlanId) + : await getAllCollaboratorOwnerByUserId(user.id); + + const userCollaborator = + studyPlanCollaborators.find( + (collaborator) => collaborator.userId === user.id, + ) ?? null; + + if (!userCollaborator) return null; + + return userCollaborator; +}; diff --git a/backend/queries/study-plan.query.ts b/backend/queries/study-plan.query.ts new file mode 100644 index 0000000..c6d032e --- /dev/null +++ b/backend/queries/study-plan.query.ts @@ -0,0 +1,44 @@ +import { AppDataSource } from "../datasource"; +import { StudyPlanPutDTO } from "../dtos/study-plan.dto"; +import { CollaboratorRole } from "../entities/enums"; +import { StudyPlan } from "../entities/studyPlan.entity"; + +export async function updateStudyPlanScopeByCollabId( + collabId: string, + body: StudyPlanPutDTO, +): Promise { + try { + const studyPlanRepository = AppDataSource.getRepository(StudyPlan); + + const studyPlan = await getStudyPlanByCollaboratorId(collabId); + + if (!studyPlan) return null; + + studyPlan.scope = body.scope; + + return await studyPlanRepository.save(studyPlan); + } catch (error) { + console.error("UpdateStudyPlanScope: ", error); + + return null; + } +} + +export async function getStudyPlanByCollaboratorId(collabId: string) { + try { + const studyPlanRepository = AppDataSource.getRepository(StudyPlan); + + return await studyPlanRepository.findOne({ + where: { + studyPlanCollaborators: { + role: CollaboratorRole.Owner, + id: collabId, + }, + }, + }); + } catch (error) { + console.error("GetStudyPlanByCollaboratorId: ", error); + + return null; + } +} diff --git a/backend/queries/user.query.ts b/backend/queries/user.query.ts new file mode 100644 index 0000000..26c4f33 --- /dev/null +++ b/backend/queries/user.query.ts @@ -0,0 +1,18 @@ +import { AppDataSource } from "../datasource"; +import { User } from "../entities/user.entity"; + +export async function getUserByLpId(lpId: string) { + try { + const userRepository = AppDataSource.getRepository(User); + + return await userRepository.findOne({ + where: { + lpId, + }, + }); + } catch (error) { + console.error("getUserByLpId: ", error); + + return null; + } +} diff --git a/components/SemestersList/useSemestersList.ts b/components/SemestersList/useSemestersList.ts index 310bbc0..898921a 100644 --- a/components/SemestersList/useSemestersList.ts +++ b/components/SemestersList/useSemestersList.ts @@ -1,8 +1,8 @@ import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { SemesterModuleDTO } from "@/backend/dtos/semester-module.dto"; import { Semester, SemesterModule } from "@/components/util/types"; -import { ApiSemesterModule } from "@/services/apiClient"; import { useStudyPlan } from "@/services/apiClient/hooks/useStudyPlan"; import { useLearningPlatformAssessmentTable } from "@/services/learningPlatform/hooks/useLearningPlatformAssessmentTable"; import { useLearningPlatformSemesters } from "@/services/learningPlatform/hooks/useLearningPlatformSemesters"; @@ -31,7 +31,7 @@ export function useSemestersList(): SemestersListProps { const assessmentTableQuery = useLearningPlatformAssessmentTable(); - const toPlannedModule = (i: ApiSemesterModule): SemesterModule => ({ + const toPlannedModule = (i: SemesterModuleDTO): SemesterModule => ({ type: "planned", id: i.moduleId, diff --git a/components/SuggestionsPanel/getMissingMandatory.ts b/components/SuggestionsPanel/getMissingMandatory.ts index 46fb31c..3c15af1 100644 --- a/components/SuggestionsPanel/getMissingMandatory.ts +++ b/components/SuggestionsPanel/getMissingMandatory.ts @@ -1,25 +1,14 @@ +import { CompulsoryElectivePairingDTO } from "@/backend/dtos/compulsory-elective-pairing.dto"; import { getGradeInfo } from "@/services/learningPlatform/util/getGradeInfo"; import { Module, Semester } from "../util/types"; import { Issue } from "./issues"; -export interface CompulsoryElectivePairing { - id: string; - moduleHandbookId: string; - modules: { - id: string; - lpId: string; - proficiency: number; - possiblyOutdated: boolean; - moduleIdentifier: string; - }[]; -} - export function getMissingMandatory( semesters: Semester[], modules: Module[], mandatoryModuleIds: string[], - compulsoryElectivePairings: CompulsoryElectivePairing[], + compulsoryElectivePairings: CompulsoryElectivePairingDTO[], ) { let issues: Issue[] = []; diff --git a/components/util/useDragDropContext.ts b/components/util/useDragDropContext.ts index 1770635..42a2f99 100644 --- a/components/util/useDragDropContext.ts +++ b/components/util/useDragDropContext.ts @@ -1,11 +1,9 @@ import { OnDragEndResponder, OnDragStartResponder } from "@hello-pangea/dnd"; +import { SemesterModulePutDTO } from "@/backend/dtos/semester-module.dto"; import { getChatSelectionState } from "@/components/util/useChatSelection"; import { useModulesInScope } from "@/components/util/useModulesInScope"; -import { - SemesterModuleCategory, - UpdateSemesterModuleInput, -} from "@/services/apiClient"; +import { SemesterModuleCategory } from "@/services/apiClient"; import { useStudyPlan } from "@/services/apiClient/hooks/useStudyPlan"; import { useUpdateSemesterModule } from "@/services/apiClient/hooks/useUpdateSemesterModules"; @@ -158,7 +156,7 @@ export function useUpdateStudyPlan() { ) { if (!studyPlan.isSuccess) return; - const body = studyPlan.data.semesters.reduce( + const body = studyPlan.data.semesters.reduce( (acc, semester) => { acc[semester.id] = semester.modules; diff --git a/ormconfig.ts b/ormconfig.ts index ebea535..d3c9015 100644 --- a/ormconfig.ts +++ b/ormconfig.ts @@ -8,6 +8,8 @@ import { StudyPlan } from "./backend/entities/studyPlan.entity"; import { StudyProgram } from "./backend/entities/studyProgram.entity"; import { ModuleHandbook } from "./backend/entities/moduleHandbook.entity"; import { CompulsoryElectivePairing } from "./backend/entities/compulsoryElectivePairing.entity"; +import { StudyPlanCollaborator } from "./backend/entities/studyPlanCollaborator.entity"; +import { Invite } from "./backend/entities/invite.entity"; const dataSourceOptions: DataSourceOptions = { type: "postgres", @@ -25,8 +27,10 @@ const dataSourceOptions: DataSourceOptions = { User, StudyPlan, StudyProgram, + StudyPlanCollaborator, ModuleHandbook, CompulsoryElectivePairing, + Invite ], subscribers: [], migrations: [], diff --git a/package.json b/package.json index 632d3b8..121d255 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "code-study-planner", "version": "0.1.0", "private": true, + "engines": { + "node": ">=20" + }, "scripts": { "dev": "next dev", "build": "next build", diff --git a/services/apiClient/hooks/useUpdateSemesterModules.ts b/services/apiClient/hooks/useUpdateSemesterModules.ts index ef8e200..400bd03 100644 --- a/services/apiClient/hooks/useUpdateSemesterModules.ts +++ b/services/apiClient/hooks/useUpdateSemesterModules.ts @@ -1,8 +1,9 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { StudyPlanDTO } from "@/backend/dtos/study-plan.dto"; import { useMessages } from "@/components/util/useMessages"; -import { StudyPlan, StudyPlannerApiClient } from ".."; +import { StudyPlannerApiClient } from ".."; import useSession from "../useSession"; export const useUpdateSemesterModule = () => { @@ -19,7 +20,7 @@ export const useUpdateSemesterModule = () => { >(api), mutationKey: ["studyPlanner", "studyPlan"], onMutate(variables) { - const prev = queryClient.getQueryData([ + const prev = queryClient.getQueryData([ "studyPlanner", "studyPlan", ]); diff --git a/services/apiClient/index.ts b/services/apiClient/index.ts index 7f5096e..23d617c 100644 --- a/services/apiClient/index.ts +++ b/services/apiClient/index.ts @@ -1,25 +1,16 @@ -import { CompulsoryElectivePairing } from "@/backend/entities/compulsoryElectivePairing.entity"; +import { CompulsoryElectivePairingDTO } from "@/backend/dtos/compulsory-elective-pairing.dto"; +import { InvitePostDTO } from "@/backend/dtos/invite.dto"; +import { SemesterModulePutDTO } from "@/backend/dtos/semester-module.dto"; +import { + StudyPlanCollaboratorDTO, + StudyPlanCollaboratorPutDTO, +} from "@/backend/dtos/study-plan-collaborator.dto"; +import { StudyPlanDTO, StudyPlanPutDTO } from "@/backend/dtos/study-plan.dto"; import { Module } from "@/backend/entities/module.entity"; -export interface ApiSemesterModule { - moduleId: string; -} - -interface ModulesRecord { - earlyAssessments: ApiSemesterModule[]; - standardAssessments: ApiSemesterModule[]; - alternativeAssessments: ApiSemesterModule[]; - reassessments: ApiSemesterModule[]; -} - -export interface StudyPlan { - semesters: { - id: string; - lpId: string | null; - startDate: string; - modules: ModulesRecord; - }[]; -} +type SuccessResponse = { + ok: boolean; +}; export class StudyPlannerApiClient { constructor( @@ -31,7 +22,7 @@ export class StudyPlannerApiClient { public async getModules(): Promise<{ modules: Module[]; - compulsoryElective: CompulsoryElectivePairing[]; + compulsoryElective: CompulsoryElectivePairingDTO[]; }> { const res = await fetch(this.url + "/modules", { headers: { @@ -44,40 +35,173 @@ export class StudyPlannerApiClient { return data; } - public async getStudyPlan(): Promise { - const res = await fetch(this.url + "/study-plan", { - headers: { - "Content-Type": "application/json", - Authorization: this.accessToken, - }, - }); - const data: StudyPlan = await res.json(); + public async getStudyPlan(): Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const res = await this.fetchStudyPlan(studyPlanId, "", "GET", {}); + const data: StudyPlanDTO = await res.json(); + + return data; + } + + public async getStudyPlanCollaborators(): Promise< + StudyPlanCollaboratorDTO[] + > { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/collaborators", + "GET", + {}, + ); + const data: StudyPlanCollaboratorDTO[] = await res.json(); + + return data; + } + + public async deleteStudyPlanCollaborator(): Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const collabId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/collaborators/" + collabId, + "DELETE", + {}, + ); + const data: SuccessResponse = await res.json(); + + return data; + } + + public async putStudyPlanCollaboratorRole( + body: StudyPlanCollaboratorPutDTO, + ): Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const collabId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/collaborators/" + collabId, + "PUT", + body, + ); + const data: SuccessResponse = await res.json(); + + return data; + } + + public async putStudyPlanScope( + body: StudyPlanPutDTO, + studyPlanId = "foo", + ): Promise { + const res = await this.fetchStudyPlan(studyPlanId, "", "PUT", body); + + const data: SuccessResponse = await res.json(); return data; } public async updateSemesterModules( - body: UpdateSemesterModuleInput, - ): Promise<{}> { - const res = await fetch( - this.url + "/study-plan/semester-modules/batch-update", - { + body: SemesterModulePutDTO, + //studyPlanId: string + ): Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/semester-modules/batch-update", + "PUT", + body, + ); + const data: SuccessResponse = await res.json(); + + return data; + } + public async postInviteCollaborator( + body: InvitePostDTO, + //studyPlanId: string + ): Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/collaborators/invites", + "POST", + body, + ); + const data: SuccessResponse = await res.json(); + + return data; + } + + public async putInviteAccept() //studyPlanId: string + //inviteId: string + : Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const inviteId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/collaborators/invites/" + inviteId + "/accept", + "PUT", + {}, + ); + const data: SuccessResponse = await res.json(); + + return data; + } + + public async putInviteDecline() //studyPlanId: string + //inviteId: string + : Promise { + //TODO: need to be added as a parameter (currently issue with the hooks), currently not used in backend either + const studyPlanId = "foo"; + const inviteId = "foo"; + const res = await this.fetchStudyPlan( + studyPlanId, + "/collaborators/invites/" + inviteId + "/decline", + "PUT", + {}, + ); + const data: SuccessResponse = await res.json(); + + return data; + } + + private async fetchStudyPlan( + studyPlanId: string, + url: string, + method: "PUT" | "GET" | "POST" | "DELETE", + body: any, + ) { + if (method === "GET") + return await fetch(this.url + "/study-plan/" + studyPlanId + url, { + headers: { + "Content-Type": "application/json", + Authorization: this.accessToken, + }, + }); + if (method === "DELETE") + return await fetch(this.url + "/study-plan/" + studyPlanId + url, { headers: { "Content-Type": "application/json", Authorization: this.accessToken, }, - method: "PUT", - body: JSON.stringify(body), + method: "DELETE", + }); + return await fetch(this.url + "/study-plan/" + studyPlanId + url, { + headers: { + "Content-Type": "application/json", + Authorization: this.accessToken, }, - ); - const data: {} = await res.json(); - - return data; + method, + body: JSON.stringify(body), + }); } } -export type UpdateSemesterModuleInput = Record; - export type SemesterModuleCategory = | "earlyAssessments" | "standardAssessments"