diff --git a/src/app.module.ts b/src/app.module.ts index 712c1a38b428725a4ff9c3359155df8a01721e05..9e9ee257abb0b46a1a689a034421ad2b3648ee36 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,22 +4,16 @@ import { AppService } from "./app.service"; import { TypeOrmModule } from "@nestjs/typeorm"; import { Bimbingan } from "./entities/bimbingan.entity"; import { Pengguna } from "./entities/pengguna.entity"; -import { RangeJadwalSeminar } from "./entities/rangeJadwalSeminar.entity"; -import { Seminar } from "./entities/seminar.entity"; import { Topik } from "./entities/topik.entity"; import { AuditLog } from "./entities/auditLog.entity"; import { DosenBimbingan } from "./entities/dosenBimbingan.entity"; import { Kelas } from "./entities/kelas.entity"; -import { MahasiswaKelas } from "./entities/mahasiswaKelas"; +import { MahasiswaKelas } from "./entities/mahasiswaKelas.entity"; import { PengajarKelas } from "./entities/pengajarKelas.entity"; import { PendaftaranTesis } from "./entities/pendaftaranTesis.entity"; -import { RangeJadwalSidang } from "./entities/rangeJadwalSidang.entity"; -import { Ruangan } from "./entities/ruangan.entity"; -import { Sidang } from "./entities/sidang.entity"; +// import { Ruangan } from "./entities/ruangan.entity"; import { Tugas } from "./entities/tugas.entity"; -import { PembimbingSeminar } from "./entities/pembimbingSeminar.entity"; -import { PembimbingSidang } from "./entities/pembimbingSidang.entity"; -import { PengujiSidang } from "./entities/pengujiSidang.entity"; +import { PengujiSidsem } from "./entities/pengujiSidsem.entity"; import { RegistrasiTesisModule } from "./registrasi-tesis/registrasi-tesis.module"; import { ConfigModule } from "@nestjs/config"; import { AuthModule } from "./auth/auth.module"; @@ -28,16 +22,18 @@ import { DashboardModule } from "./dashboard/dashboard.module"; import { BimbinganModule } from "./bimbingan/bimbingan.module"; import { Konfigurasi } from "./entities/konfigurasi.entity"; import { KonfigurasiModule } from "./konfigurasi/konfigurasi.module"; -import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module"; -import { ApprovalModule } from "./approval/approval.module"; import { validate } from "./env.validation"; -import { BerkasBimbingan } from "./entities/berkasBimbingan"; -import { MataKuliah } from "./entities/mataKuliah"; -import { SubmisiTugas } from "./entities/submisiTugas"; +import { BerkasBimbingan } from "./entities/berkasBimbingan.entity"; +import { MataKuliah } from "./entities/mataKuliah.entity"; +import { SubmisiTugas } from "./entities/submisiTugas.entity"; +import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas.entity"; +import { BerkasTugas } from "./entities/berkasTugas.entity"; +import { TugasModule } from "./tugas/tugas.module"; import { KelasModule } from "./kelas/kelas.module"; -import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas"; -import { BerkasTugas } from "./entities/berkasTugas"; -import { SubmisiModule } from "./submisi-tugas/submisi.module"; +import { SubmisiTugasModule } from "./submisi-tugas/submisi-tugas.module"; +import { NilaiModule } from "./nilai/nilai.module"; +import { PendaftaranSidsem } from "./entities/pendaftaranSidsem"; +import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module"; @Module({ imports: [ @@ -54,8 +50,7 @@ import { SubmisiModule } from "./submisi-tugas/submisi.module"; BerkasBimbingan, Bimbingan, Pengguna, - RangeJadwalSeminar, - Seminar, + PendaftaranSidsem, Topik, AuditLog, DosenBimbingan, @@ -63,13 +58,9 @@ import { SubmisiModule } from "./submisi-tugas/submisi.module"; MahasiswaKelas, PengajarKelas, PendaftaranTesis, - RangeJadwalSidang, - Ruangan, - Sidang, + // Ruangan, Tugas, - PembimbingSeminar, - PembimbingSidang, - PengujiSidang, + PengujiSidsem, Konfigurasi, MataKuliah, SubmisiTugas, @@ -85,9 +76,11 @@ import { SubmisiModule } from "./submisi-tugas/submisi.module"; BimbinganModule, KonfigurasiModule, DosenBimbinganModule, - ApprovalModule, - SubmisiModule, KelasModule, + TugasModule, + SubmisiTugasModule, + NilaiModule, + DosenBimbinganModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/approval/approval.controller.ts b/src/approval/approval.controller.ts deleted file mode 100644 index 866410794f5f767cd84ad5489fdcdc5817c9024b..0000000000000000000000000000000000000000 --- a/src/approval/approval.controller.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Controller, Patch, Param, UseGuards } from "@nestjs/common"; -import { ApprovalService } from "./approval.service"; -import { - PendaftaranTesis, - RegStatus, -} from "src/entities/pendaftaranTesis.entity"; -import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; -import { RolesGuard } from "src/middlewares/roles.guard"; -import { RoleEnum } from "src/entities/pengguna.entity"; -import { Roles } from "src/middlewares/roles.decorator"; -import { - ApiBearerAuth, - ApiCookieAuth, - ApiOkResponse, - ApiTags, -} from "@nestjs/swagger"; -import { ByIdParamDto } from "./approval.dto"; - -@ApiTags("Approval") -@ApiCookieAuth() -@ApiBearerAuth() -@Controller("approval") -@UseGuards(CustomAuthGuard, RolesGuard) -@Roles(RoleEnum.S2_PEMBIMBING) -export class ApprovalController { - constructor(private readonly approvalService: ApprovalService) {} - - @ApiOkResponse({ type: PendaftaranTesis }) - @Patch(":id/approve") - async approvePendaftaran( - @Param() param: ByIdParamDto, - ): Promise<PendaftaranTesis> { - return this.approvalService.approvePendaftaran( - param.id, - RegStatus.APPROVED, - ); - } - - @ApiOkResponse({ type: PendaftaranTesis }) - @Patch(":id/reject") - async declinePendaftaran( - @Param() param: ByIdParamDto, - ): Promise<PendaftaranTesis> { - return this.approvalService.approvePendaftaran( - param.id, - RegStatus.REJECTED, - ); - } - - @ApiOkResponse({ type: PendaftaranTesis }) - @Patch(":id/interview") - async interviewPendaftaran( - @Param() param: ByIdParamDto, - ): Promise<PendaftaranTesis> { - return this.approvalService.approvePendaftaran( - param.id, - RegStatus.INTERVIEW, - ); - } -} diff --git a/src/approval/approval.dto.ts b/src/approval/approval.dto.ts deleted file mode 100644 index b9fea475cfd34a9705b8b8fcd66f0a69ca1995d2..0000000000000000000000000000000000000000 --- a/src/approval/approval.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsUUID } from "@nestjs/class-validator"; -import { ApiProperty } from "@nestjs/swagger"; - -export class ByIdParamDto { - @IsUUID() - @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) - id: string; -} diff --git a/src/approval/approval.module.ts b/src/approval/approval.module.ts deleted file mode 100644 index c28790252c59277b51e473a33ab7d8b38ae32065..0000000000000000000000000000000000000000 --- a/src/approval/approval.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from "@nestjs/common"; -import { TypeOrmModule } from "@nestjs/typeorm"; -import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity"; -import { ApprovalController } from "./approval.controller"; -import { ApprovalService } from "./approval.service"; - -@Module({ - imports: [TypeOrmModule.forFeature([PendaftaranTesis])], - controllers: [ApprovalController], - providers: [ApprovalService], -}) -export class ApprovalModule {} diff --git a/src/approval/approval.service.ts b/src/approval/approval.service.ts deleted file mode 100644 index 8163efeda7b43052e82b6a5441b5590f384f1b6a..0000000000000000000000000000000000000000 --- a/src/approval/approval.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { FindOneOptions, Repository } from "typeorm"; -import { - PendaftaranTesis, - RegStatus, -} from "src/entities/pendaftaranTesis.entity"; - -@Injectable() -export class ApprovalService { - constructor( - @InjectRepository(PendaftaranTesis) - private readonly pendaftaranRepository: Repository<PendaftaranTesis>, - ) {} - - async approvePendaftaran( - id: string, - status: RegStatus, - ): Promise<PendaftaranTesis> { - try { - const findOneOptions: FindOneOptions<PendaftaranTesis> = { - where: { id }, - }; - const pendaftaran = - await this.pendaftaranRepository.findOneOrFail(findOneOptions); - pendaftaran.status = status; - - return await this.pendaftaranRepository.save(pendaftaran); - } catch (error) { - throw new Error("Pendaftaran not found"); - } - } -} diff --git a/src/bimbingan/bimbingan.dto.ts b/src/bimbingan/bimbingan.dto.ts index 7409b67e8c4293b1d0565e02b61883b668acde42..f3afcdac8aeeb0380633e5609bc7b35d8c864013 100644 --- a/src/bimbingan/bimbingan.dto.ts +++ b/src/bimbingan/bimbingan.dto.ts @@ -14,7 +14,7 @@ import { PickType, } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { BerkasBimbingan } from "src/entities/berkasBimbingan"; +import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity"; import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity"; import { JalurEnum, diff --git a/src/bimbingan/bimbingan.module.ts b/src/bimbingan/bimbingan.module.ts index 25e02da5c5205c69b800de0b59e223c65948519c..30174c69ff5a33b4a5caa742095ef9f830c5eb8d 100644 --- a/src/bimbingan/bimbingan.module.ts +++ b/src/bimbingan/bimbingan.module.ts @@ -6,7 +6,7 @@ import { Bimbingan } from "src/entities/bimbingan.entity"; import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity"; import { Konfigurasi } from "src/entities/konfigurasi.entity"; import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; -import { BerkasBimbingan } from "src/entities/berkasBimbingan"; +import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity"; @Module({ imports: [ diff --git a/src/bimbingan/bimbingan.service.ts b/src/bimbingan/bimbingan.service.ts index 1947b81ab45a475f276c6a8d58c4ee043d8aaec6..52c1ba28e98f9c71aad2d88e5ebeafb8d1cb1d40 100644 --- a/src/bimbingan/bimbingan.service.ts +++ b/src/bimbingan/bimbingan.service.ts @@ -24,7 +24,7 @@ import { UpdateStatusDto, UpdateStatusResDto, } from "./bimbingan.dto"; -import { BerkasBimbingan } from "src/entities/berkasBimbingan"; +import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity"; @Injectable() export class BimbinganService { diff --git a/src/dashboard/dashboard.controller.ts b/src/dashboard/dashboard.controller.ts index bb16aebd0a8cb044554d293950f8056baa3b6c4b..31a3a4cc1dc88afe12ba4b05e16bc015fd2a41e9 100644 --- a/src/dashboard/dashboard.controller.ts +++ b/src/dashboard/dashboard.controller.ts @@ -8,6 +8,7 @@ import { AuthDto } from "src/auth/auth.dto"; import { Request } from "express"; import { DashboardDto, + DashboardMahasiswaResDto, GetDashboardDosbimQueryDto, JalurStatisticDto, } from "./dashboard.dto"; @@ -47,4 +48,16 @@ export class DashboardController { (request.user as AuthDto).id, ); } + + @UseGuards(CustomAuthGuard, RolesGuard) + @Roles(RoleEnum.S2_MAHASISWA) + @ApiOkResponse({ type: DashboardMahasiswaResDto }) + @Get("/mahasiswa") + async getDashboardMahasiswa( + @Req() request: Request, + ): Promise<DashboardMahasiswaResDto> { + return this.dashboardService.getDashboardMahasiswa( + (request.user as AuthDto).id, + ); + } } diff --git a/src/dashboard/dashboard.dto.ts b/src/dashboard/dashboard.dto.ts index 26fc7b8a907cc911108da94f0d34271f83643484..8d279937d0616eb007400145e622316c5d4b9012 100644 --- a/src/dashboard/dashboard.dto.ts +++ b/src/dashboard/dashboard.dto.ts @@ -1,7 +1,17 @@ -import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger"; -import { JalurEnum } from "../entities/pendaftaranTesis.entity"; +import { + ApiProperty, + ApiPropertyOptional, + OmitType, + PickType, +} from "@nestjs/swagger"; +import { + JalurEnum, + PendaftaranTesis, +} from "../entities/pendaftaranTesis.entity"; import { Topik } from "src/entities/topik.entity"; import { Pengguna } from "src/entities/pengguna.entity"; +import { Bimbingan } from "src/entities/bimbingan.entity"; +import { PendaftaranSidsem } from "src/entities/pendaftaranSidsem"; import { IsOptional } from "class-validator"; import { BimbinganStatus } from "src/entities/bimbingan.entity"; @@ -10,8 +20,38 @@ class PickedMhsDashboard extends PickType(Pengguna, [ "id", "nama", "nim", + "email", ] as const) {} +class OmittedTopikMhsDashboard extends OmitType(Topik, ["pengaju"] as const) {} + +class NoEmailUserDashboard extends OmitType(PickedMhsDashboard, [ + "email", +] as const) {} + +export class NoNIMUserDashboard extends OmitType(PickedMhsDashboard, [ + "nim", +] as const) {} + +class OmittedPendaftaranTesisMhsDashboard extends OmitType(PendaftaranTesis, [ + "mahasiswa", + "topik", + "penerima", +] as const) { + @ApiProperty() + topik: OmittedTopikMhsDashboard; + + @ApiProperty() + penerima: NoEmailUserDashboard; +} + +class SidsemWithPenguji extends OmitType(PendaftaranSidsem, [ + "penguji", +] as const) { + @ApiProperty({ type: [NoNIMUserDashboard] }) + penguji: NoNIMUserDashboard[]; +} + export class DashboardDto { @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) id: string; @@ -26,7 +66,7 @@ export class DashboardDto { topik: PickedTopikDashboard; @ApiProperty() - mahasiswa: PickedMhsDashboard; + mahasiswa: NoEmailUserDashboard; } export class JalurStatisticDto { @@ -37,6 +77,38 @@ export class JalurStatisticDto { count: number; } +export class DashboardMahasiswaResDto { + @ApiProperty() + mahasiswa: PickedMhsDashboard; + + @ApiProperty({ type: OmittedPendaftaranTesisMhsDashboard, nullable: true }) + pendaftaranTesis: OmittedPendaftaranTesisMhsDashboard; + + @ApiProperty({ type: [NoNIMUserDashboard] }) + dosenBimbingan: NoNIMUserDashboard[]; + + @ApiProperty({ type: [Bimbingan] }) + bimbingan: Bimbingan[]; + + @ApiProperty({ + type: PendaftaranSidsem, + nullable: true, + }) + seminarSatu: PendaftaranSidsem; + + @ApiProperty({ + type: SidsemWithPenguji, + nullable: true, + }) + seminarDua: SidsemWithPenguji; + + @ApiProperty({ + type: SidsemWithPenguji, + nullable: true, + }) + sidang: SidsemWithPenguji; +} + export class GetDashboardDosbimQueryDto { @ApiPropertyOptional({}) @IsOptional() diff --git a/src/dashboard/dashboard.module.ts b/src/dashboard/dashboard.module.ts index 160e96061fc16dbb1c6ee6f0328dcf31239e2dad..8f8f447d98f6ce5c3d40de228881aa244d364bba 100644 --- a/src/dashboard/dashboard.module.ts +++ b/src/dashboard/dashboard.module.ts @@ -6,11 +6,22 @@ import { PendaftaranTesis } from "../entities/pendaftaranTesis.entity"; import { Pengguna } from "../entities/pengguna.entity"; import { Topik } from "../entities/topik.entity"; import { Konfigurasi } from "src/entities/konfigurasi.entity"; +import { Bimbingan } from "src/entities/bimbingan.entity"; +import { PendaftaranSidsem } from "src/entities/pendaftaranSidsem"; +import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; import { BimbinganModule } from "src/bimbingan/bimbingan.module"; @Module({ imports: [ - TypeOrmModule.forFeature([PendaftaranTesis, Pengguna, Topik, Konfigurasi]), + TypeOrmModule.forFeature([ + PendaftaranTesis, + Pengguna, + Topik, + Konfigurasi, + Bimbingan, + PendaftaranSidsem, + DosenBimbingan, + ]), BimbinganModule, ], controllers: [DashboardController], diff --git a/src/dashboard/dashboard.service.ts b/src/dashboard/dashboard.service.ts index c4fce66ca46e0245a43efa486e49beea9ecf33df..c2bfcb12e4e7c8bbcbfdfacf6b77a224c475a5b1 100644 --- a/src/dashboard/dashboard.service.ts +++ b/src/dashboard/dashboard.service.ts @@ -7,7 +7,18 @@ import { } from "../entities/pendaftaranTesis.entity"; import { Pengguna } from "../entities/pengguna.entity"; import { Konfigurasi } from "src/entities/konfigurasi.entity"; -import { DashboardDto, JalurStatisticDto } from "./dashboard.dto"; +import { Bimbingan } from "src/entities/bimbingan.entity"; +import { + DashboardDto, + DashboardMahasiswaResDto, + JalurStatisticDto, + NoNIMUserDashboard, +} from "./dashboard.dto"; +import { + PendaftaranSidsem, + TipeSidsemEnum, +} from "src/entities/pendaftaranSidsem"; +import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; import { BimbinganService } from "src/bimbingan/bimbingan.service"; @Injectable() @@ -19,6 +30,12 @@ export class DashboardService { private penggunaRepository: Repository<Pengguna>, @InjectRepository(Konfigurasi) private konfigurasiRepository: Repository<Konfigurasi>, + @InjectRepository(Bimbingan) + private bimbinganRepository: Repository<Bimbingan>, + @InjectRepository(PendaftaranSidsem) + private pendaftaranSidsemRepository: Repository<PendaftaranSidsem>, + @InjectRepository(DosenBimbingan) + private dosenBimbinganRepository: Repository<DosenBimbingan>, private bimbinganService: BimbinganService, ) {} @@ -131,4 +148,126 @@ export class DashboardService { return statistics as JalurStatisticDto[]; } + + async getDashboardMahasiswa( + mahasiswaId: string, + ): Promise<DashboardMahasiswaResDto> { + const currentPeriode = await this.konfigurasiRepository.findOne({ + where: { key: process.env.KONF_PERIODE_KEY }, + }); + + const mahasiswaQuery = this.penggunaRepository + .createQueryBuilder("pengguna") + .select([ + "pengguna.id", + "pengguna.nama", + "pengguna.email", + "pengguna.nim", + ]) + .where("pengguna.id = :id", { id: mahasiswaId }); + const pendaftaranTesisQuery = this.pendaftaranTesisRepository + .createQueryBuilder("pendaftaranTesis") + .select([ + "pendaftaranTesis.id", + "pendaftaranTesis.jalurPilihan", + "pendaftaranTesis.waktuPengiriman", + "pendaftaranTesis.jadwalInterview", + "pendaftaranTesis.waktuKeputusan", + "pendaftaranTesis.status", + "penerima.id", + "penerima.nama", + "penerima.email", + ]) + .leftJoin("pendaftaranTesis.mahasiswa", "mahasiswa") + .leftJoinAndSelect("pendaftaranTesis.topik", "topik") + .leftJoin("pendaftaranTesis.penerima", "penerima") + .where("mahasiswa.id = :id", { id: mahasiswaId }) + .andWhere("topik.periode = :periode", { periode: currentPeriode.value }) + .orderBy("pendaftaranTesis.waktuPengiriman", "DESC"); + + const [mahasiswa, pendaftaranTesis] = await Promise.all([ + mahasiswaQuery.getOne(), + pendaftaranTesisQuery.getOne(), + ]); + + let dosenBimbingan: DosenBimbingan[] = []; + let bimbingan: Bimbingan[] = []; + let seminarSatu: PendaftaranSidsem | null = null; + let seminarDua: PendaftaranSidsem | null = null; + let sidang: PendaftaranSidsem | null = null; + + if (pendaftaranTesis) { + const dosenBimbinganQuery = this.dosenBimbinganRepository + .createQueryBuilder("dosenBimbingan") + .select(["dosen.id", "dosen.nama", "dosen.email"]) + .leftJoin("dosenBimbingan.dosen", "dosen") + .where("dosenBimbingan.idPendaftaran = :id", { + id: pendaftaranTesis.id, + }); + const bimbinganQuery = this.bimbinganRepository + .createQueryBuilder("bimbingan") + .leftJoinAndSelect("bimbingan.berkas", "berkas") + .where("bimbingan.pendaftaranId = :id", { + id: pendaftaranTesis.id, + }); + const [seminarSatuQuery, seminarDuaQuery, sidangQuery] = Object.values( + TipeSidsemEnum, + ).map((tipe) => { + let temp = this.pendaftaranSidsemRepository + .createQueryBuilder("pendaftaranSidsem") + .leftJoinAndSelect("pendaftaranSidsem.ruangan", "ruangan") + .where("pendaftaranSidsem.pendaftaranTesisId = :id", { + id: pendaftaranTesis.id, + }) + .andWhere("pendaftaranSidsem.tipe = :tipe", { + tipe, + }) + .andWhere("NOT pendaftaranSidsem.ditolak"); + + if (tipe !== TipeSidsemEnum.SEMINAR_1) { + temp = temp + .leftJoinAndSelect("pendaftaranSidsem.penguji", "penguji") + .leftJoinAndSelect("penguji.dosen", "dosen"); + } + + return temp; + }); + + [dosenBimbingan, bimbingan, seminarSatu, seminarDua, sidang] = + await Promise.all([ + dosenBimbinganQuery.getMany(), + bimbinganQuery.getMany(), + seminarSatuQuery.getOne(), + seminarDuaQuery.getOne(), + sidangQuery.getOne(), + ]); + } + + return { + mahasiswa, + pendaftaranTesis, + dosenBimbingan: + dosenBimbingan.length > 0 + ? (dosenBimbingan as any as NoNIMUserDashboard[]) + : [pendaftaranTesis.penerima], + bimbingan, + seminarSatu, + seminarDua: { + ...seminarDua, + penguji: seminarDua?.penguji.map((p) => ({ + id: p.dosen.id, + nama: p.dosen.nama, + email: p.dosen.email, + })), + }, + sidang: { + ...sidang, + penguji: sidang?.penguji.map((p) => ({ + id: p.dosen.id, + nama: p.dosen.nama, + email: p.dosen.email, + })), + }, + }; + } } diff --git a/src/dosen-bimbingan/dosen-bimbingan.controller.ts b/src/dosen-bimbingan/dosen-bimbingan.controller.ts index ef0b6acae18dc2d24c5e25dbdf88eaa8158a46df..81a31679c5443f03c6469b478be79566aa2e80b0 100644 --- a/src/dosen-bimbingan/dosen-bimbingan.controller.ts +++ b/src/dosen-bimbingan/dosen-bimbingan.controller.ts @@ -1,30 +1,16 @@ -import { - Body, - Controller, - Delete, - Get, - NotFoundException, - Put, - Query, - UseGuards, -} from "@nestjs/common"; +import { Controller, Get, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, + ApiOperation, ApiTags, } from "@nestjs/swagger"; import { RoleEnum } from "src/entities/pengguna.entity"; import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; import { Roles } from "src/middlewares/roles.decorator"; import { RolesGuard } from "src/middlewares/roles.guard"; -import { - DosbimOptQueryDto, - DosbimQueryDto, - GetDosbimResDto, - SuccessResDto, - UpdateDosbimDto, -} from "./dosen-bimbingan.dto"; +import { GetDosbimResDto } from "./dosen-bimbingan.dto"; import { DosenBimbinganService } from "./dosen-bimbingan.service"; @ApiTags("Dosen Bimbingan") @@ -32,49 +18,17 @@ import { DosenBimbinganService } from "./dosen-bimbingan.service"; @ApiBearerAuth() @Controller("dosen-bimbingan") @UseGuards(CustomAuthGuard, RolesGuard) +@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_MAHASISWA) export class DosenBimbinganController { constructor(private readonly dosbimService: DosenBimbinganService) {} @ApiOkResponse({ type: [GetDosbimResDto] }) - @Roles( - RoleEnum.ADMIN, - RoleEnum.S2_TIM_TESIS, - RoleEnum.S2_MAHASISWA, - RoleEnum.S2_TIM_TESIS, - ) + @ApiOperation({ + summary: + "Get all available dosen bimbingan. Roles: ADMIN, S2_TIM_TESIS, S2_MAHASISWA", + }) @Get() - async get(@Query() query: DosbimOptQueryDto) { - if (!query.regId) return await this.dosbimService.getAll(); - - const res = await this.dosbimService.findByRegId(query.regId); - const mappedRes: GetDosbimResDto[] = res.map((r) => r.dosen); - return mappedRes; - } - - @ApiOkResponse({ type: SuccessResDto }) - @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) - @Put() - async updateByRegId( - @Query() query: DosbimQueryDto, - @Body() body: UpdateDosbimDto, - ): Promise<SuccessResDto> { - await this.dosbimService.updateByRegId(query.regId, body.dosbimIds); - - return { - status: "ok", - }; - } - - @ApiOkResponse({ type: SuccessResDto }) - @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) - @Delete() - async deleteByRegId(@Query() query: DosbimQueryDto): Promise<SuccessResDto> { - const res = await this.dosbimService.removeByRegId(query.regId); - - if (!res.affected) throw new NotFoundException(); - - return { - status: "ok", - }; + async get() { + return await this.dosbimService.getAll(); } } diff --git a/src/dosen-bimbingan/dosen-bimbingan.dto.ts b/src/dosen-bimbingan/dosen-bimbingan.dto.ts index 11d56b1135d145102b6ac69a56474ae66975dac5..23c823e7389a759b7d532c02543e736d5bbaf0d4 100644 --- a/src/dosen-bimbingan/dosen-bimbingan.dto.ts +++ b/src/dosen-bimbingan/dosen-bimbingan.dto.ts @@ -1,12 +1,6 @@ -import { - ArrayMaxSize, - IsArray, - IsUUID, - ArrayMinSize, - ArrayUnique, - IsOptional, -} from "@nestjs/class-validator"; -import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger"; +import { IsOptional } from "@nestjs/class-validator"; +import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; +import { IsUUID } from "class-validator"; import { Pengguna } from "src/entities/pengguna.entity"; export class DosbimOptQueryDto { @@ -16,30 +10,6 @@ export class DosbimOptQueryDto { regId?: string; } -export class DosbimQueryDto { - @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) - @IsUUID() - regId: string; -} - -export class UpdateDosbimDto { - @ApiProperty({ - type: [String], - example: ["550e8400-e29b-41d4-a716-446655440000"], - }) - @IsArray() - @IsUUID("all", { each: true }) - @ArrayMinSize(1) - @ArrayMaxSize(3) - @ArrayUnique() - dosbimIds: string[]; -} - -export class SuccessResDto { - @ApiProperty() - status: string; -} - export class GetDosbimResDto extends PickType(Pengguna, [ "id", "email", diff --git a/src/dosen-bimbingan/dosen-bimbingan.service.ts b/src/dosen-bimbingan/dosen-bimbingan.service.ts index 456364ee4bc51946474c2cd5e54e92fc26909fe8..88f0f9d159f8f0e4949c06a4c5bbcb7ad9a9643a 100644 --- a/src/dosen-bimbingan/dosen-bimbingan.service.ts +++ b/src/dosen-bimbingan/dosen-bimbingan.service.ts @@ -1,29 +1,13 @@ -import { - BadRequestException, - Injectable, - InternalServerErrorException, -} from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; -import { - PendaftaranTesis, - RegStatus, -} from "src/entities/pendaftaranTesis.entity"; import { Pengguna, RoleEnum } from "src/entities/pengguna.entity"; -import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; -import { ArrayContains, DataSource, Repository } from "typeorm"; +import { ArrayContains, Repository } from "typeorm"; @Injectable() export class DosenBimbinganService { constructor( - @InjectRepository(DosenBimbingan) - private dosbimRepo: Repository<DosenBimbingan>, @InjectRepository(Pengguna) private penggunaRepo: Repository<Pengguna>, - @InjectRepository(PendaftaranTesis) - private pendaftaranRepo: Repository<PendaftaranTesis>, - private konfService: KonfigurasiService, - private dataSource: DataSource, ) {} async getAll() { @@ -38,97 +22,4 @@ export class DosenBimbinganService { }, }); } - - async findByRegId(regId: string) { - return await this.dosbimRepo.find({ - select: { - id: true, - dosen: { - id: true, - nama: true, - email: true, - }, - }, - relations: { - dosen: true, - }, - where: { - pendaftaran: { - id: regId, - }, - }, - }); - } - - async updateByRegId(regId: string, dosbimIds: string[]) { - const [reg, currPeriod] = await Promise.all([ - this.pendaftaranRepo.findOne({ - select: { id: true, status: true }, - where: { id: regId }, - relations: { topik: true }, - }), - this.konfService.getKonfigurasiByKey(process.env.KONF_PERIODE_KEY), - ]); - - if (!reg || reg.status !== RegStatus.APPROVED) { - throw new BadRequestException( - "Registrasi tidak ditemukan atau tidak disetujui.", - ); - } - - if (!currPeriod || currPeriod !== reg.topik.periode) { - throw new BadRequestException( - "Periode belum dikonfigurasi atau tidak sesuai dengan periode sekarang.", - ); - } - - for (const dosbimId of dosbimIds) { - const res = await this.penggunaRepo.findOne({ - select: { - id: true, - }, - where: { - id: dosbimId, - roles: ArrayContains([RoleEnum.S2_PEMBIMBING]), - }, - }); - - if (!res) { - throw new BadRequestException("Invalid pembimbing id"); - } - } - - const queryRunner = this.dataSource.createQueryRunner(); - - await queryRunner.connect(); - await queryRunner.startTransaction(); - try { - await this.removeByRegId(regId); - - for (const dosbimId of dosbimIds) { - await queryRunner.manager.getRepository(DosenBimbingan).insert({ - idPendaftaran: regId, - idDosen: dosbimId, - }); - } - - await queryRunner.commitTransaction(); - } catch (err) { - await queryRunner.rollbackTransaction(); - - throw new InternalServerErrorException(); - } finally { - await queryRunner.release(); - } - } - - async removeByRegId(regId: string) { - return await this.dosbimRepo - .createQueryBuilder() - .delete() - .where("idPendaftaran = :idPendaftaran", { - idPendaftaran: regId, - }) - .execute(); - } } diff --git a/src/entities/berkasBimbingan.ts b/src/entities/berkasBimbingan.entity.ts similarity index 77% rename from src/entities/berkasBimbingan.ts rename to src/entities/berkasBimbingan.entity.ts index 6acceea0c51e5cfaa149724cb5f7824fa331544c..35b44ace3f7a70db3a1bcbd210c3a95be701c99a 100644 --- a/src/entities/berkasBimbingan.ts +++ b/src/entities/berkasBimbingan.entity.ts @@ -9,7 +9,9 @@ export class BerkasBimbingan { @PrimaryGeneratedColumn("uuid") id: string; - @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id) + @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id, { + orphanedRowAction: "delete", + }) bimbingan: Bimbingan; @Column({ type: "text" }) @@ -19,6 +21,6 @@ export class BerkasBimbingan { @Column({ type: "text" }) @IsUrl() - @ApiProperty() + @ApiProperty({ example: "https://example.com/berkas.pdf" }) url: string; } diff --git a/src/entities/berkasSubmisiTugas.entity.ts b/src/entities/berkasSubmisiTugas.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..e48d5e8d88f1449b767d3e88f1f9862e8e0f6560 --- /dev/null +++ b/src/entities/berkasSubmisiTugas.entity.ts @@ -0,0 +1,28 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { SubmisiTugas } from "./submisiTugas.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsUUID } from "@nestjs/class-validator"; +import { IsUrl } from "class-validator"; + +@Entity() +export class BerkasSubmisiTugas { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @PrimaryGeneratedColumn("uuid") + id: string; + + @ManyToOne(() => SubmisiTugas, (submisi) => submisi.id, { + orphanedRowAction: "delete", + }) + submisiTugas: SubmisiTugas; + + @ApiProperty() + @IsString() + @Column({ type: "text" }) + nama: string; + + @ApiProperty({ example: "https://example.com/berkas.pdf" }) + @IsUrl() + @Column({ type: "text" }) + url: string; +} diff --git a/src/entities/berkasSubmisiTugas.ts b/src/entities/berkasSubmisiTugas.ts deleted file mode 100644 index 9694eb7069548036483845915c5a61b8fc7e2a75..0000000000000000000000000000000000000000 --- a/src/entities/berkasSubmisiTugas.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { SubmisiTugas } from "./submisiTugas"; - -@Entity() -export class BerkasSubmisiTugas { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => SubmisiTugas, (submisi) => submisi.id) - submisiTugas: SubmisiTugas; - - @Column({ type: "text" }) - nama: string; - - @Column({ type: "text" }) - url: string; -} diff --git a/src/entities/berkasTugas.entity.ts b/src/entities/berkasTugas.entity.ts new file mode 100644 index 0000000000000000000000000000000000000000..c18bccfa9c839754ca8ed7bb0f5789f8841e3ff1 --- /dev/null +++ b/src/entities/berkasTugas.entity.ts @@ -0,0 +1,26 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Tugas } from "./tugas.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsUrl, IsUUID } from "class-validator"; +import { IsString } from "@nestjs/class-validator"; + +@Entity() +export class BerkasTugas { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @PrimaryGeneratedColumn("uuid") + id: string; + + @ManyToOne(() => Tugas, (tugas) => tugas.id, { orphanedRowAction: "delete" }) + tugas: Tugas; + + @ApiProperty() + @IsString() + @Column({ type: "text" }) + nama: string; + + @ApiProperty({ example: "https://example.com/berkas.pdf" }) + @IsUrl() + @Column({ type: "text" }) + url: string; +} diff --git a/src/entities/berkasTugas.ts b/src/entities/berkasTugas.ts deleted file mode 100644 index 91cf3ec86a5b6d8f3b243e6d076e41bfa39adcbd..0000000000000000000000000000000000000000 --- a/src/entities/berkasTugas.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Tugas } from "./tugas.entity"; - -@Entity() -export class BerkasTugas { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Tugas, (tugas) => tugas.id) - tugas: Tugas; - - @Column({ type: "text" }) - nama: string; - - @Column({ type: "text" }) - url: string; -} diff --git a/src/entities/bimbingan.entity.ts b/src/entities/bimbingan.entity.ts index 33df7a17badd1b84d408bb313c47c46fad2410b7..a846e824d3d98389e6a2402efad351f49188f9fb 100644 --- a/src/entities/bimbingan.entity.ts +++ b/src/entities/bimbingan.entity.ts @@ -7,7 +7,7 @@ import { } from "typeorm"; import { PendaftaranTesis } from "./pendaftaranTesis.entity"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { BerkasBimbingan } from "./berkasBimbingan"; +import { BerkasBimbingan } from "./berkasBimbingan.entity"; export enum BimbinganStatus { LANCAR = "LANCAR", diff --git a/src/entities/kelas.entity.ts b/src/entities/kelas.entity.ts index cc9df1e26aee879759d3260c02fa58378c7a5238..deec327ed9a9028f7eb8ddd886826f35a3294c80 100644 --- a/src/entities/kelas.entity.ts +++ b/src/entities/kelas.entity.ts @@ -6,10 +6,10 @@ import { OneToMany, PrimaryGeneratedColumn, } from "typeorm"; -import { MataKuliah } from "./mataKuliah"; +import { MataKuliah } from "./mataKuliah.entity"; import { ApiProperty } from "@nestjs/swagger"; import { PengajarKelas } from "./pengajarKelas.entity"; -import { MahasiswaKelas } from "./mahasiswaKelas"; +import { MahasiswaKelas } from "./mahasiswaKelas.entity"; import { IsPositive, IsString, @@ -17,6 +17,7 @@ import { Length, MaxLength, } from "@nestjs/class-validator"; +import { Tugas } from "./tugas.entity"; @Entity() export class Kelas { @@ -35,6 +36,7 @@ export class Kelas { @Column({ type: "text" }) periode: string; + @ApiProperty({ type: MataKuliah }) @ManyToOne(() => MataKuliah, (mataKuliah) => mataKuliah.kode) @JoinColumn({ name: "mataKuliahKode" }) mataKuliah: MataKuliah; @@ -56,4 +58,7 @@ export class Kelas { @OneToMany(() => MahasiswaKelas, (mahasiswa) => mahasiswa.kelas) mahasiswa: MahasiswaKelas[]; + + @OneToMany(() => Tugas, (tugas) => tugas.kelas) + tugas: Tugas[]; } diff --git a/src/entities/mahasiswaKelas.ts b/src/entities/mahasiswaKelas.entity.ts similarity index 96% rename from src/entities/mahasiswaKelas.ts rename to src/entities/mahasiswaKelas.entity.ts index 7d47e0abfa6461f68fc0fa11ef736601cd397c93..0753aa6f75f865913b7e42209248310948dc67c2 100644 --- a/src/entities/mahasiswaKelas.ts +++ b/src/entities/mahasiswaKelas.entity.ts @@ -28,5 +28,5 @@ export class MahasiswaKelas { mahasiswaId: string; @Column({ type: "real", nullable: true }) - nilaiAkhir: number; + nilaiAkhir?: number; } diff --git a/src/entities/mataKuliah.ts b/src/entities/mataKuliah.entity.ts similarity index 70% rename from src/entities/mataKuliah.ts rename to src/entities/mataKuliah.entity.ts index 6f78c692074b3a7c80123deac69c84643c1c7a11..9e1fed5d807a176b89288144623ec72575ef5e33 100644 --- a/src/entities/mataKuliah.ts +++ b/src/entities/mataKuliah.entity.ts @@ -1,6 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsString, Length, MaxLength } from "class-validator"; -import { Column, Entity, PrimaryColumn } from "typeorm"; +import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm"; +import { Kelas } from "./kelas.entity"; @Entity() export class MataKuliah { @@ -15,4 +16,7 @@ export class MataKuliah { @MaxLength(256) @Column({ type: "varchar", length: 256 }) nama: string; + + @OneToMany(() => Kelas, (kelas) => kelas.mataKuliah) + kelas: Kelas[]; } diff --git a/src/entities/pembimbingSidang.entity.ts b/src/entities/pembimbingSidang.entity.ts deleted file mode 100644 index a3d9dbec55b4d3e94d7a1e6d8b14262b10752e3d..0000000000000000000000000000000000000000 --- a/src/entities/pembimbingSidang.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Sidang } from "./sidang.entity"; -import { Pengguna } from "./pengguna.entity"; - -@Entity() -export class PembimbingSidang { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Sidang, (sidang) => sidang.id) - sidang: Sidang; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - dosen: Pengguna; -} diff --git a/src/entities/pendaftaranSidsem.ts b/src/entities/pendaftaranSidsem.ts new file mode 100644 index 0000000000000000000000000000000000000000..a733c9cd7aedf8825202a92c3b509e271486847f --- /dev/null +++ b/src/entities/pendaftaranSidsem.ts @@ -0,0 +1,62 @@ +import { + Column, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from "typeorm"; +import { PendaftaranTesis } from "./pendaftaranTesis.entity"; +// import { Ruangan } from "./ruangan.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { PengujiSidsem } from "./pengujiSidsem.entity"; + +export enum TipeSidsemEnum { + SEMINAR_1 = "SEMINAR_1", + SEMINAR_2 = "SEMINAR_2", + SIDANG = "SIDANG", +} + +@Entity() +export class PendaftaranSidsem { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @PrimaryGeneratedColumn("uuid") + id: string; + + @ApiProperty({ enum: TipeSidsemEnum }) + @Column({ type: "enum", enum: TipeSidsemEnum }) + tipe: TipeSidsemEnum; + + @ApiProperty() + @Column({ type: "boolean", default: false }) + ditolak: boolean; + + @ApiProperty() + @Column({ type: "boolean", nullable: true }) + lulus: boolean; + + @ApiProperty() + @Column({ type: "timestamptz", nullable: true }) + waktuMulai: Date; + + @ApiProperty() + @Column({ type: "timestamptz", nullable: true }) + waktuSelesai: Date; + + @ApiProperty() + @Column({ type: "text", nullable: true }) + linkw2m: string; + + @ManyToOne(() => PendaftaranTesis, (pendaftaranTesis) => pendaftaranTesis.id) + pendaftaranTesis: PendaftaranTesis; + + // @ApiProperty({ type: Ruangan, nullable: true }) + // @ManyToOne(() => Ruangan, (ruangan) => ruangan.id) + // ruangan: Ruangan; + + @ApiProperty() + @Column({ type: "text", nullable: true }) + ruangan: string; + + @OneToMany(() => PengujiSidsem, (pengujiSidsem) => pengujiSidsem.sidsem) + penguji: PengujiSidsem[]; +} diff --git a/src/entities/pendaftaranTesis.entity.ts b/src/entities/pendaftaranTesis.entity.ts index 0d4c46157b1411288e0788be8d9b3052a1650a6e..e9a8fa31c2f525bcc45c5d87cf250d018901ceda 100644 --- a/src/entities/pendaftaranTesis.entity.ts +++ b/src/entities/pendaftaranTesis.entity.ts @@ -1,6 +1,7 @@ import { Column, Entity, + JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -55,12 +56,19 @@ export class PendaftaranTesis { @Column({ type: "enum", enum: RegStatus, default: RegStatus.NOT_ASSIGNED }) status: RegStatus; - @ManyToOne(() => Topik, (topik) => topik.id) + @ApiProperty({ type: Topik }) + @ManyToOne(() => Topik, (topik) => topik.id, { cascade: true }) topik: Topik; + @ApiProperty() @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) + @JoinColumn({ name: "mahasiswaId" }) mahasiswa: Pengguna; + @Column() + mahasiswaId: string; + + @ApiProperty() @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) penerima: Pengguna; diff --git a/src/entities/pengguna.entity.ts b/src/entities/pengguna.entity.ts index 5739584b8d2bb86cfc6538e9180cd887505e684c..c28a606ea4031436e36fc7958c0756a78f898cbe 100644 --- a/src/entities/pengguna.entity.ts +++ b/src/entities/pengguna.entity.ts @@ -1,9 +1,12 @@ +import { IsString } from "@nestjs/class-validator"; import { ApiHideProperty, ApiProperty, ApiPropertyOptional, } from "@nestjs/swagger"; -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { PendaftaranTesis } from "./pendaftaranTesis.entity"; +import { SubmisiTugas } from "./submisiTugas.entity"; export enum RoleEnum { ADMIN = "ADMIN", @@ -50,4 +53,15 @@ export class Pengguna { default: [], }) roles: RoleEnum[]; + + @ApiPropertyOptional() + @IsString() + @Column({ type: "text", nullable: true }) + kontak: string; + + @OneToMany(() => PendaftaranTesis, (pendaftaran) => pendaftaran.mahasiswa) + pendaftaranTesis: PendaftaranTesis[]; + + @OneToMany(() => SubmisiTugas, (submisiTugas) => submisiTugas.mahasiswa) + submisiTugas: SubmisiTugas[]; } diff --git a/src/entities/pengujiSidang.entity.ts b/src/entities/pengujiSidang.entity.ts deleted file mode 100644 index dfcd36b5c2f5e06bebad302b3488aedff86338c7..0000000000000000000000000000000000000000 --- a/src/entities/pengujiSidang.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Sidang } from "./sidang.entity"; -import { Pengguna } from "./pengguna.entity"; - -@Entity() -export class PengujiSidang { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Sidang, (sidang) => sidang.id) - sidang: Sidang; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - dosen: Pengguna; -} diff --git a/src/entities/pembimbingSeminar.entity.ts b/src/entities/pengujiSidsem.entity.ts similarity index 54% rename from src/entities/pembimbingSeminar.entity.ts rename to src/entities/pengujiSidsem.entity.ts index 024ebe5b57e6835441faa33d687137fb82565cbb..09aebe7c16acbb068ae8a26c341b02e996e6a7a3 100644 --- a/src/entities/pembimbingSeminar.entity.ts +++ b/src/entities/pengujiSidsem.entity.ts @@ -1,14 +1,17 @@ import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Seminar } from "./seminar.entity"; import { Pengguna } from "./pengguna.entity"; +import { PendaftaranSidsem } from "./pendaftaranSidsem"; @Entity() -export class PembimbingSeminar { +export class PengujiSidsem { @PrimaryGeneratedColumn("uuid") id: string; - @ManyToOne(() => Seminar, (seminar) => seminar.id) - seminar: Seminar; + @ManyToOne( + () => PendaftaranSidsem, + (pendaftaranSidsem) => pendaftaranSidsem.id, + ) + sidsem: PendaftaranSidsem; @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) dosen: Pengguna; diff --git a/src/entities/rangeJadwalSeminar.entity.ts b/src/entities/rangeJadwalSeminar.entity.ts deleted file mode 100644 index fa16bf749d40cdfeff7253f61bc098063a7348a3..0000000000000000000000000000000000000000 --- a/src/entities/rangeJadwalSeminar.entity.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Entity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class RangeJadwalSeminar { - @PrimaryGeneratedColumn("uuid") - id: string; -} diff --git a/src/entities/rangeJadwalSidang.entity.ts b/src/entities/rangeJadwalSidang.entity.ts deleted file mode 100644 index 6479aef875d9d7b23cd9db1c00317af5e60756cd..0000000000000000000000000000000000000000 --- a/src/entities/rangeJadwalSidang.entity.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Entity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class RangeJadwalSidang { - @PrimaryGeneratedColumn("uuid") - id: string; -} diff --git a/src/entities/ruangan.entity.ts b/src/entities/ruangan.entity.ts index 97df4c092f30bd329d8c3ece86d385bd0e6300c2..86dfaf804683751d50f397df3a55f00488dc0bfa 100644 --- a/src/entities/ruangan.entity.ts +++ b/src/entities/ruangan.entity.ts @@ -1,7 +1,13 @@ -import { Entity, PrimaryGeneratedColumn } from "typeorm"; +// import { ApiProperty } from "@nestjs/swagger"; +// import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; -@Entity() -export class Ruangan { - @PrimaryGeneratedColumn("uuid") - id: string; -} +// @Entity() +// export class Ruangan { +// @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) +// @PrimaryGeneratedColumn("uuid") +// id: string; + +// @ApiProperty() +// @Column({ type: "text" }) +// nama: string; +// } diff --git a/src/entities/seminar.entity.ts b/src/entities/seminar.entity.ts deleted file mode 100644 index 7b0e30b783be1e1401448e73f5fd078f7d4cdd0c..0000000000000000000000000000000000000000 --- a/src/entities/seminar.entity.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Pengguna } from "./pengguna.entity"; -import { RangeJadwalSeminar } from "./rangeJadwalSeminar.entity"; -import { Ruangan } from "./ruangan.entity"; - -@Entity() -export class Seminar { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - mahasiswa: Pengguna; - - @ManyToOne( - () => RangeJadwalSeminar, - (rangeJadwalSeminar) => rangeJadwalSeminar.id, - ) - rangeJadwal: RangeJadwalSeminar; - - @ManyToOne(() => Ruangan, (ruangan) => ruangan.id) - ruangan: Ruangan; -} diff --git a/src/entities/sidang.entity.ts b/src/entities/sidang.entity.ts deleted file mode 100644 index c2dc57ecdca72af17a7496883e74a716f46dbf1c..0000000000000000000000000000000000000000 --- a/src/entities/sidang.entity.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Pengguna } from "./pengguna.entity"; -import { RangeJadwalSidang } from "./rangeJadwalSidang.entity"; -import { Ruangan } from "./ruangan.entity"; - -@Entity() -export class Sidang { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - mahasiswa: Pengguna; - - @ManyToOne( - () => RangeJadwalSidang, - (rangeJadwalSidang) => rangeJadwalSidang.id, - ) - rangeJadwal: RangeJadwalSidang; - - @ManyToOne(() => Ruangan, (ruangan) => ruangan.id) - ruangan: Ruangan; -} diff --git a/src/entities/submisiTugas.ts b/src/entities/submisiTugas.entity.ts similarity index 50% rename from src/entities/submisiTugas.ts rename to src/entities/submisiTugas.entity.ts index ef0c8da1a854dc6b985b23b313a65eaba2aa7d22..be380d37d6c0b4dadf641f794735a343990e13b5 100644 --- a/src/entities/submisiTugas.ts +++ b/src/entities/submisiTugas.entity.ts @@ -1,34 +1,53 @@ import { Column, Entity, + JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, } from "typeorm"; import { Pengguna } from "./pengguna.entity"; import { Tugas } from "./tugas.entity"; -import { BerkasSubmisiTugas } from "./berkasSubmisiTugas"; +import { BerkasSubmisiTugas } from "./berkasSubmisiTugas.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsString, IsUUID } from "class-validator"; +import { IsBoolean } from "@nestjs/class-validator"; @Entity() export class SubmisiTugas { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() @PrimaryGeneratedColumn("uuid") id: string; @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) + @JoinColumn({ name: "mahasiswaId" }) mahasiswa: Pengguna; + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @Column() + mahasiswaId: string; + + @ApiProperty() + @IsString() @Column({ type: "text" }) jawaban: string; + @ApiProperty({ description: "true means submitted, false means draft" }) + @IsBoolean() @Column({ type: "boolean" }) isSubmitted: boolean; // false means draft (saved), true means submitted + @ApiProperty({ type: [BerkasSubmisiTugas] }) @OneToMany( () => BerkasSubmisiTugas, (berkasSubmisiTugas) => berkasSubmisiTugas.submisiTugas, + { cascade: true }, ) berkasSubmisiTugas: BerkasSubmisiTugas[]; + @ApiProperty() @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP", @@ -37,5 +56,11 @@ export class SubmisiTugas { submittedAt: Date; @ManyToOne(() => Tugas, (tugas) => tugas.id) + @JoinColumn({ name: "tugasId" }) tugas: Tugas; + + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @Column() + tugasId: string; } diff --git a/src/entities/tugas.entity.ts b/src/entities/tugas.entity.ts index 563bcb32a0d12b55e6e3a30232bea75a162a233d..2424b87973fd01fe854e40ca50079dbc40e0f4ee 100644 --- a/src/entities/tugas.entity.ts +++ b/src/entities/tugas.entity.ts @@ -1,46 +1,88 @@ import { Column, Entity, + JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, } from "typeorm"; import { Kelas } from "./kelas.entity"; import { Pengguna } from "./pengguna.entity"; -import { BerkasTugas } from "./berkasTugas"; +import { BerkasTugas } from "./berkasTugas.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsDateString, IsUUID, MaxLength } from "class-validator"; +import { IsString } from "@nestjs/class-validator"; +import { SubmisiTugas } from "./submisiTugas.entity"; @Entity() export class Tugas { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() @PrimaryGeneratedColumn("uuid") id: string; @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) + @JoinColumn({ name: "pembuatId" }) pembuat: Pengguna; + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @Column() + pembuatId: string; + @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) + @JoinColumn({ name: "pengubahId" }) pengubah: Pengguna; + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @Column() + pengubahId: string; + + @ApiProperty() + @IsString() + @MaxLength(256) @Column({ type: "varchar", length: 256 }) judul: string; + @ApiProperty() + @IsDateString() @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" }) waktuMulai: Date; + @ApiProperty() + @IsDateString() @Column({ type: "timestamptz" }) waktuSelesai: Date; + @ApiProperty() + @IsString() @Column({ type: "text" }) deskripsi: string; - @OneToMany(() => BerkasTugas, (berkasTugas) => berkasTugas.tugas) + @ApiProperty({ type: [BerkasTugas] }) + @OneToMany(() => BerkasTugas, (berkasTugas) => berkasTugas.tugas, { + cascade: true, + }) berkasTugas: BerkasTugas[]; + @ApiProperty() @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" }) createdAt: Date; + @ApiProperty() @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" }) updatedAt: Date; @ManyToOne(() => Kelas, (kelas) => kelas.id) + @JoinColumn({ name: "kelasId" }) kelas: Kelas; + + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + @Column() + kelasId: string; + + @OneToMany(() => SubmisiTugas, (submisiTugas) => submisiTugas.tugas) + submisiTugas: SubmisiTugas[]; } diff --git a/src/kelas/kelas.controller.ts b/src/kelas/kelas.controller.ts index 88460f5dd5acbe94f91341ea795f387f2d02fe17..ae942b029bd3415ece79a2d5df0b3b4ad5befd3a 100644 --- a/src/kelas/kelas.controller.ts +++ b/src/kelas/kelas.controller.ts @@ -12,10 +12,12 @@ import { UseGuards, } from "@nestjs/common"; import { + ByIdKelasDto, CreateKelasDto, DeleteKelasDto, + GetKelasDetailRespDto, GetKelasQueryDto, - GetListKelasRespDto, + GetKelasRespDto, GetNextNomorResDto, IdKelasResDto, KodeRespDto, @@ -32,13 +34,14 @@ import { ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOkResponse, + ApiOperation, ApiTags, } from "@nestjs/swagger"; import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; import { RolesGuard } from "src/middlewares/roles.guard"; import { KelasService } from "./kelas.service"; import { Roles } from "src/middlewares/roles.decorator"; -import { MataKuliah } from "src/entities/mataKuliah"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; import { Kelas } from "src/entities/kelas.entity"; @ApiTags("Kelas") @@ -49,7 +52,11 @@ import { Kelas } from "src/entities/kelas.entity"; export class KelasController { constructor(private readonly kelasServ: KelasService) {} - @ApiOkResponse({ type: GetListKelasRespDto, isArray: true }) + @ApiOperation({ + summary: + "Get list of kelas. Roles: S2_KULIAH, S2_MAHASISWA, S2_TIM_TESIS, ADMIN", + }) + @ApiOkResponse({ type: GetKelasRespDto, isArray: true }) @Roles( RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA, @@ -148,4 +155,77 @@ export class KelasController { async delete(@Body() body: DeleteKelasDto): Promise<Kelas> { return await this.kelasServ.delete(body); } + + @Roles( + RoleEnum.S2_TIM_TESIS, + RoleEnum.ADMIN, + RoleEnum.S2_KULIAH, + RoleEnum.S2_MAHASISWA, + ) + @ApiOkResponse({ type: GetKelasRespDto }) + @ApiOperation({ + summary: + "Get kelas general information by kelas id. Roles: S2_KULIAH, S2_MAHASISWA, S2_TIM_TESIS, ADMIN", + }) + @Get("/:id") + async getById( + @Param() param: ByIdKelasDto, + @Req() req: Request, + ): Promise<GetKelasRespDto> { + let idMahasiswa = undefined; + let idPengajar = undefined; + + const { id, roles } = req.user as AuthDto; + + if ( + !roles.includes(RoleEnum.S2_TIM_TESIS) && + !roles.includes(RoleEnum.ADMIN) + ) { + if (roles.includes(RoleEnum.S2_KULIAH)) { + idPengajar = id; + } else { + // requester only has S2_MAHASISWA access + idMahasiswa = id; + } + } + + return await this.kelasServ.getById(param.id, idMahasiswa, idPengajar); + } + + @Roles( + RoleEnum.S2_TIM_TESIS, + RoleEnum.ADMIN, + RoleEnum.S2_KULIAH, + RoleEnum.S2_MAHASISWA, + ) + @ApiOperation({ + summary: + "Get kelas mahasiswa and pengajar list by kelas id. Roles: S2_KULIAH, S2_MAHASISWA, S2_TIM_TESIS, ADMIN", + }) + @ApiOkResponse({ type: GetKelasDetailRespDto }) + @Get("/:id/detail") + async getKelasDetail(@Param() param: ByIdKelasDto, @Req() req: Request) { + let idMahasiswa = undefined; + let idPengajar = undefined; + + const { id, roles } = req.user as AuthDto; + + if ( + !roles.includes(RoleEnum.S2_TIM_TESIS) && + !roles.includes(RoleEnum.ADMIN) + ) { + if (roles.includes(RoleEnum.S2_KULIAH)) { + idPengajar = id; + } else { + // requester only has S2_MAHASISWA access + idMahasiswa = id; + } + } + + return await this.kelasServ.getKelasDetail( + param.id, + idMahasiswa, + idPengajar, + ); + } } diff --git a/src/kelas/kelas.dto.ts b/src/kelas/kelas.dto.ts index e9d6b5f5b2b7b2b265d59cf87a9ec73ba3f05033..352ae36ea1a1d0c0e45ee7e9eb6eb778cd460439 100644 --- a/src/kelas/kelas.dto.ts +++ b/src/kelas/kelas.dto.ts @@ -6,8 +6,8 @@ import { ApiPropertyOptional, } from "@nestjs/swagger"; import { Kelas } from "src/entities/kelas.entity"; -import { MataKuliah } from "src/entities/mataKuliah"; -import { RoleEnum } from "src/entities/pengguna.entity"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; +import { Pengguna, RoleEnum } from "src/entities/pengguna.entity"; export class CreateKelasDto extends PickType(Kelas, [ "mataKuliahKode", @@ -43,18 +43,26 @@ export class GetKelasQueryDto { search: string; } -export class GetListKelasRespDto { +export class ByIdKelasDto extends PickType(Kelas, ["id"] as const) {} + +export class GetKelasRespDto { @ApiProperty() id: string; @ApiProperty({ example: "K02" }) nomor: string; - @ApiProperty({ example: "IF4031 Pengembangan Aplikasi Terdistribusi" }) - mata_kuliah: string; + @ApiProperty({ example: "IF3270" }) + kode_mata_kuliah: string; + + @ApiProperty({ example: "Pengembangan Aplikasi Terdistribusi" }) + nama_mata_kuliah: string; @ApiProperty() jumlah_mahasiswa: number; + + @ApiProperty({ example: "bg-blue-600/20" }) + warna: string; } export class KodeRespDto extends PickType(MataKuliah, ["kode"] as const) {} @@ -63,3 +71,22 @@ export class GetNextNomorResDto { @ApiProperty({ example: 2 }) nomor: number; } + +class PickedPengajarKelasDto extends PickType(Pengguna, [ + "id", + "nama", +] as const) {} + +class PickedMahasiswaKelasDto extends PickType(Pengguna, [ + "id", + "nama", + "nim", +] as const) {} + +export class GetKelasDetailRespDto extends PickType(Kelas, ["id"] as const) { + @ApiProperty({ type: [PickedPengajarKelasDto] }) + pengajar: PickedPengajarKelasDto[]; + + @ApiProperty({ type: [PickedMahasiswaKelasDto] }) + mahasiswa: PickedMahasiswaKelasDto[]; +} diff --git a/src/kelas/kelas.module.ts b/src/kelas/kelas.module.ts index 80ddae351a722bc5c4aecc32508e90fcd108c9ce..38dfa27f8690f2608b1450c134536f7d3d920651 100644 --- a/src/kelas/kelas.module.ts +++ b/src/kelas/kelas.module.ts @@ -6,7 +6,7 @@ import { AuthModule } from "src/auth/auth.module"; import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; import { KelasController } from "./kelas.controller"; import { CustomStrategy } from "src/middlewares/custom.strategy"; -import { MataKuliah } from "src/entities/mataKuliah"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; @Module({ imports: [ diff --git a/src/kelas/kelas.service.ts b/src/kelas/kelas.service.ts index c771ac6324a2ac862cbc114275577a633d3a334b..fa62a4707fe36efeca92ef76b8074dd392e7a92b 100644 --- a/src/kelas/kelas.service.ts +++ b/src/kelas/kelas.service.ts @@ -10,12 +10,13 @@ import { Brackets, Repository } from "typeorm"; import { CreateKelasDto, DeleteKelasDto, - GetListKelasRespDto, + GetKelasDetailRespDto, + GetKelasRespDto, IdKelasResDto, UpdateKelasDto, } from "./kelas.dto"; import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; -import { MataKuliah } from "src/entities/mataKuliah"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; import { CARD_COLORS } from "./kelas.constant"; @Injectable() @@ -27,20 +28,13 @@ export class KelasService { private mataKuliahRepo: Repository<MataKuliah>, private konfService: KonfigurasiService, ) {} - async getListKelas( idMahasiswa?: string, idPengajar?: string, kodeMatkul?: string, search?: string, ) { - const currPeriod = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!currPeriod) { - throw new BadRequestException("Periode belum dikonfigurasi"); - } + const currPeriod = await this.konfService.getPeriodeOrFail(); let baseQuery = this.kelasRepo .createQueryBuilder("kelas") @@ -49,6 +43,7 @@ export class KelasService { .select([ "kelas.id AS id", "kelas.nomor AS nomor", + "kelas.warna AS warna", "mataKuliah.kode AS kode_mata_kuliah", "mataKuliah.nama AS nama_mata_kuliah", "COUNT(mahasiswa) AS jumlah_mahasiswa", @@ -95,25 +90,143 @@ export class KelasService { .groupBy("kelas.id, mataKuliah.kode") .getRawMany(); - const mapped: GetListKelasRespDto[] = result.map((r) => ({ + const mapped: GetKelasRespDto[] = result.map((r) => ({ id: r.id, nomor: "K" + `${r.nomor}`.padStart(2, "0"), - mata_kuliah: `${r.kode_mata_kuliah} ${r.nama_mata_kuliah}`, + kode_mata_kuliah: r.kode_mata_kuliah, + nama_mata_kuliah: r.nama_mata_kuliah, jumlah_mahasiswa: parseInt(r.jumlah_mahasiswa), + warna: r.warna, })); return mapped; } - async create(createDto: CreateKelasDto): Promise<IdKelasResDto> { - const currPeriod = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); + async getById(id: string, idMahasiswa?: string, idPengajar?: string) { + const currPeriod = await this.konfService.getPeriodeOrFail(); + + let baseQuery = this.kelasRepo + .createQueryBuilder("kelas") + .leftJoinAndSelect("kelas.mahasiswa", "mahasiswa") + .leftJoinAndSelect("kelas.mataKuliah", "mataKuliah") + .select([ + "kelas.id AS id", + "kelas.nomor AS nomor", + "kelas.warna AS warna", + "mataKuliah.kode AS kode_mata_kuliah", + "mataKuliah.nama AS nama_mata_kuliah", + "COUNT(mahasiswa) AS jumlah_mahasiswa", + ]) + .where("kelas.id = :id", { id }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + if (idMahasiswa) { + baseQuery = baseQuery + .innerJoin("kelas.mahasiswa", "mahasiswa_filter") + .andWhere("mahasiswa_filter.mahasiswaId = :idMahasiswa", { + idMahasiswa, + }); + } + + if (idPengajar) { + baseQuery = baseQuery + .innerJoin("kelas.pengajar", "pengajar") + .andWhere("pengajar.pengajarId = :idPengajar", { + idPengajar, + }); + } + + const result = await baseQuery + .groupBy("kelas.id, mataKuliah.kode") + .getRawOne(); + + if (!result) { + throw new NotFoundException("Kelas tidak ditemukan"); + } + + const mapped: GetKelasRespDto = { + id: result.id, + nomor: "K" + `${result.nomor}`.padStart(2, "0"), + kode_mata_kuliah: result.kode_mata_kuliah, + nama_mata_kuliah: result.nama_mata_kuliah, + jumlah_mahasiswa: parseInt(result.jumlah_mahasiswa), + warna: result.warna, + }; - if (!currPeriod) { - throw new BadRequestException("Periode belum dikonfigurasi"); + return mapped; + } + + async getKelasDetail( + idKelas: string, + idMahasiswa?: string, + idPengajar?: string, + ) { + const currPeriod = await this.konfService.getPeriodeOrFail(); + + let baseQuery = this.kelasRepo + .createQueryBuilder("kelas") + .leftJoinAndSelect("kelas.mahasiswa", "mahasiswaKelas") + .leftJoinAndSelect("mahasiswaKelas.mahasiswa", "mahasiswa") + .leftJoinAndSelect("kelas.pengajar", "pengajarKelas") + .leftJoinAndSelect("pengajarKelas.pengajar", "pengajar") + .select([ + "kelas.id", + "mahasiswaKelas.id", + "mahasiswa.id", + "mahasiswa.nim", + "mahasiswa.nama", + "pengajarKelas.id", + "pengajar.id", + "pengajar.nama", + ]) + .orderBy("pengajar.nama", "ASC") + .addOrderBy("mahasiswa.nim", "ASC") + .where("kelas.id = :idKelas", { idKelas }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + if (idMahasiswa) { + baseQuery = baseQuery + .innerJoin("kelas.mahasiswa", "mahasiswaFilter") + .andWhere("mahasiswaFilter.mahasiswaId = :idMahasiswa", { + idMahasiswa, + }); + } + + if (idPengajar) { + baseQuery = baseQuery + .innerJoin("kelas.pengajar", "pengajarFilter") + .andWhere("pengajarFilter.pengajarId = :idPengajar", { + idPengajar, + }); + } + + const result = await baseQuery.getOne(); + + if (!result) { + throw new NotFoundException( + "Kelas tidak ditemukan di antara kelas yang dapat Anda akses", + ); } + const mapped: GetKelasDetailRespDto = { + id: result.id, + pengajar: result.pengajar.map((p) => ({ + id: p.pengajar.id, + nama: p.pengajar.nama, + })), + mahasiswa: result.mahasiswa.map((m) => ({ + id: m.mahasiswa.id, + nim: m.mahasiswa.nim, + nama: m.mahasiswa.nama, + })), + }; + + return mapped; + } + + async create(createDto: CreateKelasDto): Promise<IdKelasResDto> { + const currPeriod = await this.konfService.getPeriodeOrFail(); + let nomor = createDto.nomor; if (nomor) { const checkClassQueary = this.kelasRepo @@ -159,13 +272,7 @@ export class KelasService { } async updateOrCreate(dto: UpdateKelasDto): Promise<IdKelasResDto> { - const currPeriod = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!currPeriod) { - throw new BadRequestException("Periode belum dikonfigurasi"); - } + const currPeriod = await this.konfService.getPeriodeOrFail(); if (!dto.id) { // Create kelas @@ -207,13 +314,7 @@ export class KelasService { } async delete(dto: DeleteKelasDto): Promise<Kelas> { - const currPeriod = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!currPeriod) { - throw new BadRequestException("Periode belum dikonfigurasi"); - } + const currPeriod = await this.konfService.getPeriodeOrFail(); const kelasQuery = this.kelasRepo .createQueryBuilder("kelas") @@ -243,13 +344,7 @@ export class KelasService { } async getNextNomorKelas(kodeMatkul: string): Promise<number> { - const currPeriod = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!currPeriod) { - throw new BadRequestException("Periode belum dikonfigurasi"); - } + const currPeriod = await this.konfService.getPeriodeOrFail(); const maxClass = await this.kelasRepo.findOne({ where: { diff --git a/src/konfigurasi/konfigurasi.service.ts b/src/konfigurasi/konfigurasi.service.ts index 23ee9f8761d27dc8f257181c6f7d2abbf35de001..a8e7edaccbcafeeab8aa59f9039ce69ae88a21c2 100644 --- a/src/konfigurasi/konfigurasi.service.ts +++ b/src/konfigurasi/konfigurasi.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Konfigurasi } from "src/entities/konfigurasi.entity"; import { Repository } from "typeorm"; @@ -29,4 +29,16 @@ export class KonfigurasiService { return data?.value; } + + async getPeriodeOrFail() { + const currPeriod = await this.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + return currPeriod; + } } diff --git a/src/main.ts b/src/main.ts index 11cb941afa48d31dc673e827398bac25ee2df6c0..fdce89e3372e4a1ea2d1a7bf5006ee2054c3f487 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,13 +22,15 @@ async function bootstrap() { .setDescription("GraduIT API Documentation for S2 services") .setVersion("1.0") .addTag("Alokasi Topik") - .addTag("Approval") .addTag("Bimbingan") .addTag("Dashboard") .addTag("Dosen Bimbingan") .addTag("Kelas") .addTag("Konfigurasi") + .addTag("Nilai") .addTag("Registrasi Tesis") + .addTag("Submisi Tugas") + .addTag("Tugas") .addCookieAuth(process.env.COOKIE_NAME) .addBearerAuth() .build(); diff --git a/src/middlewares/forbidden-exception.filter.ts b/src/middlewares/forbidden-exception.filter.ts index ec5ba9ddf450d25a3698c6fbc8ac20f32f1d468b..9d6de0d98638596e89d2c171d2c28dbbc145c205 100644 --- a/src/middlewares/forbidden-exception.filter.ts +++ b/src/middlewares/forbidden-exception.filter.ts @@ -13,9 +13,9 @@ export class ForbiddenExceptionFilter implements ExceptionFilter { const response = ctx.getResponse<Response>(); const status = exception.getStatus(); - response.clearCookie(process.env.COOKIE_NAME).status(status).json({ - message: "Forbidden", - status, - }); + response + .clearCookie(process.env.COOKIE_NAME) + .status(status) + .json(exception.getResponse()); } } diff --git a/src/nilai/nilai.controller.ts b/src/nilai/nilai.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bb1cb659a4ad0347e37c69df0ad85750da2d1ad --- /dev/null +++ b/src/nilai/nilai.controller.ts @@ -0,0 +1,45 @@ +import { Body, Controller, Get, Patch, Query, UseGuards } from "@nestjs/common"; +import { NilaiService } from "./nilai.service"; +import { + ApiBearerAuth, + ApiCookieAuth, + ApiOkResponse, + ApiTags, +} from "@nestjs/swagger"; +import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; +import { RolesGuard } from "src/middlewares/roles.guard"; +import { Roles } from "src/middlewares/roles.decorator"; +import { RoleEnum } from "src/entities/pengguna.entity"; +import { + GetNilaiByMatkulQueryDto, + GetNilaiByMatkulRespDto, + UpdateNilaiDto, + UpdateNilaiRespDto, +} from "./nilai.dto"; + +@ApiBearerAuth() +@ApiCookieAuth() +@ApiTags("Nilai") +@UseGuards(CustomAuthGuard, RolesGuard) +@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) +@Controller("nilai") +export class NilaiController { + constructor(private nilaiServ: NilaiService) {} + + @ApiOkResponse({ type: [GetNilaiByMatkulRespDto] }) + @Get() + async getNilaiByMatkul(@Query() query: GetNilaiByMatkulQueryDto) { + return this.nilaiServ.getNilaiByMatkul( + query.kode, + query.page || 1, + query.limit || 10, + query.search || "", + ); + } + + @ApiOkResponse({ type: UpdateNilaiRespDto }) + @Patch() + async updateNilai(@Body() body: UpdateNilaiDto) { + return this.nilaiServ.updateNilai(body.mahasiswaKelasIds, body.nilaiAkhir); + } +} diff --git a/src/nilai/nilai.dto.ts b/src/nilai/nilai.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b933d07468212dbfacd8fc41d441227b5084418 --- /dev/null +++ b/src/nilai/nilai.dto.ts @@ -0,0 +1,78 @@ +import { IsNumberString } from "@nestjs/class-validator"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { + IsNumber, + IsOptional, + IsPositive, + IsString, + IsUUID, + MaxLength, + MinLength, +} from "class-validator"; + +export class GetNilaiByMatkulQueryDto { + @ApiPropertyOptional({ example: "IF4031" }) + @IsOptional() + @IsString() + @MinLength(6) + @MaxLength(6) + kode?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @IsNumberString() + @ApiPropertyOptional({ description: "default: 1" }) + page?: number; + + @IsOptional() + @IsNumberString() + @ApiPropertyOptional({ description: "default: 10" }) + limit?: number; +} + +export class GetNilaiByMatkulRespDto { + @ApiProperty({ example: "IF4031" }) + mata_kuliah_kode: string; + + @ApiProperty({ example: "Pemrograman Berbasis Kerangka Kerja" }) + mata_kuliah_nama: string; + + @ApiProperty() + kelas_nomor: number; + + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + mahasiswa_kelas_id: string; + + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + mahasiswa_id: string; + + @ApiProperty() + mahasiswa_nama: string; + + @ApiProperty({ example: "13517000" }) + mahasiswa_nim: string; + + @ApiProperty() + nilai_akhir: string | null; +} + +export class UpdateNilaiRespDto { + @ApiProperty({ + type: [String], + example: ["550e8400-e29b-41d4-a716-446655440000"], + }) + @IsUUID("all", { each: true }) + mahasiswaKelasIds: string[]; +} + +export class UpdateNilaiDto extends UpdateNilaiRespDto { + @ApiPropertyOptional({ description: "undefined: assign nilai to null" }) + @IsOptional() + @IsNumber() + @IsPositive() + nilaiAkhir?: number; +} diff --git a/src/nilai/nilai.module.ts b/src/nilai/nilai.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9aca923b411670865c235c946b28d698a099a52 --- /dev/null +++ b/src/nilai/nilai.module.ts @@ -0,0 +1,26 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { AuthModule } from "src/auth/auth.module"; +import { Kelas } from "src/entities/kelas.entity"; +import { Konfigurasi } from "src/entities/konfigurasi.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; +import { KelasModule } from "src/kelas/kelas.module"; +import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; +import { NilaiController } from "./nilai.controller"; +import { NilaiService } from "./nilai.service"; +import { CustomStrategy } from "src/middlewares/custom.strategy"; +import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; +import { KelasService } from "src/kelas/kelas.service"; + +@Module({ + imports: [ + TypeOrmModule.forFeature([MahasiswaKelas, Kelas, MataKuliah, Konfigurasi]), + AuthModule, + KonfigurasiModule, + KelasModule, + ], + controllers: [NilaiController], + providers: [NilaiService, CustomStrategy, KelasService, KonfigurasiService], +}) +export class NilaiModule {} diff --git a/src/nilai/nilai.service.ts b/src/nilai/nilai.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..81ebba4d7151fc7b42f495d542af94057fd4f66e --- /dev/null +++ b/src/nilai/nilai.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Kelas } from "src/entities/kelas.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; +import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; +import { Brackets, In, Repository } from "typeorm"; +import { GetNilaiByMatkulRespDto, UpdateNilaiRespDto } from "./nilai.dto"; + +@Injectable() +export class NilaiService { + constructor( + @InjectRepository(MahasiswaKelas) + private mhsKelasRepo: Repository<MahasiswaKelas>, + @InjectRepository(Kelas) + private kelasRepo: Repository<Kelas>, + @InjectRepository(MataKuliah) + private mataKuliahRepo: Repository<MataKuliah>, + private konfServ: KonfigurasiService, + ) {} + + private async isMhsKelasOrFail(mhsKelasIds: string[]) { + const periode = await this.konfServ.getPeriodeOrFail(); + + const mhsKelas = await this.mhsKelasRepo.find({ + select: { id: true }, + where: { id: In(mhsKelasIds), kelas: { periode } }, + }); + + for (const mhsKelasId of mhsKelasIds) { + if (!mhsKelas.find((mk) => mk.id === mhsKelasId)) { + throw new Error(`Mahasiswa kelas ${mhsKelasId} tidak ditemukan`); + } + } + } + + async getNilaiByMatkul( + mataKuliahKode: string, + page: number, + limit: number, + search: string, + ) { + const currPeriode = await this.konfServ.getPeriodeOrFail(); + console.log(limit); + + const baseQuery = this.mataKuliahRepo + .createQueryBuilder("matkul") + .select([ + "matkul.kode AS mata_kuliah_kode", + "matkul.nama AS mata_kuliah_nama", + "kelas.nomor AS kelas_nomor", + "mhsKelas.id AS mahasiswa_kelas_id", + "mahasiswa.id AS mahasiswa_id", + "mahasiswa.nama AS mahasiswa_nama", + "mahasiswa.nim AS mahasiswa_nim", + "mhsKelas.nilaiAkhir AS nilai_akhir", + ]) + .innerJoin("matkul.kelas", "kelas") + .leftJoin("kelas.mahasiswa", "mhsKelas") + .innerJoin("mhsKelas.mahasiswa", "mahasiswa") + .where("kelas.periode = :periode", { periode: currPeriode }) + .andWhere( + new Brackets((qb) => { + qb.where("mahasiswa.nama ILIKE :search", { + search: `%${search}%`, + }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` }); + }), + ); + + if (mataKuliahKode) { + baseQuery.andWhere("matkul.kode = :kode", { kode: mataKuliahKode }); + } + + const mhsKelas: GetNilaiByMatkulRespDto[] = await baseQuery + .orderBy("matkul.kode") + .addOrderBy("kelas.nomor") + .addOrderBy("mahasiswa.nim") + .skip((page - 1) * limit) + .limit(limit) + .getRawMany(); + + return mhsKelas; + } + + async updateNilai( + mhsKelasIds: string[], + nilaiAkhir?: number, + ): Promise<UpdateNilaiRespDto> { + await this.isMhsKelasOrFail(mhsKelasIds); + + await this.mhsKelasRepo.update( + { id: In(mhsKelasIds) }, + { nilaiAkhir: nilaiAkhir ?? null }, + ); + + return { mahasiswaKelasIds: mhsKelasIds }; + } +} diff --git a/src/registrasi-tesis/registrasi-tesis.controller.ts b/src/registrasi-tesis/registrasi-tesis.controller.ts index 4b0904fe472ce0dba0b8363e62f53291e5988af4..9802752f5de61d55f75b2b89791038abc827c2e2 100644 --- a/src/registrasi-tesis/registrasi-tesis.controller.ts +++ b/src/registrasi-tesis/registrasi-tesis.controller.ts @@ -4,7 +4,6 @@ import { Controller, ForbiddenException, Get, - NotFoundException, Param, Patch, Post, @@ -13,9 +12,13 @@ import { UseGuards, } from "@nestjs/common"; import { + ApiBadRequestResponse, ApiBearerAuth, ApiCookieAuth, + ApiCreatedResponse, + ApiNotFoundResponse, ApiOkResponse, + ApiOperation, ApiTags, } from "@nestjs/swagger"; import { Request } from "express"; @@ -27,9 +30,10 @@ import { Roles } from "src/middlewares/roles.decorator"; import { RolesGuard } from "src/middlewares/roles.guard"; import { FindAllNewestRegRespDto, + GetByIdRespDto, + IdDto, RegByMhsParamDto, RegDto, - RegParamDto, RegQueryDto, RegStatisticsRespDto, UpdateByMhsParamsDto, @@ -50,29 +54,121 @@ export class RegistrasiTesisController { private readonly konfService: KonfigurasiService, ) {} + @ApiOperation({ + summary: "Create new registration. Roles: S2_MAHASISWA, ADMIN", + }) + @ApiCreatedResponse({ type: IdDto }) + @ApiNotFoundResponse({ description: "Penerima atau topik tidak ditemukan" }) + @ApiBadRequestResponse({ + description: + "Mahasiswa sedang memiliki pendaftaran aktif atau judul dan deskripsi topik baru tidak ada", + }) @UseGuards(CustomAuthGuard, RolesGuard) - @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) - @Get("/mahasiswa/:mahasiswaId") - findByUserId(@Param() params: RegByMhsParamDto) { - return this.registrasiTesisService.findByUserId(params.mahasiswaId); - } - - @UseGuards(CustomAuthGuard, RolesGuard) - @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) + @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN) @Post() async createTopicRegistration( @Body() topicRegistrationDto: RegDto, @Req() req: Request, - ) { + ): Promise<IdDto> { const { id } = req.user as AuthDto; + const periode = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!periode) { + throw new BadRequestException("Periode belum dikonfigurasi."); + } + return this.registrasiTesisService.createTopicRegistration( id, topicRegistrationDto, + periode, + ); + } + + @ApiOperation({ + summary: + "Find registrations (historical) by Mahasiswa ID. Roles: S2_MAHASISWA, ADMIN, S2_TIM_TESIS", + }) + @ApiOkResponse({ type: [GetByIdRespDto] }) + @UseGuards(CustomAuthGuard, RolesGuard) + @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) + @Get("/mahasiswa/:mahasiswaId") + async findByUserId(@Param() params: RegByMhsParamDto, @Req() req: Request) { + const { id, roles } = req.user as AuthDto; + + if ( + !roles.includes(RoleEnum.ADMIN) && + !roles.includes(RoleEnum.S2_TIM_TESIS) + ) { + // roles only include RoleEnum.S2_MAHASISWA + if (id !== params.mahasiswaId) { + throw new ForbiddenException(); + } + } + + const periode = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!periode) { + throw new BadRequestException("Periode belum dikonfigurasi."); + } + + return this.registrasiTesisService.findByUserId( + params.mahasiswaId, + periode, + false, + undefined, ); } - // Right now only admin & timtesis view is handled (apakah dosen perlu summary juga?) + @ApiOperation({ + summary: + "Find newest registration by Mahasiswa ID. Roles: ADMIN, S2_TIM_TESIS, S2_PEMBIMBING", + }) + @ApiOkResponse({ type: GetByIdRespDto }) + @UseGuards(CustomAuthGuard, RolesGuard) + @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING) + @Get("/mahasiswa/:mahasiswaId/newest") + async findNewestByUserId( + @Param() params: RegByMhsParamDto, + @Req() req: Request, + ) { + const { id, roles } = req.user as AuthDto; + + let idPenerima = undefined; + if ( + !roles.includes(RoleEnum.ADMIN) && + !roles.includes(RoleEnum.S2_TIM_TESIS) + ) { + // roles only include RoleEnum.S2_PEMBIMBING + idPenerima = id; + } + + const periode = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!periode) { + throw new BadRequestException("Periode belum dikonfigurasi."); + } + + const res = await this.registrasiTesisService.findByUserId( + params.mahasiswaId, + periode, + true, + idPenerima, + ); + + return res[0]; + } + + @ApiOperation({ + summary: + "Get statistics of registrations. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS", + }) @ApiOkResponse({ type: RegStatisticsRespDto }) @UseGuards(CustomAuthGuard, RolesGuard) @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) @@ -96,6 +192,10 @@ export class RegistrasiTesisController { // Admin & TimTesis view will show newst reg records per Mahasiswa // Pembimbing view will show all regs towards them + @ApiOperation({ + summary: + "Find all newest registration for each Mahasiswa. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS", + }) @ApiOkResponse({ type: FindAllNewestRegRespDto, isArray: true }) @UseGuards(CustomAuthGuard, RolesGuard) @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) @@ -128,42 +228,18 @@ export class RegistrasiTesisController { }); } + @ApiOperation({ + summary: + "Update interview date of newest in process registration by Mahasiswa ID. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS", + }) + @ApiOkResponse({ type: IdDto }) @UseGuards(CustomAuthGuard, RolesGuard) - @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) - @Get("/:id") - async findById( - @Req() req: Request, - @Param() params: RegParamDto, - @Query() - query: ViewQueryDto, - ) { - const { id: idPenerima, roles } = req.user as AuthDto; - - if (!roles.includes(query.view)) { - throw new ForbiddenException(); - } - - const res = await this.registrasiTesisService.findRegById(params.id); - if (!res) { - throw new NotFoundException(); - } - - if ( - query.view === RoleEnum.S2_PEMBIMBING && - res.penerima.id !== idPenerima - ) { - throw new ForbiddenException(); - } - - return res; - } - - @UseGuards(CustomAuthGuard, RolesGuard) - @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) + @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING) @Patch("/:mhsId/interview") async updateInterviewDateByMhsId( @Param() params: UpdateByMhsParamsDto, @Body() body: UpdateInterviewBodyDto, + @Req() req: Request, ) { const periode = await this.konfService.getKonfigurasiByKey( process.env.KONF_PERIODE_KEY, @@ -173,19 +249,37 @@ export class RegistrasiTesisController { throw new BadRequestException("Periode belum dikonfigurasi."); } + const { id, roles } = req.user as AuthDto; + let idPenerima = undefined; + + if ( + !roles.includes(RoleEnum.ADMIN) && + !roles.includes(RoleEnum.S2_TIM_TESIS) + ) { + // roles only include RoleEnum.S2_PEMBIMBING + idPenerima = id; + } + return await this.registrasiTesisService.updateInterviewDate( params.mhsId, periode, body, + idPenerima, ); } + @ApiOperation({ + summary: + "Update status of newest registration by Mahasiswa ID. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS", + }) + @ApiOkResponse({ type: IdDto }) @UseGuards(CustomAuthGuard, RolesGuard) - @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) + @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING) @Patch("/:mhsId/status") async updateStatusByMhsId( @Param() params: UpdateByMhsParamsDto, @Body() body: UpdateStatusBodyDto, + @Req() req: Request, ) { const periode = await this.konfService.getKonfigurasiByKey( process.env.KONF_PERIODE_KEY, @@ -195,17 +289,34 @@ export class RegistrasiTesisController { throw new BadRequestException("Periode belum dikonfigurasi."); } + const { id, roles } = req.user as AuthDto; + let idPenerima = undefined; + + if ( + !roles.includes(RoleEnum.ADMIN) && + !roles.includes(RoleEnum.S2_TIM_TESIS) + ) { + // roles only include RoleEnum.S2_PEMBIMBING + idPenerima = id; + } + return await this.registrasiTesisService.updateStatus( params.mhsId, periode, body, + idPenerima, ); } + @ApiOperation({ + summary: + "Update pembimbing list of approved registration by Mahasiswa ID. Roles: ADMIN, S2_TIM_TESIS", + }) + @ApiOkResponse({ type: IdDto }) @UseGuards(CustomAuthGuard, RolesGuard) @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) @Patch("/:mhsId/pembimbing") - async udpatePembimbingListByMhsId( + async updatePembimbingListByMhsId( @Param() params: UpdateByMhsParamsDto, @Body() body: UpdatePembimbingBodyDto, ) { diff --git a/src/registrasi-tesis/registrasi-tesis.dto.ts b/src/registrasi-tesis/registrasi-tesis.dto.ts index f18cca53a0f91f6503197d1318f7ca283cb85015..cc9541b2956aedc63449aa5144a9b87c34f6e0ba 100644 --- a/src/registrasi-tesis/registrasi-tesis.dto.ts +++ b/src/registrasi-tesis/registrasi-tesis.dto.ts @@ -6,31 +6,38 @@ import { IsString, IsUUID, } from "@nestjs/class-validator"; -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger"; import { ArrayMinSize, ArrayUnique, IsArray } from "class-validator"; -import { JalurEnum, RegStatus } from "src/entities/pendaftaranTesis.entity"; -import { RoleEnum } from "src/entities/pengguna.entity"; +import { + JalurEnum, + PendaftaranTesis, + RegStatus, +} from "src/entities/pendaftaranTesis.entity"; +import { Pengguna, RoleEnum } from "src/entities/pengguna.entity"; export class RegDto { - @IsUUID() - @ApiProperty() - idMahasiswa: string; - @IsUUID() @ApiProperty() idPenerima: string; - @IsString() - @ApiProperty() - judulTopik: string; - - @IsString() - @ApiProperty() - deskripsi: string; + @IsUUID() + @IsOptional() + @ApiPropertyOptional() + idTopik?: string; @IsEnum(JalurEnum) @ApiProperty({ enum: JalurEnum }) jalurPilihan: JalurEnum; + + @IsString() + @IsOptional() + @ApiPropertyOptional() + judulTopik?: string; + + @IsString() + @IsOptional() + @ApiPropertyOptional() + deskripsiTopik?: string; } export class RegByMhsParamDto { @@ -39,13 +46,21 @@ export class RegByMhsParamDto { mahasiswaId: string; } -export class RegParamDto { +export class IdDto { @IsUUID() @ApiProperty() id: string; } -export class RegQueryDto { +export class ViewQueryDto { + @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS]) + @ApiProperty({ + enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS], + }) + view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS; +} + +export class RegQueryDto extends ViewQueryDto { @IsOptional() @IsNumberString() @ApiPropertyOptional() @@ -75,35 +90,29 @@ export class RegQueryDto { @IsEnum(["ASC", "DESC"]) @ApiPropertyOptional({ enum: ["ASC", "DESC"] }) sort?: "ASC" | "DESC"; - - @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS]) - @ApiProperty({ - enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS], - }) - view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS; -} - -export class ViewQueryDto { - @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS]) - @ApiProperty({ - enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS], - }) - view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS; } export class FindAllNewestRegRespDataDto { @ApiProperty() pendaftaran_id: string; + @ApiProperty() nim: string; + @ApiProperty() mahasiswa_nama: string; + @ApiProperty() mahasiswa_id: string; + @ApiProperty() pembimbing_nama: string; + @ApiProperty() status: string; + + @ApiProperty() + jadwal_interview: Date; } export class FindAllNewestRegRespDto { @@ -158,3 +167,26 @@ export class UpdatePembimbingBodyDto { @ArrayUnique() pembimbing_ids: string[]; } + +class DosenPembimbingDto extends PickType(Pengguna, [ + "id", + "nama", + "kontak", +] as const) {} + +export class GetByIdRespDto extends PickType(PendaftaranTesis, [ + "id", + "jadwalInterview", + "status", + "jalurPilihan", + "waktuPengiriman", +] as const) { + @ApiProperty() + judulTopik: string; + + @ApiProperty() + deskripsiTopik: string; + + @ApiProperty({ type: [DosenPembimbingDto] }) + dosenPembimbing: DosenPembimbingDto[]; +} diff --git a/src/registrasi-tesis/registrasi-tesis.service.ts b/src/registrasi-tesis/registrasi-tesis.service.ts index 1513caa4c7d7cae5ada90e6c5857f93349c6b533..e19127b3878b6411462d75d1a7126a6d565ae723 100644 --- a/src/registrasi-tesis/registrasi-tesis.service.ts +++ b/src/registrasi-tesis/registrasi-tesis.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ForbiddenException, Injectable, InternalServerErrorException, NotFoundException, @@ -13,16 +14,17 @@ import { import { Pengguna, RoleEnum } from "src/entities/pengguna.entity"; import { Topik } from "src/entities/topik.entity"; import { generateQueryBuilderOrderByObj } from "src/helper/sorting"; -import { validateId } from "src/helper/validation"; -import { ArrayContains, DataSource, In, Repository } from "typeorm"; +import { ArrayContains, Brackets, DataSource, In, Repository } from "typeorm"; import { FindAllNewestRegRespDto, + IdDto, RegDto, RegStatisticsRespDto, UpdateInterviewBodyDto, UpdatePembimbingBodyDto, UpdateStatusBodyDto, } from "./registrasi-tesis.dto"; +import * as dayjs from "dayjs"; @Injectable() export class RegistrasiTesisService { @@ -41,64 +43,153 @@ export class RegistrasiTesisService { async createTopicRegistration( userId: string, topicRegistrationDto: RegDto, - ): Promise<PendaftaranTesis> { - // TODO: Proper validations - - // Validate id - validateId([ - { id: userId, object: "Pengguna" }, - { id: topicRegistrationDto.idPenerima, object: "Pembimbing" }, - ]); - - // Validate user id, supervisor id - const [user, supervisor, topic] = await Promise.all([ - this.penggunaRepository.findOne({ - where: { id: userId }, - }), + periode: string, + ): Promise<IdDto> { + const queries: ( + | Promise<void | PendaftaranTesis> + | Promise<Pengguna> + | Promise<Topik> + )[] = [ + this.getNewestRegByMhsOrFail(userId, periode).catch( + (ex: BadRequestException) => { + if (ex.message === "No mahasiswa user with given id exists") { + throw ex; + } + // else: mahasiswa does not have pending registration -> allowed + }, + ), this.penggunaRepository.findOne({ where: { id: topicRegistrationDto.idPenerima }, }), - this.topicRepostitory.findOne({ - where: { judul: topicRegistrationDto.judulTopik }, - }), - ]); + ]; - if (!user) { - throw new NotFoundException("User not found."); - } else if (!supervisor) { - throw new NotFoundException("Supervisor not found."); - } else if (!topic) { + if (topicRegistrationDto.idTopik) { + queries.push( + this.topicRepostitory.findOne({ + where: { id: topicRegistrationDto.idTopik }, + }), + ); + } + + const queryResult = await Promise.all(queries); + const lastPendaftaran = queryResult[0] as PendaftaranTesis; + const penerima = queryResult[1] as Pengguna; + let topik = topicRegistrationDto.idTopik ? (queryResult[2] as Topik) : null; + + if (!penerima) { + throw new NotFoundException("Penerima not found."); + } + + if (topicRegistrationDto.idTopik && !topik) { throw new NotFoundException("Topic not found."); } + if (lastPendaftaran && lastPendaftaran.status !== RegStatus.REJECTED) { + throw new BadRequestException( + "Mahasiswa already has pending registration in this period", + ); + } + + if (!topik) { + if ( + !topicRegistrationDto.judulTopik || + !topicRegistrationDto.deskripsiTopik + ) { + throw new BadRequestException( + "Judul dan deskripsi topik tidak boleh kosong.", + ); + } + + topik = this.topicRepostitory.create({ + judul: topicRegistrationDto.judulTopik, + deskripsi: topicRegistrationDto.deskripsiTopik, + idPengaju: userId, + periode, + }); + } + // Create new registration const createdRegistration = this.pendaftaranTesisRepository.create({ ...topicRegistrationDto, - mahasiswa: user, - penerima: supervisor, - topik: topic, + mahasiswaId: userId, + penerima, + topik, }); await this.pendaftaranTesisRepository.save(createdRegistration); - return createdRegistration; + return { + id: createdRegistration.id, + }; } - async findByUserId(mahasiswaId: string) { - const res = await this.pendaftaranTesisRepository.find({ - relations: ["topik", "penerima"], - where: { mahasiswa: { id: mahasiswaId } }, - }); + async findByUserId( + mahasiswaId: string, + periode: string, + isNewestOnly: boolean, + idPenerima?: string, + ) { + const baseQuery = this.pendaftaranTesisRepository + .createQueryBuilder("pt") + .select("pt.id") + .addSelect("pt.jadwalInterview") + .addSelect("pt.status") + .addSelect("pt.jalurPilihan") + .addSelect("pt.waktuPengiriman") + .addSelect("topik.judul") + .addSelect("penerima.id") + .addSelect("penerima.nama") + .addSelect("dosenBimbingan") + .addSelect("dosen.id") + .addSelect("dosen.nama") + .addSelect("dosen.kontak") + .addSelect("topik.judul") + .addSelect("topik.deskripsi") + .leftJoin("pt.topik", "topik") + .leftJoin("pt.penerima", "penerima") + .leftJoin("pt.dosenBimbingan", "dosenBimbingan") + .leftJoin("dosenBimbingan.dosen", "dosen") + .where("pt.mahasiswaId = :mahasiswaId", { mahasiswaId }) + .andWhere("topik.periode = :periode", { periode }) + .orderBy("pt.waktuPengiriman", "DESC"); + + const res = await baseQuery.getMany(); + + if (res.length === 0) { + throw new NotFoundException("Tidak ada registrasi tesis yang ditemukan."); + } - return res.map((r) => ({ - ...r, - penerima: { - ...r.penerima, - password: undefined, - roles: undefined, - nim: undefined, - }, + if (idPenerima) { + // requester only has S2_PEMBIMBING access + const reg = res[0]; + + if (reg.penerima.id !== idPenerima) { + throw new ForbiddenException(); + } + } + + const mappedRes = res.map((r) => ({ + id: r.id, + jadwalInterview: r.jadwalInterview, + jalurPilihan: r.jalurPilihan, + status: r.status, + waktuPengiriman: r.waktuPengiriman, + judulTopik: r.topik.judul, + deskripsiTopik: r.topik.deskripsi, + dosenPembimbing: + r.status === RegStatus.APPROVED + ? r.dosenBimbingan.map((db) => db.dosen) + : [r.penerima], })); + + if (isNewestOnly) { + // only get last registration + // slow performance because get all records first then only returns the first one + // need to change to use subquery + mappedRes.splice(1); + } + + return mappedRes; } async getRegsStatistics(options: { @@ -109,69 +200,70 @@ export class RegistrasiTesisService { where: { roles: ArrayContains([RoleEnum.S2_MAHASISWA]) }, }); - // Show newest regs per Mhs if POV TimTesis or Admin - if (!options.idPenerima) { - const baseQuery = this.pendaftaranTesisRepository - .createQueryBuilder("pt") - .innerJoinAndSelect( - (qb) => - qb - .select([ - "pt.mahasiswaId AS latest_mahasiswaId", - "MAX(pt.waktuPengiriman) AS latestPengiriman", - ]) - .from(PendaftaranTesis, "pt") - .groupBy("pt.mahasiswaId"), - "latest", - "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman", - ) - .innerJoinAndSelect("pt.topik", "topik") - .where("topik.periode = :periode", { periode: options.periode }); - - const totalDiterima = baseQuery - .clone() - .andWhere("pt.status = :status", { status: RegStatus.APPROVED }) - .getCount(); - - const totalProses = baseQuery - .clone() - .where("pt.status IN (:...status)", { - status: [RegStatus.NOT_ASSIGNED, RegStatus.INTERVIEW], - }) - .getCount(); - - const totalDitolak = baseQuery - .clone() - .where("pt.status = :status", { status: RegStatus.REJECTED }) - .getCount(); - - const [total, diterima, proses, ditolak] = await Promise.all([ - totalMahasiswa, - totalDiterima, - totalProses, - totalDitolak, - ]); - - return { - diterima: { - amount: diterima, - percentage: Math.round((diterima / total) * 100), - }, - sedang_proses: { - amount: proses, - percentage: Math.round((proses / total) * 100), - }, - ditolak: { - amount: ditolak, - percentage: Math.round((ditolak / total) * 100), - }, - }; - } else { - throw new InternalServerErrorException("Not implemented"); + // Show newest regs per Mhs + const baseQuery = this.pendaftaranTesisRepository + .createQueryBuilder("pt") + .innerJoinAndSelect( + (qb) => + qb + .select([ + "pt.mahasiswaId AS latest_mahasiswaId", + "MAX(pt.waktuPengiriman) AS latestPengiriman", + ]) + .from(PendaftaranTesis, "pt") + .groupBy("pt.mahasiswaId"), + "latest", + "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman", + ) + .innerJoinAndSelect("pt.topik", "topik") + .where("topik.periode = :periode", { periode: options.periode }); + + if (options.idPenerima) { + baseQuery.andWhere("pt.penerimaId = :idPenerima", { + idPenerima: options.idPenerima, + }); } + + const totalDiterima = baseQuery + .clone() + .andWhere("pt.status = :status", { status: RegStatus.APPROVED }) + .getCount(); + + const totalProses = baseQuery + .clone() + .andWhere("pt.status IN (:...status)", { + status: [RegStatus.NOT_ASSIGNED, RegStatus.INTERVIEW], + }) + .getCount(); + + const totalDitolak = baseQuery + .clone() + .andWhere("pt.status = :status", { status: RegStatus.REJECTED }) + .getCount(); + + const [total, diterima, proses, ditolak] = await Promise.all([ + totalMahasiswa, + totalDiterima, + totalProses, + totalDitolak, + ]); + + return { + diterima: { + amount: diterima, + percentage: Math.round((diterima / total) * 100), + }, + sedang_proses: { + amount: proses, + percentage: Math.round((proses / total) * 100), + }, + ditolak: { + amount: ditolak, + percentage: Math.round((ditolak / total) * 100), + }, + }; } - // TODO sort async findAllRegs(options: { status?: RegStatus; page: number; @@ -186,22 +278,20 @@ export class RegistrasiTesisService { .createQueryBuilder("pt") .select("pt"); - // Show newest regs per Mhs if POV TimTesis or Admin + // Show newest regs per Mhs // May need to make materialized view to improve performance - if (!options.idPenerima) { - baseQuery.innerJoinAndSelect( - (qb) => - qb - .select([ - "pt.mahasiswaId AS latest_mahasiswaId", - "MAX(pt.waktuPengiriman) AS latestPengiriman", - ]) - .from(PendaftaranTesis, "pt") - .groupBy("pt.mahasiswaId"), - "latest", - "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman", - ); - } + baseQuery.innerJoinAndSelect( + (qb) => + qb + .select([ + "pt.mahasiswaId AS latest_mahasiswaId", + "MAX(pt.waktuPengiriman) AS latestPengiriman", + ]) + .from(PendaftaranTesis, "pt") + .groupBy("pt.mahasiswaId"), + "latest", + "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman", + ); baseQuery .innerJoinAndSelect("pt.topik", "topik") @@ -209,19 +299,25 @@ export class RegistrasiTesisService { .innerJoinAndSelect("pt.mahasiswa", "mahasiswa") .where("topik.periode = :periode", { periode: options.periode }); + if (options.idPenerima) { + baseQuery.andWhere("pt.penerimaId = :idPenerima", { + idPenerima: options.idPenerima, + }); + } + if (options.search) baseQuery.andWhere( - "mahasiswa.nama LIKE '%' || :search || '%' OR mahasiswa.nim LIKE '%' || :search || '%'", - { - search: options.search, - }, + new Brackets((qb) => + qb + .where("mahasiswa.nama ILIKE :search", { + search: `%${options.search}%`, + }) + .orWhere("mahasiswa.nim ILIKE :search", { + search: `%${options.search}%`, + }), + ), ); - if (options.idPenerima) - baseQuery.andWhere("penerima.id = :idPenerima", { - idPenerima: options.idPenerima, - }); - if (options.status) baseQuery.andWhere("pt.status = :status", { status: options.status, @@ -256,6 +352,7 @@ export class RegistrasiTesisService { mahasiswa_nama: reg.mahasiswa.nama, pembimbing_nama: reg.penerima.nama, status: reg.status, + jadwal_interview: reg.jadwalInterview, })), count, }; @@ -263,40 +360,7 @@ export class RegistrasiTesisService { return resData; } - async findRegById(id: string) { - // not periode-protected - return await this.pendaftaranTesisRepository.findOne({ - select: { - id: true, - waktuPengiriman: true, - jadwalInterview: true, - waktuKeputusan: true, - status: true, - jalurPilihan: true, - penerima: { - id: true, - nama: true, - email: true, - }, - mahasiswa: { - id: true, - nama: true, - email: true, - nim: true, - }, - }, - where: { - id, - }, - relations: { - penerima: true, - topik: true, - mahasiswa: true, - }, - }); - } - - async getNewestRegByMhs(mahasiswaId: string, periode: string) { + private async getNewestRegByMhsOrFail(mahasiswaId: string, periode: string) { const mahasiswa = await this.penggunaRepository.findOne({ select: { id: true, @@ -313,14 +377,23 @@ export class RegistrasiTesisService { const newestReg = await this.pendaftaranTesisRepository.findOne({ select: { id: true, + jadwalInterview: true, status: true, waktuPengiriman: true, + jalurPilihan: true, topik: { + judul: true, + deskripsi: true, periode: true, }, + penerima: { + id: true, + }, }, relations: { topik: true, + penerima: true, + mahasiswa: true, }, where: { mahasiswa: mahasiswa, @@ -345,8 +418,22 @@ export class RegistrasiTesisService { mahasiswaId: string, periode: string, dto: UpdateInterviewBodyDto, + idPenerima?: string, ) { - const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode); + const minDate = new Date(); + minDate.setDate(minDate.getDate() + 2); + + if (dayjs(dto.date).isBefore(dayjs(minDate).endOf("d"))) { + throw new BadRequestException( + "Interview date must be at least 2 days from now", + ); + } + + const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId, periode); + + if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) { + throw new ForbiddenException(); + } const restrictedStatus: RegStatus[] = [ RegStatus.APPROVED, @@ -367,22 +454,54 @@ export class RegistrasiTesisService { { jadwalInterview: newDate, status: RegStatus.INTERVIEW }, ); - return { status: "ok" }; + return { id: newestReg.id } as IdDto; } async updateStatus( mahasiswaId: string, periode: string, dto: UpdateStatusBodyDto, + idPenerima?: string, ) { - const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode); + const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId, periode); - await this.pendaftaranTesisRepository.update( - { id: newestReg.id }, - { status: dto.status }, - ); + if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) { + throw new ForbiddenException(); + } + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + PendaftaranTesis, + { id: newestReg.id }, + { status: dto.status, waktuKeputusan: new Date() }, + ); + + if (dto.status === RegStatus.APPROVED) { + await queryRunner.manager.insert(DosenBimbingan, { + idPendaftaran: newestReg.id, + idDosen: newestReg.penerima.id, + }); + } else { + // dto.status === RegStatus.REJECTED + await queryRunner.manager.delete(DosenBimbingan, { + idPendaftaran: newestReg.id, + }); + } - return { status: "ok" }; + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } + + return { id: newestReg.id } as IdDto; } async updatePembimbingList( @@ -390,9 +509,8 @@ export class RegistrasiTesisService { periode: string, { pembimbing_ids: dosen_ids }: UpdatePembimbingBodyDto, ) { - const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode); + const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId, periode); - // TODO decide to allow unapproved Registrations to have their Penerima changed or not if (newestReg.status !== RegStatus.APPROVED) throw new BadRequestException( "Cannot update pembimbing on non-approved registration", @@ -427,7 +545,7 @@ export class RegistrasiTesisService { (newId) => !newPembimbingIds.includes(newId), ); - const queryRunner = await this.dataSource.createQueryRunner(); + const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); @@ -443,10 +561,12 @@ export class RegistrasiTesisService { await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException(); } finally { await queryRunner.release(); } - return { status: "ok" }; + return { id: newestReg.id } as IdDto; } } diff --git a/src/submisi-tugas/submisi-tugas.controller.ts b/src/submisi-tugas/submisi-tugas.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb4f45a1b1cdb0d5535344ac9d8ccee62e4db8b9 --- /dev/null +++ b/src/submisi-tugas/submisi-tugas.controller.ts @@ -0,0 +1,104 @@ +import { + Body, + Controller, + Get, + Param, + Post, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { SubmisiTugasService } from "./submisi-tugas.service"; +import { + ApiBearerAuth, + ApiCookieAuth, + ApiOkResponse, + ApiOperation, + ApiTags, +} from "@nestjs/swagger"; +import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; +import { RolesGuard } from "src/middlewares/roles.guard"; +import { Roles } from "src/middlewares/roles.decorator"; +import { RoleEnum } from "src/entities/pengguna.entity"; +import { + CreateSubmisiTugasDto, + GetSubmisiTugasByIdRespDto, + GetSubmisiTugasByTugasIdQueryDto, + GetSubmisiTugasByTugasIdRespDto, + SubmisiTugasIdDto, +} from "./submisi-tugas.dto"; +import { AuthDto } from "src/auth/auth.dto"; +import { Request } from "express"; + +@ApiTags("Submisi Tugas") +@ApiBearerAuth() +@ApiCookieAuth() +@UseGuards(CustomAuthGuard, RolesGuard) +@Controller("submisi-tugas") +export class SubmisiTugasController { + constructor(private submisiTugasServ: SubmisiTugasService) {} + + @Roles(RoleEnum.S2_MAHASISWA) + @Post() + async createSubmisiTugas( + @Body() createDto: CreateSubmisiTugasDto, + @Req() req: Request, + ) { + // TODO: More validation + const { id } = req.user as AuthDto; + + return await this.submisiTugasServ.createSubmisiTugas(createDto, id); + } + + @ApiOperation({ + summary: + "Get submisi tugas by sumbisi tugas id. Roles: S2_KULIAH, S2_MAHASISWA", + }) + @ApiOkResponse({ type: GetSubmisiTugasByIdRespDto }) + @Roles(RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA) + @Get("/:id") + async getSubmisiTugasById( + @Req() req: Request, + @Param() param: SubmisiTugasIdDto, + ) { + const { id, roles } = req.user as AuthDto; + + let idMahasiswa = undefined; + let idPengajar = undefined; + + if (!roles.includes(RoleEnum.S2_KULIAH)) { + idMahasiswa = id; + } else { + idPengajar = id; + } + + return await this.submisiTugasServ.getSubmisiTugasById( + param.id, + idMahasiswa, + idPengajar, + ); + } + + @ApiOperation({ + summary: "Get list of submisi tugas summary by tugas id. Roles: S2_KULIAH", + }) + @ApiOkResponse({ type: [GetSubmisiTugasByTugasIdRespDto] }) + @Roles(RoleEnum.S2_KULIAH) + @Get() + async getSubmisiTugasByTugasId( + @Req() req: Request, + @Query() query: GetSubmisiTugasByTugasIdQueryDto, + ) { + const { id } = req.user as AuthDto; + + return await this.submisiTugasServ.getSubmisiTugasByTugasId( + query.tugasId, + id, + query.search || "", + query.page || 1, + query.limit || 10, + query.order || "ASC", + query.isSubmitted, + ); + } +} diff --git a/src/submisi-tugas/submisi-tugas.dto.ts b/src/submisi-tugas/submisi-tugas.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..daa8486087d0e417d1ef0c72f9b74e8b8cf15900 --- /dev/null +++ b/src/submisi-tugas/submisi-tugas.dto.ts @@ -0,0 +1,126 @@ +import { IsString } from "@nestjs/class-validator"; +import { + ApiProperty, + ApiPropertyOptional, + OmitType, + PickType, +} from "@nestjs/swagger"; +import { Transform, Type } from "class-transformer"; +import { + IsBoolean, + IsEnum, + IsNumberString, + IsOptional, + ValidateNested, +} from "class-validator"; +import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity"; +import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { SubmisiTugas } from "src/entities/submisiTugas.entity"; +import { GetTugasByIdRespDto } from "src/tugas/tugas.dto"; + +class BerkasSubmisiTugasWithoutId extends OmitType(BerkasSubmisiTugas, [ + "id", +] as const) {} + +export class CreateSubmisiTugasDto extends PickType(SubmisiTugas, [ + "jawaban", + "isSubmitted", + "tugasId", +]) { + @ApiProperty({ type: [BerkasSubmisiTugasWithoutId] }) + @ValidateNested({ each: true }) + @Type(() => BerkasSubmisiTugasWithoutId) + berkasSubmisiTugas: BerkasSubmisiTugasWithoutId[]; +} + +export class SubmisiTugasIdDto extends PickType(SubmisiTugas, [ + "id", +] as const) {} + +export class GetSubmisiTugasByTugasIdQueryDto extends PickType(SubmisiTugas, [ + "tugasId", +] as const) { + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @IsNumberString() + @ApiPropertyOptional({ description: "default: 1" }) + page?: number; + + @IsOptional() + @IsNumberString() + @ApiPropertyOptional({ description: "default: 10" }) + limit?: number; + + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === "true") + @ApiPropertyOptional({ + description: "if not specified, will return all submisi tugas", + }) + isSubmitted?: boolean; + + @ApiPropertyOptional({ + enum: ["ASC", "DESC"], + description: "order by nim. default: ASC", + }) + @IsOptional() + @IsEnum(["ASC", "DESC"]) + order?: "ASC" | "DESC"; +} + +class PickedSubmisiTugas extends PickType(SubmisiTugas, [ + "id", + "isSubmitted", + "berkasSubmisiTugas", +] as const) {} + +class PickedSubmisiTugasExtended extends PickType(SubmisiTugas, [ + "id", + "isSubmitted", + "jawaban", + "submittedAt", + "berkasSubmisiTugas", +] as const) {} + +class PickedPendaftaranTesis extends PickType(PendaftaranTesis, [ + "id", + "jalurPilihan", + "waktuPengiriman", + "jadwalInterview", + "status", + "topik", +] as const) {} + +class PickedPendaftaran extends PickType(Pengguna, [ + "id", + "nama", + "email", +] as const) { + @ApiProperty({ type: PickedPendaftaranTesis }) + pendaftaranTesis: PickedPendaftaranTesis; +} + +export class GetSubmisiTugasByTugasIdRespDto extends PickType(Pengguna, [ + "id", + "nim", + "nama", +] as const) { + @ApiPropertyOptional({ type: PickedSubmisiTugas }) + submisiTugas?: PickedSubmisiTugas; +} + +export class GetSubmisiTugasByIdRespDto { + @ApiProperty({ type: GetTugasByIdRespDto }) + tugas: GetTugasByIdRespDto; + + @ApiPropertyOptional({ type: PickedPendaftaran }) + pendaftaran?: PickedPendaftaran; + + @ApiProperty({ type: PickedSubmisiTugasExtended }) + submisiTugas: PickedSubmisiTugasExtended; +} diff --git a/src/submisi-tugas/submisi-tugas.module.ts b/src/submisi-tugas/submisi-tugas.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..1fd15428a499fe276ed0aeaef1b8168bd532b105 --- /dev/null +++ b/src/submisi-tugas/submisi-tugas.module.ts @@ -0,0 +1,31 @@ +import { Module } from "@nestjs/common"; +import { SubmisiTugasController } from "./submisi-tugas.controller"; +import { SubmisiTugasService } from "./submisi-tugas.service"; +import { CustomStrategy } from "src/middlewares/custom.strategy"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { AuthModule } from "src/auth/auth.module"; +import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; +import { SubmisiTugas } from "src/entities/submisiTugas.entity"; +import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity"; +import { TugasModule } from "src/tugas/tugas.module"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { Tugas } from "src/entities/tugas.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + SubmisiTugas, + BerkasSubmisiTugas, + Pengguna, + Tugas, + MahasiswaKelas, + ]), + AuthModule, + KonfigurasiModule, + TugasModule, + ], + controllers: [SubmisiTugasController], + providers: [SubmisiTugasService, CustomStrategy], +}) +export class SubmisiTugasModule {} diff --git a/src/submisi-tugas/submisi-tugas.service.ts b/src/submisi-tugas/submisi-tugas.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3c1c11a98b638aecbf96c5b67c1b8326073ef93 --- /dev/null +++ b/src/submisi-tugas/submisi-tugas.service.ts @@ -0,0 +1,257 @@ +import { + ForbiddenException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity"; +import { SubmisiTugas } from "src/entities/submisiTugas.entity"; +import { TugasService } from "src/tugas/tugas.service"; +import { Brackets, Repository } from "typeorm"; +import { + CreateSubmisiTugasDto, + GetSubmisiTugasByIdRespDto, + GetSubmisiTugasByTugasIdRespDto, +} from "./submisi-tugas.dto"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { Tugas } from "src/entities/tugas.entity"; +import { RegStatus } from "src/entities/pendaftaranTesis.entity"; +import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; + +@Injectable() +export class SubmisiTugasService { + constructor( + @InjectRepository(SubmisiTugas) + private submisiTugasRepo: Repository<SubmisiTugas>, + @InjectRepository(BerkasSubmisiTugas) + private berkasSubmisiTugasRepo: Repository<BerkasSubmisiTugas>, + @InjectRepository(Pengguna) + private penggunaRepo: Repository<Pengguna>, + @InjectRepository(Tugas) + private tugasRepo: Repository<Tugas>, + @InjectRepository(MahasiswaKelas) + private mahasiswaKelasRepo: Repository<MahasiswaKelas>, + private tugasService: TugasService, + private konfService: KonfigurasiService, + ) {} + private async isMahasiswaSubmisiTugasOrFail( + submisiTugasId: string, + mahasiswaId: string, + ) { + const submisiTugas = await this.submisiTugasRepo.findOne({ + where: { id: submisiTugasId }, + }); + + if (!submisiTugas) { + throw new NotFoundException("Submisi tugas tidak ditemukan"); + } + + if (submisiTugas.mahasiswaId !== mahasiswaId) { + throw new ForbiddenException("Anda tidak memiliki akses"); + } + + // validate periode + await this.tugasService.isMahasiswaTugasOrFail( + mahasiswaId, + submisiTugas.tugasId, + ); + } + + private async isPengajarSubmisiTugasOrFail( + submisiTugasId: string, + pengajarId: string, + ) { + const submisiTugas = await this.submisiTugasRepo.findOne({ + where: { id: submisiTugasId }, + relations: ["tugas"], + }); + + if (!submisiTugas) { + throw new NotFoundException("Submisi tugas tidak ditemukan"); + } + + await this.tugasService.isPengajarTugasOrFail( + pengajarId, + submisiTugas.tugas.id, + ); + } + + async createSubmisiTugas( + createDto: CreateSubmisiTugasDto, + mahasiswaId: string, + ) { + await this.tugasService.isMahasiswaTugasOrFail( + mahasiswaId, + createDto.tugasId, + ); + + const tugas = await this.tugasRepo.findOneBy({ id: createDto.tugasId }); + const mahasiswa = await this.penggunaRepo.findOneBy({ id: mahasiswaId }); + + const berkasSubmisiTugas = createDto.berkasSubmisiTugas.map( + (berkasSubmisiTugas) => + this.berkasSubmisiTugasRepo.create(berkasSubmisiTugas), + ); + + const submisiTugas = this.submisiTugasRepo.create({ + ...createDto, + mahasiswa, + submittedAt: createDto.isSubmitted ? new Date() : null, + tugas, + berkasSubmisiTugas, + }); + + await this.submisiTugasRepo.save(submisiTugas); + + return submisiTugas; + } + + private async getSubmisiTugas(id: string) { + const submisiTugas = await this.submisiTugasRepo.findOne({ + where: { id }, + relations: ["berkasSubmisiTugas"], + }); + + if (!submisiTugas) { + throw new NotFoundException("Submisi tugas tidak ditemukan"); + } + + return submisiTugas; + } + + async getSubmisiTugasById( + id: string, + mahasiswaId?: string, + pengajarId?: string, + ) { + if (mahasiswaId) { + await this.isMahasiswaSubmisiTugasOrFail(id, mahasiswaId); + } + + if (pengajarId) { + await this.isPengajarSubmisiTugasOrFail(id, pengajarId); + } + + const currPeriod = await this.konfService.getPeriodeOrFail(); + const submisiTugas = await this.getSubmisiTugas(id); + + const pendaftaranQuery = this.penggunaRepo + .createQueryBuilder("pengguna") + .select([ + "pengguna.id", + "pengguna.nama", + "pengguna.email", + "pendaftaranTesis.jalurPilihan", + "pendaftaranTesis.waktuPengiriman", + "topik.id", + "topik.judul", + "topik.deskripsi", + ]) + .leftJoinAndSelect("pengguna.pendaftaranTesis", "pendaftaranTesis") + .leftJoinAndSelect("pendaftaranTesis.topik", "topik") + .where("pengguna.id = :id", { id: submisiTugas.mahasiswaId }) + .andWhere("pendaftaranTesis.status = :status", { + status: RegStatus.APPROVED, + }) + .andWhere("topik.periode = :periode", { periode: currPeriod }) + .getOne(); + + const [tugas, pendaftaran] = await Promise.all([ + this.tugasService.getTugasById(submisiTugas.tugasId), + pendaftaranQuery, + ]); + + const result: GetSubmisiTugasByIdRespDto = { + tugas, + submisiTugas, + pendaftaran: { + ...pendaftaran, + pendaftaranTesis: + pendaftaran.pendaftaranTesis.length > 0 + ? pendaftaran.pendaftaranTesis[0] + : undefined, + }, + }; + + return result; + } + + async getSubmisiTugasByTugasId( + tugasId: string, + idPenerima: string, + search: string, + page: number, + limit: number, + order: "ASC" | "DESC", + isSubmitted?: boolean, + ) { + await this.tugasService.isPengajarTugasOrFail(idPenerima, tugasId); + + const baseQuery = await this.mahasiswaKelasRepo + .createQueryBuilder("mk") + .innerJoin("mk.kelas", "kelas", "kelas.id = mk.kelasId") + .innerJoinAndSelect("mk.mahasiswa", "mahasiswa") + .leftJoinAndSelect( + "mahasiswa.submisiTugas", + "submisiTugas", + "submisiTugas.tugasId = :tugasId", + { tugasId }, + ) + .leftJoinAndSelect( + "submisiTugas.berkasSubmisiTugas", + "berkasSubmisiTugas", + ) + .select([ + "mk.id", + "mahasiswa.id", + "mahasiswa.nim", + "mahasiswa.nama", + "submisiTugas.id", + "submisiTugas.isSubmitted", + "berkasSubmisiTugas", + ]) + .distinctOn(["mahasiswa.nim"]) + .where( + new Brackets((qb) => { + qb.where("mahasiswa.nama ILIKE :search", { + search: `%${search}%`, + }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` }); + }), + ) + .orderBy("mahasiswa.nim", order); + + if (isSubmitted !== undefined) { + if (isSubmitted) { + baseQuery.andWhere("submisiTugas.isSubmitted = true"); + } else { + baseQuery.andWhere( + new Brackets((qb) => + qb + .where("submisiTugas.isSubmitted <> true") + .orWhere("submisiTugas.isSubmitted IS NULL"), + ), + ); + } + } + + const submisiTugas = await baseQuery + .limit(limit) + .skip((page - 1) * limit) + .getMany(); + + const mappedResult: GetSubmisiTugasByTugasIdRespDto[] = submisiTugas.map( + (submisi) => ({ + id: submisi.mahasiswa.id, + nim: submisi.mahasiswa.nim, + nama: submisi.mahasiswa.nama, + submisiTugas: + submisi.mahasiswa.submisiTugas.length > 0 + ? submisi.mahasiswa.submisiTugas[0] + : undefined, + }), + ); + + return mappedResult; + } +} diff --git a/src/tugas/tugas.controller.ts b/src/tugas/tugas.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..b66f4efe167c7a14fea84ac9c710f4f559155186 --- /dev/null +++ b/src/tugas/tugas.controller.ts @@ -0,0 +1,105 @@ +import { + Body, + Controller, + Get, + Param, + Post, + Put, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { TugasService } from "./tugas.service"; +import { Roles } from "src/middlewares/roles.decorator"; +import { RoleEnum } from "src/entities/pengguna.entity"; +import { + ApiBearerAuth, + ApiCookieAuth, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from "@nestjs/swagger"; +import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; +import { RolesGuard } from "src/middlewares/roles.guard"; +import { + TugasIdDto, + CreateTugasDto, + UpdateTugasDto, + GetTugasByIdRespDto, + GetTugasByKelasIdQueryDto, + GetTugasByKelasIdRespDto, +} from "./tugas.dto"; +import { Request } from "express"; +import { AuthDto } from "src/auth/auth.dto"; + +@ApiCookieAuth() +@ApiBearerAuth() +@ApiTags("Tugas") +@UseGuards(CustomAuthGuard, RolesGuard) +@Controller("tugas") +export class TugasController { + constructor(private readonly tugasService: TugasService) {} + + @ApiOperation({ summary: "Create Tugas. Roles: S2_KULIAH" }) + @ApiCreatedResponse({ type: TugasIdDto }) + @Roles(RoleEnum.S2_KULIAH) + @Post() + async createTugas(@Body() createDto: CreateTugasDto, @Req() req: Request) { + const { id } = req.user as AuthDto; + + return await this.tugasService.createTugas(createDto, id); + } + + @ApiOperation({ summary: "Update Tugas. Roles: S2_KULIAH" }) + @ApiOkResponse({ type: TugasIdDto }) + @Roles(RoleEnum.S2_KULIAH) + @Put() + async updateTugas(@Body() updateDto: UpdateTugasDto, @Req() req: Request) { + const { id } = req.user as AuthDto; + + return await this.tugasService.updateTugasById(updateDto, id); + } + + @ApiOperation({ + summary: "Get Tugas by id. Roles: S2_KULIAH, S2_MAHASISWA", + }) + @ApiOkResponse({ type: GetTugasByIdRespDto }) + @Roles(RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA) + @Get("/:id") + async getTugasById(@Param() param: TugasIdDto, @Req() req: Request) { + let idPengajar = undefined; + let idMahasiswa = undefined; + + const { id, roles } = req.user as AuthDto; + + if (!roles.includes(RoleEnum.S2_KULIAH)) { + idMahasiswa = id; + } else { + idPengajar = id; + } + + return this.tugasService.getTugasById(param.id, idMahasiswa, idPengajar); + } + + @ApiOperation({ + summary: "Get Tugas list by kelas id. Roles: S2_KULIAH", + }) + @ApiOkResponse({ type: GetTugasByKelasIdRespDto }) + @Roles(RoleEnum.S2_KULIAH) + @Get() + async getTugasByKelasId( + @Req() req: Request, + @Query() query: GetTugasByKelasIdQueryDto, + ) { + const { id } = req.user as AuthDto; + + return this.tugasService.getTugasByKelasId( + query.kelasId, + id, + query.search || "", + query.page || 1, + query.limit || 10, + ); + } +} diff --git a/src/tugas/tugas.dto.ts b/src/tugas/tugas.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..afbb6eff67825b726e20169db6dee9e73d79f0ce --- /dev/null +++ b/src/tugas/tugas.dto.ts @@ -0,0 +1,109 @@ +import { + ApiProperty, + ApiPropertyOptional, + OmitType, + PickType, +} from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { + IsNumberString, + IsOptional, + IsString, + IsUUID, + ValidateNested, +} from "class-validator"; +import { BerkasTugas } from "src/entities/berkasTugas.entity"; +import { Kelas } from "src/entities/kelas.entity"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { Tugas } from "src/entities/tugas.entity"; +import { GetKelasRespDto } from "src/kelas/kelas.dto"; + +class BerkasTugasWithoutId extends OmitType(BerkasTugas, ["id"] as const) {} + +export class CreateTugasDto extends PickType(Tugas, [ + "judul", + "waktuMulai", + "waktuSelesai", + "deskripsi", + "kelasId", +]) { + @ApiProperty({ type: [BerkasTugasWithoutId] }) + @ValidateNested({ each: true }) + @Type(() => BerkasTugasWithoutId) + berkasTugas: BerkasTugasWithoutId[]; +} + +export class UpdateTugasDto extends OmitType(CreateTugasDto, [ + "kelasId", +] as const) { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + id: string; +} + +export class TugasIdDto extends PickType(Tugas, ["id"] as const) {} + +class PickedPengajarKelas extends PickType(Pengguna, ["id", "nama"] as const) {} + +class PickedTugasKelas extends PickType(Kelas, [ + "id", + "nomor", + "mataKuliah", +] as const) {} + +export class GetTugasByIdRespDto extends PickType(Tugas, [ + "id", + "judul", + "waktuMulai", + "waktuSelesai", + "deskripsi", + "createdAt", + "updatedAt", + "berkasTugas", +] as const) { + @ApiProperty({ type: PickedPengajarKelas }) + pembuat: PickedPengajarKelas; + + @ApiProperty({ type: PickedPengajarKelas }) + pengubah: PickedPengajarKelas; + + @ApiProperty({ type: PickedTugasKelas }) + kelas: PickedTugasKelas; +} + +export class GetTugasByKelasIdQueryDto extends PickType(Tugas, [ + "kelasId", +] as const) { + @ApiPropertyOptional() + @IsOptional() + @IsString() + search?: string; + + @IsOptional() + @IsNumberString() + @ApiPropertyOptional({ description: "default: 1" }) + page?: number; + + @IsOptional() + @IsNumberString() + @ApiPropertyOptional({ description: "default: 10" }) + limit?: number; +} + +export class GetTugasSummaryRespDto extends PickType(Tugas, [ + "id", + "judul", + "waktuMulai", + "waktuSelesai", +] as const) { + @ApiProperty() + totalSubmisi: number; +} + +export class GetTugasByKelasIdRespDto { + @ApiProperty({ type: [GetTugasSummaryRespDto] }) + tugas: GetTugasSummaryRespDto[]; + + @ApiProperty({ type: GetKelasRespDto }) + kelas: GetKelasRespDto; +} diff --git a/src/tugas/tugas.module.ts b/src/tugas/tugas.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a8d49258c546ffaa9d9d95014f4d10af9190442 --- /dev/null +++ b/src/tugas/tugas.module.ts @@ -0,0 +1,44 @@ +import { Module } from "@nestjs/common"; +import { TugasController } from "./tugas.controller"; +import { TugasService } from "./tugas.service"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { PengajarKelas } from "src/entities/pengajarKelas.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; +import { Tugas } from "src/entities/tugas.entity"; +import { SubmisiTugas } from "src/entities/submisiTugas.entity"; +import { AuthModule } from "src/auth/auth.module"; +import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; +import { CustomStrategy } from "src/middlewares/custom.strategy"; +import { BerkasTugas } from "src/entities/berkasTugas.entity"; +import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity"; +import { Kelas } from "src/entities/kelas.entity"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { KelasModule } from "src/kelas/kelas.module"; +import { KelasService } from "src/kelas/kelas.service"; +import { MataKuliah } from "src/entities/mataKuliah.entity"; +import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; +import { Konfigurasi } from "src/entities/konfigurasi.entity"; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + PengajarKelas, + MahasiswaKelas, + Tugas, + SubmisiTugas, + BerkasTugas, + BerkasSubmisiTugas, + Kelas, + Pengguna, + MataKuliah, + Konfigurasi, + ]), + AuthModule, + KonfigurasiModule, + KelasModule, + ], + controllers: [TugasController], + providers: [TugasService, CustomStrategy, KelasService, KonfigurasiService], + exports: [TugasService], +}) +export class TugasModule {} diff --git a/src/tugas/tugas.service.ts b/src/tugas/tugas.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f99625c045bdc011de895afd60bd01371658ecb --- /dev/null +++ b/src/tugas/tugas.service.ts @@ -0,0 +1,277 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Tugas } from "src/entities/tugas.entity"; +import { Repository } from "typeorm"; +import { + CreateTugasDto, + GetTugasByIdRespDto, + GetTugasByKelasIdRespDto, + GetTugasSummaryRespDto, + TugasIdDto, + UpdateTugasDto, +} from "./tugas.dto"; +import { BerkasTugas } from "src/entities/berkasTugas.entity"; +import { SubmisiTugas } from "src/entities/submisiTugas.entity"; +import { PengajarKelas } from "src/entities/pengajarKelas.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; +import { Kelas } from "src/entities/kelas.entity"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { KelasService } from "src/kelas/kelas.service"; +import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; +import * as dayjs from "dayjs"; + +@Injectable() +export class TugasService { + constructor( + @InjectRepository(Tugas) private tugasRepo: Repository<Tugas>, + @InjectRepository(BerkasTugas) + private berkasTugasRepo: Repository<BerkasTugas>, + @InjectRepository(SubmisiTugas) + private submisiTugasRepo: Repository<SubmisiTugas>, + @InjectRepository(PengajarKelas) + private doskelRepo: Repository<PengajarKelas>, + @InjectRepository(MahasiswaKelas) + private mahasiswaKelasRepo: Repository<MahasiswaKelas>, + @InjectRepository(Kelas) + private kelasRepo: Repository<Kelas>, + @InjectRepository(Pengguna) + private penggunaRepo: Repository<Pengguna>, + private kelasService: KelasService, + private konfService: KonfigurasiService, + ) {} + + private async isPengajarKelasOrFail(pengajarId: string, kelasId: string) { + const periode = await this.konfService.getPeriodeOrFail(); + + const doskel = await this.doskelRepo.findOne({ + where: { + pengajarId, + kelasId, + kelas: { + periode, + }, + }, + relations: ["kelas"], + }); + + if (!doskel) { + throw new ForbiddenException("Anda tidak memiliki akses"); + } + } + + private async isMahasiswaKelasOrFail(mahasiswaId: string, kelasId: string) { + const periode = await this.konfService.getPeriodeOrFail(); + + const mahasiswaKelas = await this.mahasiswaKelasRepo.findOne({ + where: { + mahasiswaId, + kelasId, + kelas: { + periode, + }, + }, + relations: ["kelas"], + }); + + if (!mahasiswaKelas) { + throw new ForbiddenException("Anda tidak memiliki akses"); + } + } + + async isPengajarTugasOrFail(pengajarId: string, tugasId: string) { + const tugas = await this.tugasRepo.findOne({ + where: { id: tugasId }, + }); + + if (!tugas) { + throw new NotFoundException("Tugas tidak ditemukan"); + } + + await this.isPengajarKelasOrFail(pengajarId, tugas.kelasId); + } + + async isMahasiswaTugasOrFail(mahasiswaId: string, tugasId: string) { + const tugas = await this.tugasRepo.findOne({ + where: { id: tugasId }, + }); + + if (!tugas) { + throw new NotFoundException("Tugas tidak ditemukan"); + } + + return await this.isMahasiswaKelasOrFail(mahasiswaId, tugas.kelasId); + } + + private async getTugas(tugasId: string): Promise<GetTugasByIdRespDto> { + const result: GetTugasByIdRespDto[] = await this.tugasRepo + .createQueryBuilder("tugas") + .leftJoinAndSelect("tugas.pembuat", "pembuat") + .leftJoinAndSelect("tugas.pengubah", "pengubah") + .leftJoinAndSelect("tugas.kelas", "kelas") + .leftJoinAndSelect("kelas.mataKuliah", "mataKuliah") + .leftJoinAndSelect("tugas.berkasTugas", "berkasTugas") + .select([ + "tugas.id", + "pembuat.id", + "pembuat.nama", + "pengubah.id", + "pengubah.nama", + "tugas.judul", + "tugas.waktuMulai", + "tugas.waktuSelesai", + "tugas.deskripsi", + "tugas.createdAt", + "tugas.updatedAt", + "berkasTugas", + "kelas.id", + "kelas.nomor", + "mataKuliah.kode", + "mataKuliah.nama", + ]) + .where("tugas.id = :tugasId", { tugasId }) + .getMany(); + + return result[0]; + } + + async createTugas( + createDto: CreateTugasDto, + pembuatId: string, + ): Promise<TugasIdDto> { + await this.isPengajarKelasOrFail(pembuatId, createDto.kelasId); + + if (dayjs(createDto.waktuMulai).isAfter(dayjs(createDto.waktuSelesai))) { + throw new BadRequestException( + "Waktu mulai tidak boleh setelah waktu selesai", + ); + } + + const kelas = await this.kelasRepo.findOne({ + where: { id: createDto.kelasId }, + }); + + const pembuat = await this.penggunaRepo.findOne({ + where: { id: pembuatId }, + }); + + const berkasTugas = createDto.berkasTugas.map((berkas) => + this.berkasTugasRepo.create(berkas), + ); + + const tugas = this.tugasRepo.create({ + ...createDto, + kelas, + pembuat, + pengubah: pembuat, + berkasTugas, + }); + + const result = await this.tugasRepo.save(tugas); + + return { id: result.id }; + } + + async updateTugasById( + updateDto: UpdateTugasDto, + pengubahId: string, + ): Promise<TugasIdDto> { + await this.isPengajarTugasOrFail(pengubahId, updateDto.id); + + if (dayjs(updateDto.waktuMulai).isAfter(dayjs(updateDto.waktuSelesai))) { + throw new BadRequestException( + "Waktu mulai tidak boleh setelah waktu selesai", + ); + } + + const berkasTugas = updateDto.berkasTugas.map((berkas) => + this.berkasTugasRepo.create(berkas), + ); + + const pengubah = await this.penggunaRepo.findOne({ + where: { id: pengubahId }, + }); + + const prevTugas = await this.tugasRepo.findOne({ + where: { id: updateDto.id }, + relations: ["kelas", "pembuat"], + }); + + const data = { + ...updateDto, + updatedAt: new Date(), + pengubah, + kelas: prevTugas.kelas, + pembuat: prevTugas.pembuat, + berkasTugas, + }; + + await this.tugasRepo.save(data); + + return { id: updateDto.id }; + } + + async getTugasById(id: string, idMahasiswa?: string, idPengajar?: string) { + if (idMahasiswa) { + await this.isMahasiswaTugasOrFail(idMahasiswa, id); + } + + if (idPengajar) { + await this.isPengajarTugasOrFail(idPengajar, id); + } + + const result = await this.getTugas(id); + + return result; + } + + async getTugasByKelasId( + kelasId: string, + idPengajar: string, + search: string, + page: number, + limit: number, + ): Promise<GetTugasByKelasIdRespDto> { + await this.isPengajarKelasOrFail(idPengajar, kelasId); + + const kelasQuery = this.kelasService.getById(kelasId); + const tugasQuery = await this.tugasRepo + .createQueryBuilder("tugas") + .leftJoinAndSelect( + "tugas.submisiTugas", + "submisi_tugas", + "submisi_tugas.isSubmitted = true", + ) + .select([ + "tugas.id AS id", + "tugas.judul AS judul", + "tugas.waktuMulai AS waktu_mulai", + "tugas.waktuSelesai AS waktu_selesai", + "COUNT(submisi_tugas) AS total_submisi", + ]) + .where("tugas.kelasId = :kelasId", { + kelasId, + }) + .andWhere("tugas.judul ILIKE :search", { search: `%${search}%` }) + .groupBy("tugas.id") + .orderBy("tugas.createdAt", "DESC") + .limit(limit) + .skip((page - 1) * limit) + .getRawMany(); + + const [kelas, tugas] = await Promise.all([kelasQuery, tugasQuery]); + const mappedTugas: GetTugasSummaryRespDto[] = tugas.map((tugas) => ({ + id: tugas.id, + judul: tugas.judul, + waktuMulai: tugas.waktu_mulai, + waktuSelesai: tugas.waktu_selesai, + totalSubmisi: parseInt(tugas.total_submisi), + })); + + return { kelas, tugas: mappedTugas }; + } +}