diff --git a/src/app.module.ts b/src/app.module.ts index c3f97759..3cdfa4f1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,11 +11,13 @@ import { DepartmentsModule } from './modules/departments/departments.module'; import { FeedsModule } from './modules/feeds/feeds.module'; import { LecturesModule } from './modules/lectures/lectures.module'; import { NoticesModule } from './modules/notices/notices.module'; +import { PlannersModule } from './modules/planners/planners.module'; import { ReviewsModule } from './modules/reviews/reviews.module'; import { SemestersModule } from './modules/semesters/semesters.module'; import { SessionModule } from './modules/session/session.module'; import { StatusModule } from './modules/status/status.module'; import { TimetablesModule } from './modules/timetables/timetables.module'; +import { TracksModule } from './modules/tracks/tracks.module'; import { UserModule } from './modules/user/user.module'; import { WishlistModule } from './modules/wishlist/wishlist.module'; import { PrismaModule } from './prisma/prisma.module'; @@ -36,6 +38,8 @@ import { PrismaModule } from './prisma/prisma.module'; NoticesModule, SessionModule, DepartmentsModule, + PlannersModule, + TracksModule, ], controllers: [AppController], providers: [ diff --git a/src/common/interfaces/constants/additional.track.response.dto.ts b/src/common/interfaces/constants/additional.track.response.dto.ts new file mode 100644 index 00000000..bd0f06b5 --- /dev/null +++ b/src/common/interfaces/constants/additional.track.response.dto.ts @@ -0,0 +1,5 @@ +import { generationUnionTypeChecker } from 'src/common/utils/method.utils'; + +const types = ['DOUBLE', 'MINOR', 'ADVANCED', 'INTERDISCIPLINARY'] as const; +export type AdditionalTrackType = (typeof types)[number]; +export const AddtionalTrackTypeNarrower = generationUnionTypeChecker(...types); diff --git a/src/common/interfaces/dto/planner/planner.request.dto.ts b/src/common/interfaces/dto/planner/planner.request.dto.ts new file mode 100644 index 00000000..48e690b9 --- /dev/null +++ b/src/common/interfaces/dto/planner/planner.request.dto.ts @@ -0,0 +1,58 @@ +import { Transform, Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsInt, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { + OrderDefaultValidator, + _PROHIBITED_FIELD_PATTERN, +} from '../../../decorators/validators.decorator'; +export class PlannerQueryDto { + @IsOptional() + @Transform(({ value }) => (typeof value === 'string' ? [value] : value)) + @IsArray() + @IsString({ each: true }) + @OrderDefaultValidator(_PROHIBITED_FIELD_PATTERN) + order?: string[]; + + @IsOptional() + @IsNumber() + @Type(() => Number) + offset?: number; + + @IsOptional() + @IsNumber() + @Type(() => Number) + limit?: number; +} + +export class PlannerBodyDto { + @IsInt() + start_year!: number; + @IsInt() + end_year!: number; + @IsInt() + general_track!: number; + @IsInt() + major_track!: number; + @IsOptional() + @IsArray() + @IsInt({ each: true }) + additional_tracks?: number[]; + @IsOptional() + @IsBoolean() + should_update_taken_semesters?: boolean; + @IsArray() + @IsInt({ each: true }) + taken_items_to_copy!: number[]; + @IsArray() + @IsInt({ each: true }) + future_items_to_copy!: number[]; + @IsArray() + @IsInt({ each: true }) + arbitrary_items_to_copy!: number[]; +} diff --git a/src/common/interfaces/dto/planner/planner.response.dto.ts b/src/common/interfaces/dto/planner/planner.response.dto.ts new file mode 100644 index 00000000..231fdc79 --- /dev/null +++ b/src/common/interfaces/dto/planner/planner.response.dto.ts @@ -0,0 +1,19 @@ +import { ArbitraryPlannerItemResponseDto } from '../planner_item/arbitrary.response.dto'; +import { FuturePlannerItemResponseDto } from '../planner_item/future.reponse.dto'; +import { TakenPlannerItemResponseDto } from '../planner_item/taken.response.dto'; +import { AdditionalTrackResponseDto } from '../track/additional.response.dto'; +import { GeneralTrackResponseDto } from '../track/general.response.dto'; +import { MajorTrackResponseDto } from '../track/major.response.dto'; + +export interface PlannerResponseDto { + id: number; + start_year: number; + end_year: number; + general_track: GeneralTrackResponseDto; + major_track: MajorTrackResponseDto; + additional_tracks: AdditionalTrackResponseDto[]; + taken_items: TakenPlannerItemResponseDto[]; + future_items: FuturePlannerItemResponseDto[]; + arbitrary_items: ArbitraryPlannerItemResponseDto[]; + arrange_order: number; +} diff --git a/src/common/interfaces/dto/planner_item/arbitrary.response.dto.ts b/src/common/interfaces/dto/planner_item/arbitrary.response.dto.ts new file mode 100644 index 00000000..ac689ac3 --- /dev/null +++ b/src/common/interfaces/dto/planner_item/arbitrary.response.dto.ts @@ -0,0 +1,14 @@ +import { DepartmentResponseDto } from '../department/department.response.dto'; + +export interface ArbitraryPlannerItemResponseDto { + id: number; + item_type: 'ARBITRARY'; + is_excluded: boolean; + year: number; + semester: number; + department: DepartmentResponseDto | null; + type: string; + type_en: string; + credit: number; + credit_au: number; +} diff --git a/src/common/interfaces/dto/planner_item/future.reponse.dto.ts b/src/common/interfaces/dto/planner_item/future.reponse.dto.ts new file mode 100644 index 00000000..f94be0af --- /dev/null +++ b/src/common/interfaces/dto/planner_item/future.reponse.dto.ts @@ -0,0 +1,10 @@ +import { CourseResponseDto } from '../course/course.response.dto'; + +export interface FuturePlannerItemResponseDto { + id: number; + item_type: 'FUTURE'; + is_excluded: boolean; + year: number; + semester: number; + course: CourseResponseDto; +} diff --git a/src/common/interfaces/dto/planner_item/taken.response.dto.ts b/src/common/interfaces/dto/planner_item/taken.response.dto.ts new file mode 100644 index 00000000..cdc08b0b --- /dev/null +++ b/src/common/interfaces/dto/planner_item/taken.response.dto.ts @@ -0,0 +1,10 @@ +import { CourseResponseDto } from '../course/course.response.dto'; +import { LectureResponseDto } from '../lecture/lecture.response.dto'; + +export interface TakenPlannerItemResponseDto { + id: number; + item_type: 'TAKEN'; + is_excluded: boolean; + lecture: LectureResponseDto; + course: CourseResponseDto; +} diff --git a/src/common/interfaces/dto/track/additional.response.dto.ts b/src/common/interfaces/dto/track/additional.response.dto.ts new file mode 100644 index 00000000..116f6a95 --- /dev/null +++ b/src/common/interfaces/dto/track/additional.response.dto.ts @@ -0,0 +1,12 @@ +import { AdditionalTrackType } from '../../constants/additional.track.response.dto'; +import { DepartmentResponseDto } from '../department/department.response.dto'; + +export interface AdditionalTrackResponseDto { + id: number; + start_year: number; + end_year: number; + type: AdditionalTrackType; + department: DepartmentResponseDto | null; + major_required: number; + major_elective: number; +} diff --git a/src/common/interfaces/dto/track/general.response.dto.ts b/src/common/interfaces/dto/track/general.response.dto.ts new file mode 100644 index 00000000..e3970f03 --- /dev/null +++ b/src/common/interfaces/dto/track/general.response.dto.ts @@ -0,0 +1,16 @@ +export interface GeneralTrackResponseDto { + id: number; + start_year: number; + end_year: number; + is_foreign: boolean; + total_credit: number; + total_au: number; + basic_required: number; + basic_elective: number; + thesis_study: number; + thesis_study_doublemajor: number; + general_required_credit: number; + general_required_au: number; + humanities: number; + humanities_doublemajor: number; +} diff --git a/src/common/interfaces/dto/track/major.response.dto.ts b/src/common/interfaces/dto/track/major.response.dto.ts new file mode 100644 index 00000000..ec7925bb --- /dev/null +++ b/src/common/interfaces/dto/track/major.response.dto.ts @@ -0,0 +1,11 @@ +import { DepartmentResponseDto } from '../department/department.response.dto'; + +export interface MajorTrackResponseDto { + id: number; + start_year: number; + end_year: number; + department: DepartmentResponseDto; + basic_elective_doublemajor: number; + major_required: number; + major_elective: number; +} diff --git a/src/common/interfaces/serializer/planner.item.serializer.ts b/src/common/interfaces/serializer/planner.item.serializer.ts new file mode 100644 index 00000000..884d3fe5 --- /dev/null +++ b/src/common/interfaces/serializer/planner.item.serializer.ts @@ -0,0 +1,70 @@ +import { + ArbitraryPlannerItem, + FuturePlannerItem, + TakenPlannerItem, +} from 'src/common/schemaTypes/types'; +import { ArbitraryPlannerItemResponseDto } from '../dto/planner_item/arbitrary.response.dto'; +import { FuturePlannerItemResponseDto } from '../dto/planner_item/future.reponse.dto'; +import { TakenPlannerItemResponseDto } from '../dto/planner_item/taken.response.dto'; +import { toJsonCourse } from './course.serializer'; +import { toJsonDepartment } from './department.serializer'; +import { toJsonLecture } from './lecture.serializer'; + +export const toJsonTakenItem = ( + taken_item: TakenPlannerItem, +): TakenPlannerItemResponseDto => { + return { + id: taken_item.id, + item_type: 'TAKEN', + is_excluded: taken_item.is_excluded, + lecture: toJsonLecture(taken_item.subject_lecture, false), + course: toJsonCourse( + taken_item.subject_lecture.course, + taken_item.subject_lecture, + taken_item.subject_lecture.course.subject_course_professors.map( + (x) => x.professor, + ), + false, + ), + }; +}; + +export const toJsonArbitraryItem = ( + arbitrary_item: ArbitraryPlannerItem, +): ArbitraryPlannerItemResponseDto => { + return { + id: arbitrary_item.id, + item_type: 'ARBITRARY', + is_excluded: arbitrary_item.is_excluded, + year: arbitrary_item.year, + semester: arbitrary_item.semester, + department: + arbitrary_item.subject_department !== null + ? toJsonDepartment(arbitrary_item.subject_department) + : null, + type: arbitrary_item.type, + type_en: arbitrary_item.type_en, + credit: arbitrary_item.credit, + credit_au: arbitrary_item.credit_au, + }; +}; + +export const toJsonFutureItem = ( + future_item: FuturePlannerItem, +): FuturePlannerItemResponseDto => { + return { + id: future_item.id, + item_type: 'FUTURE', + is_excluded: future_item.is_excluded, + year: future_item.year, + semester: future_item.semester, + course: toJsonCourse( + future_item.subject_course, + future_item.subject_course.lecture[0], + future_item.subject_course.subject_course_professors.map( + (x) => x.professor, + ), + false, + ), + }; +}; diff --git a/src/common/interfaces/serializer/planner.serializer.ts b/src/common/interfaces/serializer/planner.serializer.ts new file mode 100644 index 00000000..e011f6d5 --- /dev/null +++ b/src/common/interfaces/serializer/planner.serializer.ts @@ -0,0 +1,36 @@ +import { PlannerDetails } from 'src/common/schemaTypes/types'; +import { PlannerResponseDto } from '../dto/planner/planner.response.dto'; +import { + toJsonArbitraryItem, + toJsonFutureItem, + toJsonTakenItem, +} from './planner.item.serializer'; +import { + toJsonAdditionalTrack, + toJsonGeneralTrack, + toJsonMajorTrack, +} from './track.serializer'; + +export const toJsonPlanner = (planner: PlannerDetails): PlannerResponseDto => { + return { + id: planner.id, + start_year: planner.start_year, + end_year: planner.end_year, + general_track: toJsonGeneralTrack(planner.graduation_generaltrack), + major_track: toJsonMajorTrack(planner.graduation_majortrack), + additional_tracks: planner.planner_planner_additional_tracks.map( + (additional_track) => + toJsonAdditionalTrack(additional_track.graduation_additionaltrack), + ), + taken_items: planner.planner_takenplanneritem.map((item) => + toJsonTakenItem(item), + ), + future_items: planner.planner_futureplanneritem.map((item) => + toJsonFutureItem(item), + ), + arbitrary_items: planner.planner_arbitraryplanneritem.map((item) => + toJsonArbitraryItem(item), + ), + arrange_order: planner.arrange_order, + }; +}; diff --git a/src/common/interfaces/serializer/track.serializer.ts b/src/common/interfaces/serializer/track.serializer.ts new file mode 100644 index 00000000..320e7dbb --- /dev/null +++ b/src/common/interfaces/serializer/track.serializer.ts @@ -0,0 +1,68 @@ +import { + AdditionalTrackDetails, + GeneralTrackBasic, + MajorTrackDetails, +} from 'src/common/schemaTypes/types'; +import { AddtionalTrackTypeNarrower } from '../constants/additional.track.response.dto'; +import { AdditionalTrackResponseDto } from '../dto/track/additional.response.dto'; +import { GeneralTrackResponseDto } from '../dto/track/general.response.dto'; +import { MajorTrackResponseDto } from '../dto/track/major.response.dto'; +import { toJsonDepartment } from './department.serializer'; + +export const toJsonGeneralTrack = ( + generalTrack: GeneralTrackBasic, +): GeneralTrackResponseDto => { + return { + id: generalTrack.id, + start_year: generalTrack.start_year, + end_year: generalTrack.end_year, + is_foreign: generalTrack.is_foreign, + total_credit: generalTrack.total_credit, + total_au: generalTrack.total_au, + basic_required: generalTrack.basic_required, + basic_elective: generalTrack.basic_elective, + thesis_study: generalTrack.thesis_study, + thesis_study_doublemajor: generalTrack.thesis_study_doublemajor, + general_required_credit: generalTrack.general_required_credit, + general_required_au: generalTrack.general_required_au, + humanities: generalTrack.humanities, + humanities_doublemajor: generalTrack.humanities_doublemajor, + }; +}; + +export const toJsonMajorTrack = ( + majorTrack: MajorTrackDetails, +): MajorTrackResponseDto => { + return { + id: majorTrack.id, + start_year: majorTrack.start_year, + end_year: majorTrack.end_year, + department: toJsonDepartment(majorTrack.subject_department), + basic_elective_doublemajor: majorTrack.basic_elective_doublemajor, + major_required: majorTrack.major_required, + major_elective: majorTrack.major_elective, + }; +}; + +export const toJsonAdditionalTrack = ( + additionalTrack: AdditionalTrackDetails, +): AdditionalTrackResponseDto => { + const type = AddtionalTrackTypeNarrower(additionalTrack.type); + + if (type instanceof Error) { + throw type; + } + + return { + id: additionalTrack.id, + start_year: additionalTrack.start_year, + end_year: additionalTrack.end_year, + type, + department: + additionalTrack.subject_department === null + ? null + : toJsonDepartment(additionalTrack.subject_department), + major_required: additionalTrack.major_required, + major_elective: additionalTrack.major_elective, + }; +}; diff --git a/src/common/schemaTypes/types.ts b/src/common/schemaTypes/types.ts index 6e78fd82..78140211 100644 --- a/src/common/schemaTypes/types.ts +++ b/src/common/schemaTypes/types.ts @@ -46,6 +46,60 @@ export const timeTableDetails = }, }); +const majorTrack = Prisma.validator()({ + include: { + subject_department: true, + }, +}); + +export const additionalTrack = + Prisma.validator()({ + include: { + subject_department: true, + }, + }); + +export const takenPlannerItem = + Prisma.validator()({ + include: { + subject_lecture: { + include: { + ...lectureDetails.include, + course: courseDetails, + }, + }, + }, + }); + +export const arbitraryPlannerItem = + Prisma.validator()({ + include: { + subject_department: true, + }, + }); + +export const futurePlannerItem = + Prisma.validator()({ + include: { + subject_course: courseDetails, + }, + }); + +export const plannerDetails = Prisma.validator()({ + include: { + planner_planner_additional_tracks: { + include: { + graduation_additionaltrack: additionalTrack, + }, + }, + graduation_generaltrack: true, + graduation_majortrack: majorTrack, + planner_takenplanneritem: takenPlannerItem, + planner_arbitraryplanneritem: arbitraryPlannerItem, + planner_futureplanneritem: futurePlannerItem, + }, +}); + export type NESTED = true; export const reviewDetails = Prisma.validator()({ @@ -104,6 +158,24 @@ export type TimeTableDetails = Prisma.timetable_timetableGetPayload< >; export type TimeTableBasic = Prisma.timetable_timetableGetPayload; export type SemesterBasic = Prisma.subject_semesterGetPayload; +export type PlannerBasic = Prisma.planner_plannerGetPayload; +export type PlannerDetails = Prisma.planner_plannerGetPayload< + typeof plannerDetails +>; +export type ArbitraryPlannerItem = + Prisma.planner_arbitraryplanneritemGetPayload; +export type FuturePlannerItem = Prisma.planner_futureplanneritemGetPayload< + typeof futurePlannerItem +>; +export type TakenPlannerItem = Prisma.planner_takenplanneritemGetPayload< + typeof takenPlannerItem +>; +export type GeneralTrackBasic = Prisma.graduation_generaltrackGetPayload; +export type MajorTrackDetails = Prisma.graduation_majortrackGetPayload< + typeof majorTrack +>; +export type AdditionalTrackDetails = + Prisma.graduation_additionaltrackGetPayload; export type WishlistWithLectures = Prisma.timetable_wishlistGetPayload< typeof wishlistWithLectures diff --git a/src/common/utils/method.utils.ts b/src/common/utils/method.utils.ts index 87c248b0..866059df 100644 --- a/src/common/utils/method.utils.ts +++ b/src/common/utils/method.utils.ts @@ -39,3 +39,14 @@ export function getRandomChoice(choices: T[]): T { const randomIndex = Math.floor(Math.random() * choices.length); return choices[randomIndex]; } + +export function generationUnionTypeChecker( + ...values: UnionType[] +) { + return function (value: unknown): UnionType | Error { + if (typeof value !== 'string') return new Error('Invalid value: ' + value); + return values.includes(value as UnionType) + ? (value as UnionType) + : new Error('Invalid value: ' + value); + }; +} diff --git a/src/modules/planners/planners.controller.ts b/src/modules/planners/planners.controller.ts index 953148c5..2d5a9951 100644 --- a/src/modules/planners/planners.controller.ts +++ b/src/modules/planners/planners.controller.ts @@ -1,4 +1,47 @@ -import { Controller } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Post, + Query, + UnauthorizedException, +} from '@nestjs/common'; +import { session_userprofile } from '@prisma/client'; +import { GetUser } from 'src/common/decorators/get-user.decorator'; +import { + PlannerBodyDto, + PlannerQueryDto, +} from 'src/common/interfaces/dto/planner/planner.request.dto'; +import { PlannersService } from './planners.service'; -@Controller('planners') -export class PlannersController {} +@Controller('api/users/:id/planners') +export class PlannersController { + constructor(private readonly plannersService: PlannersService) {} + + @Get() + async getPlanners( + @Query() query: PlannerQueryDto, + @Param('id') id: number, + @GetUser() user: session_userprofile, + ) { + if (id !== user.id) { + throw new UnauthorizedException(); + } + const planners = await this.plannersService.getPlannerByUser(query, user); + return planners; + } + + @Post() + async postPlanner( + @Body() planner: PlannerBodyDto, + @Param('id') id: number, + @GetUser() user: session_userprofile, + ) { + if (id !== user.id) { + throw new UnauthorizedException(); + } + const newPlanner = await this.plannersService.postPlanner(planner, user); + return newPlanner; + } +} diff --git a/src/modules/planners/planners.module.ts b/src/modules/planners/planners.module.ts index 82449cee..18623ddf 100644 --- a/src/modules/planners/planners.module.ts +++ b/src/modules/planners/planners.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/prisma/prisma.module'; import { PlannersController } from './planners.controller'; import { PlannersService } from './planners.service'; @Module({ + imports: [PrismaModule], controllers: [PlannersController], providers: [PlannersService], }) diff --git a/src/modules/planners/planners.service.ts b/src/modules/planners/planners.service.ts index 19a289f1..bfb610b6 100644 --- a/src/modules/planners/planners.service.ts +++ b/src/modules/planners/planners.service.ts @@ -1,4 +1,96 @@ import { Injectable } from '@nestjs/common'; +import { session_userprofile } from '@prisma/client'; +import { + PlannerBodyDto, + PlannerQueryDto, +} from 'src/common/interfaces/dto/planner/planner.request.dto'; +import { PlannerResponseDto } from 'src/common/interfaces/dto/planner/planner.response.dto'; +import { toJsonPlanner } from 'src/common/interfaces/serializer/planner.serializer'; +import { LectureRepository } from 'src/prisma/repositories/lecture.repository'; +import { PlannerRepository } from 'src/prisma/repositories/planner.repository'; @Injectable() -export class PlannersService {} +export class PlannersService { + constructor( + private readonly PlannerRepository: PlannerRepository, + private readonly LectureRepository: LectureRepository, + ) {} + + public async getPlannerByUser( + query: PlannerQueryDto, + user: session_userprofile, + ) { + const queryResult = await this.PlannerRepository.getPlannerByUser( + query, + user, + ); + return queryResult.map(toJsonPlanner); + } + + async getRelatedPlanner(user: session_userprofile) { + return await this.PlannerRepository.getRelatedPlanner(user); + } + + public async postPlanner( + body: PlannerBodyDto, + user: session_userprofile, + ): Promise { + const relatedPlanner = await this.getRelatedPlanner(user); + const arrangeOrder = + relatedPlanner.length == 0 + ? 0 + : relatedPlanner[relatedPlanner.length - 1].arrange_order + 1; + const planner = await this.PlannerRepository.createPlanner( + body, + arrangeOrder, + user, + ); + + if (body.should_update_taken_semesters) { + const takenLectures = + await this.LectureRepository.findReviewWritableLectures( + user, + new Date(), + ); + const valid_takenLectures = takenLectures.filter((lecture) => { + const validStartYear = lecture.year >= body.start_year; + const validEndYear = lecture.year <= body.end_year; + return validStartYear && validEndYear; + }); + valid_takenLectures.forEach(async (lecture) => { + await this.PlannerRepository.createTakenPlannerItem(planner, lecture); + }); + } + + body.taken_items_to_copy.forEach(async (item) => { + const targetItem = await this.PlannerRepository.getTakenPlannerItemById( + user, + item, + ); + await this.PlannerRepository.createTakenPlannerItem( + planner, + targetItem.subject_lecture, + targetItem.is_excluded, + ); + }); + + body.future_items_to_copy.forEach(async (item) => { + const targetItem = await this.PlannerRepository.getFuturePlannerItemById( + user, + item, + ); + await this.PlannerRepository.createFuturePlannerItem(planner, targetItem); + }); + + body.arbitrary_items_to_copy.forEach(async (item) => { + const targetItem = + await this.PlannerRepository.getArbitraryPlannerItemById(user, item); + await this.PlannerRepository.createArbitraryPlannerItem( + planner, + targetItem, + ); + }); + + return toJsonPlanner(planner); + } +} diff --git a/src/modules/tracks/tracks.controller.ts b/src/modules/tracks/tracks.controller.ts index 743df993..bcde01c2 100644 --- a/src/modules/tracks/tracks.controller.ts +++ b/src/modules/tracks/tracks.controller.ts @@ -1,4 +1,12 @@ -import { Controller } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; +import { TracksService } from './tracks.service'; -@Controller('tracks') -export class TracksController {} +@Controller('/api/tracks') +export class TracksController { + constructor(private readonly tracksService: TracksService) {} + + @Get() + async getTracks() { + return await this.tracksService.getAllTrack(); + } +} diff --git a/src/modules/tracks/tracks.module.ts b/src/modules/tracks/tracks.module.ts index 9bc5af05..fc9883ae 100644 --- a/src/modules/tracks/tracks.module.ts +++ b/src/modules/tracks/tracks.module.ts @@ -1,8 +1,10 @@ import { Module } from '@nestjs/common'; +import { PrismaModule } from 'src/prisma/prisma.module'; import { TracksController } from './tracks.controller'; import { TracksService } from './tracks.service'; @Module({ + imports: [PrismaModule], controllers: [TracksController], providers: [TracksService], }) diff --git a/src/modules/tracks/tracks.service.ts b/src/modules/tracks/tracks.service.ts index 193549e3..86e98b12 100644 --- a/src/modules/tracks/tracks.service.ts +++ b/src/modules/tracks/tracks.service.ts @@ -1,4 +1,22 @@ import { Injectable } from '@nestjs/common'; +import { + toJsonAdditionalTrack, + toJsonGeneralTrack, + toJsonMajorTrack, +} from 'src/common/interfaces/serializer/track.serializer'; +import { TracksRepository } from 'src/prisma/repositories/track.repository'; @Injectable() -export class TracksService {} +export class TracksService { + constructor(private readonly TracksRepository: TracksRepository) {} + + public async getAllTrack() { + const { generalTracks, majorTracks, addtionalTracks } = + await this.TracksRepository.getAllTracks(); + return { + general: generalTracks.map(toJsonGeneralTrack), + major: majorTracks.map(toJsonMajorTrack), + additional: addtionalTracks.map(toJsonAdditionalTrack), + }; + } +} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts index 7d347b02..35abbbb1 100644 --- a/src/prisma/prisma.module.ts +++ b/src/prisma/prisma.module.ts @@ -4,9 +4,11 @@ import { CourseRepository } from './repositories/course.repository'; import { DepartmentRepository } from './repositories/department.repository'; import { LectureRepository } from './repositories/lecture.repository'; import { NoticesRepository } from './repositories/notices.repository'; +import { PlannerRepository } from './repositories/planner.repository'; import { ReviewsRepository } from './repositories/review.repository'; import { SemesterRepository } from './repositories/semester.repository'; import { TimetableRepository } from './repositories/timetable.repository'; +import { TracksRepository } from './repositories/track.repository'; import { UserRepository } from './repositories/user.repository'; import { WishlistRepository } from './repositories/wishlist.repository'; @@ -21,6 +23,8 @@ import { WishlistRepository } from './repositories/wishlist.repository'; SemesterRepository, TimetableRepository, WishlistRepository, + PlannerRepository, + TracksRepository, NoticesRepository, ], exports: [ @@ -33,6 +37,8 @@ import { WishlistRepository } from './repositories/wishlist.repository'; SemesterRepository, TimetableRepository, WishlistRepository, + PlannerRepository, + TracksRepository, NoticesRepository, ], }) diff --git a/src/prisma/repositories/planner.repository.ts b/src/prisma/repositories/planner.repository.ts new file mode 100644 index 00000000..041c23c0 --- /dev/null +++ b/src/prisma/repositories/planner.repository.ts @@ -0,0 +1,250 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { session_userprofile } from '@prisma/client'; +import { + PlannerBodyDto, + PlannerQueryDto, +} from 'src/common/interfaces/dto/planner/planner.request.dto'; +import { + ArbitraryPlannerItem, + FuturePlannerItem, + LectureDetails, + PlannerBasic, + PlannerDetails, + TakenPlannerItem, + arbitraryPlannerItem, + futurePlannerItem, + plannerDetails, + takenPlannerItem, +} from 'src/common/schemaTypes/types'; +import { orderFilter } from 'src/common/utils/search.utils'; +import { PrismaService } from '../prisma.service'; + +@Injectable() +export class PlannerRepository { + constructor(private readonly prisma: PrismaService) {} + + public async getPlannerByUser( + query: PlannerQueryDto, + user: session_userprofile, + ): Promise { + return await this.prisma.planner_planner.findMany({ + ...plannerDetails, + where: { + user_id: user.id, + }, + orderBy: orderFilter(query.order), + skip: query.offset, + take: query.limit, + }); + } + + public async createPlanner( + body: PlannerBodyDto, + arrange_order: number, + user: session_userprofile, + ): Promise { + return await this.prisma.planner_planner.create({ + ...plannerDetails, + data: { + session_userprofile: { + connect: { + id: user.id, + }, + }, + graduation_generaltrack: { + connect: { + id: body.general_track, + }, + }, + graduation_majortrack: { + connect: { + id: body.major_track, + }, + }, + planner_planner_additional_tracks: { + create: body?.additional_tracks?.map((t) => { + return { + graduation_additionaltrack: { + connect: { + id: t, + }, + }, + }; + }), + }, + start_year: body.start_year, + end_year: body.end_year, + arrange_order: arrange_order, + }, + }); + } + + public async getRelatedPlanner( + user: session_userprofile, + ): Promise { + return await this.prisma.planner_planner.findMany({ + where: { + user_id: user.id, + }, + orderBy: { + arrange_order: 'asc', + }, + }); + } + + public async getTakenPlannerItemById( + user: session_userprofile, + id: number, + ): Promise { + const planner = await this.prisma.planner_planner.findMany({ + include: { + planner_takenplanneritem: { + ...takenPlannerItem, + }, + }, + where: { + user_id: user.id, + planner_takenplanneritem: { + some: { + id: id, + }, + }, + }, + }); + const candidates = planner.map((p) => p.planner_takenplanneritem).flat(); + const result = candidates.find((c) => c.id === id); + if (!result) { + throw new NotFoundException(); + } + return result; + } + + public async createTakenPlannerItem( + planner: PlannerBasic, + lecture: LectureDetails, + isExcluded: boolean = false, + ) { + return await this.prisma.planner_takenplanneritem.create({ + data: { + planner_planner: { + connect: { + id: planner.id, + }, + }, + is_excluded: isExcluded, + subject_lecture: { + connect: { + id: lecture.id, + }, + }, + }, + }); + } + public async getFuturePlannerItemById( + user: session_userprofile, + id: number, + ): Promise { + const planner = await this.prisma.planner_planner.findMany({ + include: { + planner_futureplanneritem: { + ...futurePlannerItem, + }, + }, + where: { + user_id: user.id, + planner_futureplanneritem: { + some: { + id: id, + }, + }, + }, + }); + const candidates = planner.map((p) => p.planner_futureplanneritem).flat(); + const result = candidates.find((c) => c.id === id); + if (!result) { + throw new NotFoundException(); + } + return result; + } + + public async createFuturePlannerItem( + planner: PlannerBasic, + target_item: FuturePlannerItem, + ) { + return await this.prisma.planner_futureplanneritem.create({ + data: { + planner_planner: { + connect: { + id: planner.id, + }, + }, + subject_course: { + connect: { + id: target_item.course_id, + }, + }, + is_excluded: target_item.is_excluded, + year: target_item.year, + semester: target_item.semester, + }, + }); + } + + public async getArbitraryPlannerItemById( + user: session_userprofile, + id: number, + ): Promise { + const planner = await this.prisma.planner_planner.findMany({ + include: { + planner_arbitraryplanneritem: { + ...arbitraryPlannerItem, + }, + }, + where: { + user_id: user.id, + planner_arbitraryplanneritem: { + some: { + id: id, + }, + }, + }, + }); + const candidates = planner + .map((p) => p.planner_arbitraryplanneritem) + .flat(); + const result = candidates.find((c) => c.id === id); + if (!result) { + throw new NotFoundException(); + } + return result; + } + + public async createArbitraryPlannerItem( + planner: PlannerBasic, + target_item: ArbitraryPlannerItem, + ) { + return await this.prisma.planner_arbitraryplanneritem.create({ + data: { + planner_planner: { + connect: { + id: planner.id, + }, + }, + subject_department: target_item.department_id + ? { + connect: { + id: target_item.department_id, + }, + } + : undefined, + is_excluded: target_item.is_excluded, + year: target_item.year, + semester: target_item.semester, + type: target_item.type, + type_en: target_item.type_en, + credit: target_item.credit, + credit_au: target_item.credit_au, + }, + }); + } +} diff --git a/src/prisma/repositories/track.repository.ts b/src/prisma/repositories/track.repository.ts new file mode 100644 index 00000000..96635bd0 --- /dev/null +++ b/src/prisma/repositories/track.repository.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { + AdditionalTrackDetails, + GeneralTrackBasic, + MajorTrackDetails, +} from 'src/common/schemaTypes/types'; +import { PrismaService } from '../prisma.service'; + +@Injectable() +export class TracksRepository { + constructor(private readonly prisma: PrismaService) {} + + public async getAllTracks(): Promise<{ + generalTracks: GeneralTrackBasic[]; + majorTracks: MajorTrackDetails[]; + addtionalTracks: AdditionalTrackDetails[]; + }> { + const generalTracks = await this.prisma.graduation_generaltrack.findMany({ + orderBy: [ + { is_foreign: 'asc' }, + { start_year: 'asc' }, + { end_year: 'asc' }, + ], + }); + const majorTracks = await this.prisma.graduation_majortrack.findMany({ + include: { + subject_department: true, + }, + orderBy: [ + { + subject_department: { + code: 'asc', + }, + }, + { start_year: 'asc' }, + { end_year: 'asc' }, + ], + }); + const addtionalTracks = + await this.prisma.graduation_additionaltrack.findMany({ + include: { + subject_department: true, + }, + orderBy: [ + { + subject_department: { + code: 'asc', + }, + }, + { start_year: 'asc' }, + { end_year: 'asc' }, + ], + }); + const sortedAddtionalTracks = addtionalTracks.sort((a, b) => { + return a.type.length - b.type.length; + }); + + return { + generalTracks, + majorTracks, + addtionalTracks: sortedAddtionalTracks, + }; + } +}