diff --git a/.env.example b/.env.example index 72f7f62791250d9e59c7aa77dcac87908056c761..636ce5ecb1f6cbdeac73878e12263937afd06f5b 100644 --- a/.env.example +++ b/.env.example @@ -6,15 +6,4 @@ POSTGRES_DATABASE= AUTH_SERVICE_URL=http://localhost:3001 FE_URL=http://localhost:5173 -KONF_PERIODE_KEY=PERIODE -KONF_MIN_BIMBINGAN_KEY=MINIMAL_BIMBINGAN -KONF_AWAL_PENDAFTARAN_KEY=AWAL_PENDAFTARAN -KONF_AKHIR_PENDAFTARAN_KEY=AKHIR_PENDAFTARAN -KONF_AWAL_SEMPRO_KEY=AWAL_SEMPRO -KONF_AKHIR_SEMPRO_KEY=AKHIR_SEMPRO -KONF_AWAL_SEM_TESIS_KEY=AWAL_SEM_TESIS -KONF_AKHIR_SEM_TESIS_KEY=AKHIR_SEM_TESIS -KONF_AWAL_SIDANG_KEY=AWAL_SIDANG -KONF_AKHIR_SIDANG_KEY=AKHIR_SIDANG - COOKIE_NAME=gradu-it.access-token diff --git a/package-lock.json b/package-lock.json index bd68a73e016611adc98632e4bcfadd3d500ebfa3..88b4e7cf5b71bf20a1847997a4f1f8261ae0640b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1944,13 +1944,13 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.3.tgz", - "integrity": "sha512-GGKSEU48Os7nYFIsUM0nutuFUGn5AbeP8gzFBiBCAtiuJWrXZXpZ58pMBYxAbMf7IrcOZFInHEukjHGAQU0OZw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.2", + "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" }, @@ -3851,9 +3851,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -4593,16 +4593,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -4633,29 +4633,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4674,20 +4651,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -4937,9 +4900,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", diff --git a/src/alokasi-ruangan/alokasi-ruangan.controller.ts b/src/alokasi-ruangan/alokasi-ruangan.controller.ts index 1994fd435d23c13bf931d45fdd02a01344096301..7d8b01d99043b3b6827afdd02095b5903fd0c0d5 100644 --- a/src/alokasi-ruangan/alokasi-ruangan.controller.ts +++ b/src/alokasi-ruangan/alokasi-ruangan.controller.ts @@ -13,7 +13,6 @@ import { ApiOkResponse, 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"; @@ -25,12 +24,13 @@ import { UpdateAlokasiRuanganRespDto, } from "./alokasi-ruangan.dto"; import { AlokasiRuanganService } from "./alokasi-ruangan.service"; +import { HIGH_AUTHORITY_ROLES } from "src/helper/roles"; @ApiTags("Alokasi Ruangan") @ApiBearerAuth() @ApiCookieAuth() @UseGuards(CustomAuthGuard, RolesGuard) -@Roles(RoleEnum.ADMIN, RoleEnum.TU) +@Roles(...HIGH_AUTHORITY_ROLES) @Controller("alokasi-ruangan") export class AlokasiRuanganController { constructor(private readonly alokasiRuanganService: AlokasiRuanganService) {} diff --git a/src/alokasi-topik/alokasi-topik.controller.ts b/src/alokasi-topik/alokasi-topik.controller.ts index 6fa4e26ac50478a46780cab0a29e11ccda0d7035..069ae9fb2290d3831476d20cb7068beae81fbed4 100644 --- a/src/alokasi-topik/alokasi-topik.controller.ts +++ b/src/alokasi-topik/alokasi-topik.controller.ts @@ -9,22 +9,24 @@ import { Post, Put, Query, + Req, UseGuards, } from "@nestjs/common"; import { ApiBearerAuth, ApiCookieAuth, + ApiCreatedResponse, ApiOkResponse, + ApiOperation, ApiTags, } from "@nestjs/swagger"; import { RoleEnum } from "src/entities/pengguna.entity"; -import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; import { Roles } from "src/middlewares/roles.decorator"; import { RolesGuard } from "src/middlewares/roles.guard"; import { CreateBulkTopikDto, - CreateRespDto, + TopikIdRespDto, CreateTopikDto, GetAllRespDto, OmittedTopik, @@ -34,6 +36,9 @@ import { createBulkRespDto, } from "./alokasi-topik.dto"; import { AlokasiTopikService } from "./alokasi-topik.service"; +import { Request } from "express"; +import { AuthDto } from "src/auth/auth.dto"; +import { HIGH_AUTHORITY_ROLES, isHighAuthority } from "src/helper/roles"; @ApiTags("Alokasi Topik") @ApiCookieAuth() @@ -41,82 +46,129 @@ import { AlokasiTopikService } from "./alokasi-topik.service"; @Controller("alokasi-topik") @UseGuards(CustomAuthGuard, RolesGuard) export class AlokasiTopikController { - constructor( - private alokasiTopikService: AlokasiTopikService, - private konfService: KonfigurasiService, - ) {} + constructor(private alokasiTopikService: AlokasiTopikService) {} - @ApiOkResponse({ type: CreateRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Create new topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING", + }) + @ApiCreatedResponse({ type: TopikIdRespDto }) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING) @Post() - async create(@Body() createDto: CreateTopikDto) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) throw new BadRequestException("Periode belum dikonfigurasi."); + async create( + @Body() createDto: CreateTopikDto, + @Req() req: Request, + ): Promise<TopikIdRespDto> { + const { roles, id } = req.user as AuthDto; + // user only has S2_PEMBIMBING role + if (!isHighAuthority(roles) && createDto.idPengaju !== id) { + throw new BadRequestException("Pengaju ID harus sama dengan user ID"); + } - return await this.alokasiTopikService.create({ ...createDto, periode }); + return await this.alokasiTopikService.create(createDto); } + @ApiOperation({ + summary: "Create multiple topik. Roles: S2_TIM_TESIS, ADMIN", + }) @ApiOkResponse({ type: createBulkRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @Roles(...HIGH_AUTHORITY_ROLES) @Post("/bulk") async createBulk(@Body() createDto: CreateBulkTopikDto) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) throw new BadRequestException("Periode belum dikonfigurasi."); - - return await this.alokasiTopikService.createBulk(createDto, periode); + return await this.alokasiTopikService.createBulk(createDto); } + @ApiOperation({ + summary: + "Get topik by ID. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING, S2_MAHASISWA", + }) @ApiOkResponse({ type: OmittedTopik }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING, RoleEnum.S2_MAHASISWA) @Get("/:id") async getById(@Param() params: TopikParamDto) { - const res = await this.alokasiTopikService.findById(params.id); + const res = await this.alokasiTopikService.findActiveTopikById(params.id); if (!res) throw new NotFoundException(); return res as OmittedTopik; } + @ApiOperation({ + summary: + "Get all topik. Roles: S2_TIM_TESIS, ADMIN, S2_MAHASISWA, S2_PEMBIMBING", + }) @ApiOkResponse({ type: GetAllRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN, RoleEnum.S2_MAHASISWA) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_MAHASISWA, RoleEnum.S2_PEMBIMBING) @Get() async getAll( @Query() query: TopikQueryDto, ) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, + return await this.alokasiTopikService.findAllActiveTopikCreatedByPembimbing( + { + ...query, + page: query.page || 1, + }, ); - - if (!periode) throw new BadRequestException("Periode belum dikonfigurasi."); - - return await this.alokasiTopikService.findAllCreatedByPembimbing({ - page: query.page || 1, - ...query, - periode, - }); } - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Update topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING", + }) + @ApiOkResponse({ type: TopikIdRespDto }) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING) @Put("/:id") async update( @Param() params: TopikParamDto, @Body() updateDto: UpdateTopikDto, - ) { - const res = await this.alokasiTopikService.update(params.id, updateDto); - if (!res.affected) throw new NotFoundException(); - return res; + @Req() req: Request, + ): Promise<TopikIdRespDto> { + let idPengaju = undefined; + const { roles, id } = req.user as AuthDto; + // user only has S2_PEMBIMBING role + if (!isHighAuthority(roles)) { + if (updateDto.idPengaju !== id) { + throw new BadRequestException("Pengaju ID harus sama dengan user ID"); + } + idPengaju = id; + } + + const res = await this.alokasiTopikService.update( + params.id, + updateDto, + idPengaju, + ); + if (!res.affected) + throw new NotFoundException( + "Topik tidak ditemukan di antara topik yang dapat Anda akses", + ); + + const resp: TopikIdRespDto = { id: params.id }; + + return resp; } - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Delete topik. Roles: S2_TIM_TESIS, ADMIN, S2_PEMBIMBING", + }) + @ApiOkResponse({ type: TopikIdRespDto }) + @Roles(...HIGH_AUTHORITY_ROLES, RoleEnum.S2_PEMBIMBING) @Delete("/:id") - async delete(@Param() params: TopikParamDto) { - const res = await this.alokasiTopikService.remove(params.id); - if (!res.affected) throw new NotFoundException(); - return res; + async delete( + @Param() params: TopikParamDto, + @Req() req: Request, + ): Promise<TopikIdRespDto> { + let idPengaju = undefined; + const { roles, id } = req.user as AuthDto; + // user only has S2_PEMBIMBING role + if (!isHighAuthority(roles)) { + idPengaju = id; + } + + const res = await this.alokasiTopikService.remove(params.id, idPengaju); + if (!res.affected) + throw new NotFoundException( + "Topik tidak ditemukan di antara topik yang dapat Anda akses", + ); + + const resp: TopikIdRespDto = { id: params.id }; + return resp; } } diff --git a/src/alokasi-topik/alokasi-topik.dto.ts b/src/alokasi-topik/alokasi-topik.dto.ts index 93e7dd82db19970390d4cef921e1a774ed2ffabc..5c41225c23ddf1fbefe996dfc7f961ad5ecebc01 100644 --- a/src/alokasi-topik/alokasi-topik.dto.ts +++ b/src/alokasi-topik/alokasi-topik.dto.ts @@ -61,7 +61,7 @@ export class TopikQueryDto { idPembimbing?: string; } -export class PengajuDto extends OmitType(Pengguna, ["nim"] as const) {} +export class PengajuDto extends OmitType(Pengguna, ["nim", "aktif"] as const) {} export class OmittedTopik extends OmitType(Topik, ["idPengaju"] as const) { @ApiProperty({ type: PengajuDto }) @@ -81,7 +81,7 @@ export class createBulkRespDto { ids: string[]; } -export class CreateRespDto { +export class TopikIdRespDto { @ApiProperty() id: string; } diff --git a/src/alokasi-topik/alokasi-topik.module.ts b/src/alokasi-topik/alokasi-topik.module.ts index e9769a4998d5a192e5ee21fa17ad23f4449353fb..f7daaff6363cf0e2d4d1bcedeaea2c38023f6a67 100644 --- a/src/alokasi-topik/alokasi-topik.module.ts +++ b/src/alokasi-topik/alokasi-topik.module.ts @@ -5,10 +5,9 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Topik } from "src/entities/topik.entity"; import { CustomStrategy } from "src/middlewares/custom.strategy"; import { AuthModule } from "src/auth/auth.module"; -import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; @Module({ - imports: [TypeOrmModule.forFeature([Topik]), AuthModule, KonfigurasiModule], + imports: [TypeOrmModule.forFeature([Topik]), AuthModule], providers: [AlokasiTopikService, CustomStrategy], controllers: [AlokasiTopikController], }) diff --git a/src/alokasi-topik/alokasi-topik.service.ts b/src/alokasi-topik/alokasi-topik.service.ts index b9056ab744f4cb42d6fc970e629cf3f189afcaec..d52534adfd7f8155ba67dbf0ffe4684b689e9773 100644 --- a/src/alokasi-topik/alokasi-topik.service.ts +++ b/src/alokasi-topik/alokasi-topik.service.ts @@ -2,10 +2,10 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { RoleEnum } from "src/entities/pengguna.entity"; import { Topik } from "src/entities/topik.entity"; -import { ArrayContains, Like, Repository } from "typeorm"; +import { ArrayContains, ILike, Repository } from "typeorm"; import { CreateBulkTopikDto, - CreateRespDto, + TopikIdRespDto, CreateTopikDto, GetAllRespDto, UpdateTopikDto, @@ -16,35 +16,26 @@ import { export class AlokasiTopikService { constructor(@InjectRepository(Topik) private topikRepo: Repository<Topik>) {} - async create( - createDto: CreateTopikDto & { periode: string }, - ): Promise<CreateRespDto> { + async create(createDto: CreateTopikDto): Promise<TopikIdRespDto> { const ids = (await this.topikRepo.insert(createDto)).identifiers; return { id: ids[0].id }; } - async createBulk( - createDto: CreateBulkTopikDto, - periode: string, - ): Promise<createBulkRespDto> { + async createBulk(createDto: CreateBulkTopikDto): Promise<createBulkRespDto> { const ids = ( - await this.topikRepo.insert( - createDto.data.map((dto) => ({ ...dto, periode })), - ) + await this.topikRepo.insert(createDto.data.map((dto) => ({ ...dto }))) ).identifiers; return { ids: ids.map(({ id }) => id) }; } - async findById(id: string) { - // not periode-protected + async findActiveTopikById(id: string) { return await this.topikRepo.findOne({ select: { id: true, judul: true, deskripsi: true, - periode: true, pengaju: { id: true, nama: true, @@ -54,6 +45,7 @@ export class AlokasiTopikService { }, where: { id, + aktif: true, }, relations: { pengaju: true, @@ -61,19 +53,17 @@ export class AlokasiTopikService { }); } - async findAllCreatedByPembimbing(options: { + async findAllActiveTopikCreatedByPembimbing(options: { page: number; limit?: number; search?: string; idPembimbing?: string; - periode: string; }): Promise<GetAllRespDto> { const dataQuery = this.topikRepo.find({ select: { id: true, judul: true, deskripsi: true, - periode: true, pengaju: { id: true, nama: true, @@ -82,12 +72,12 @@ export class AlokasiTopikService { }, }, where: { - periode: options.periode, + aktif: true, pengaju: { id: options.idPembimbing || undefined, roles: ArrayContains([RoleEnum.S2_PEMBIMBING]), }, - judul: Like(`%${options.search || ""}%`), + judul: ILike(`%${options.search || ""}%`), }, relations: { pengaju: true, @@ -107,8 +97,7 @@ export class AlokasiTopikService { .createQueryBuilder("topik") .select("topik.id") .innerJoinAndSelect("topik.pengaju", "pengaju") - .where("topik.periode = :periode", { periode: options.periode }) - .andWhere("pengaju.roles @> :role", { + .where("pengaju.roles @> :role", { role: [RoleEnum.S2_PEMBIMBING], }); @@ -142,13 +131,15 @@ export class AlokasiTopikService { } } - async update(id: string, updateDto: UpdateTopikDto) { - // not periode-protected - return await this.topikRepo.update(id, updateDto); + async update(id: string, updateDto: UpdateTopikDto, idPengaju?: string) { + const findOpt = idPengaju + ? { id, idPengaju, aktif: true } + : { id, aktif: true }; + return await this.topikRepo.update(findOpt, updateDto); } - async remove(id: string) { - // not periode-protected - return await this.topikRepo.delete({ id }); // TODO: manage relation cascading option + async remove(id: string, idPengaju?: string) { + const findOpt = idPengaju ? { id, idPengaju } : { id }; + return await this.topikRepo.update(findOpt, { aktif: false }); } } diff --git a/src/app.module.ts b/src/app.module.ts index 0898cf406d426d16670b566e96a8f75eb025710e..1fa72cf33516b968b419451019bbdaa1725db5a9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,39 +2,26 @@ import { Module } from "@nestjs/common"; import { TypeOrmModule } from "@nestjs/typeorm"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; -import { AuditLog } from "./entities/auditLog.entity"; import { Bimbingan } from "./entities/bimbingan.entity"; -import { DosenBimbingan } from "./entities/dosenBimbingan.entity"; -import { Kelas } from "./entities/kelas.entity"; -import { MahasiswaKelas } from "./entities/mahasiswaKelas.entity"; -import { PendaftaranTesis } from "./entities/pendaftaranTesis.entity"; -import { PengajarKelas } from "./entities/pengajarKelas.entity"; import { Pengguna } from "./entities/pengguna.entity"; import { Topik } from "./entities/topik.entity"; -// import { Ruangan } from "./entities/ruangan.entity"; +import { DosenBimbingan } from "./entities/dosenBimbingan.entity"; +import { PendaftaranTesis } from "./entities/pendaftaranTesis.entity"; +import { PengujiSidsem } from "./entities/pengujiSidsem.entity"; +import { RegistrasiTesisModule } from "./registrasi-tesis/registrasi-tesis.module"; import { ConfigModule } from "@nestjs/config"; import { AlokasiRuanganModule } from "./alokasi-ruangan/alokasi-ruangan.module"; import { AlokasiTopikModule } from "./alokasi-topik/alokasi-topik.module"; import { AuthModule } from "./auth/auth.module"; import { BimbinganModule } from "./bimbingan/bimbingan.module"; -import { DashboardModule } from "./dashboard/dashboard.module"; -import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module"; +import { validate } from "./env.validation"; import { BerkasBimbingan } from "./entities/berkasBimbingan.entity"; -import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas.entity"; -import { BerkasTugas } from "./entities/berkasTugas.entity"; -import { Konfigurasi } from "./entities/konfigurasi.entity"; -import { MataKuliah } from "./entities/mataKuliah.entity"; import { PendaftaranSidsem } from "./entities/pendaftaranSidsem"; -import { PengujiSidsem } from "./entities/pengujiSidsem.entity"; -import { SubmisiTugas } from "./entities/submisiTugas.entity"; -import { Tugas } from "./entities/tugas.entity"; -import { validate } from "./env.validation"; -import { KelasModule } from "./kelas/kelas.module"; +import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module"; +import { PenggunaModule } from "./pengguna/pengguna.module"; import { KonfigurasiModule } from "./konfigurasi/konfigurasi.module"; -import { NilaiModule } from "./nilai/nilai.module"; -import { RegistrasiTesisModule } from "./registrasi-tesis/registrasi-tesis.module"; -import { SubmisiTugasModule } from "./submisi-tugas/submisi-tugas.module"; -import { TugasModule } from "./tugas/tugas.module"; +import { Konfigurasi } from "./entities/konfigurasi.entity"; +import { DashboardModule } from "./dashboard/dashboard.module"; @Module({ imports: [ @@ -53,20 +40,10 @@ import { TugasModule } from "./tugas/tugas.module"; Pengguna, PendaftaranSidsem, Topik, - AuditLog, DosenBimbingan, - Kelas, - MahasiswaKelas, - PengajarKelas, + Konfigurasi, PendaftaranTesis, - // Ruangan, - Tugas, PengujiSidsem, - Konfigurasi, - MataKuliah, - SubmisiTugas, - BerkasSubmisiTugas, - BerkasTugas, ], synchronize: true, }), @@ -75,13 +52,10 @@ import { TugasModule } from "./tugas/tugas.module"; AlokasiTopikModule, DashboardModule, BimbinganModule, - KonfigurasiModule, - KelasModule, - TugasModule, - SubmisiTugasModule, - NilaiModule, DosenBimbinganModule, AlokasiRuanganModule, + PenggunaModule, + KonfigurasiModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/bimbingan/bimbingan.controller.ts b/src/bimbingan/bimbingan.controller.ts index 99eab5d006d76622b2d8cf3a7699c41f93e4ead2..4f03874b94e456fe8a890c47cc05661fb890835d 100644 --- a/src/bimbingan/bimbingan.controller.ts +++ b/src/bimbingan/bimbingan.controller.ts @@ -45,9 +45,7 @@ export class BimbinganController { constructor(private readonly bimbinganService: BimbinganService) {} @ApiOkResponse({ type: GetByMahasiswaIdResDto }) - @ApiNotFoundResponse({ - description: "Tidak ada pendaftaran pada periode sekarang", - }) + @ApiNotFoundResponse({ description: "Tidak ada pendaftaran" }) @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS) @Get("/mahasiswa/:mahasiswaId") async getByMahasiswaId( @@ -61,9 +59,7 @@ export class BimbinganController { } @ApiOkResponse({ type: GetByMahasiswaIdResDto }) - @ApiNotFoundResponse({ - description: "Tidak ada pendaftaran pada periode sekarang", - }) + @ApiNotFoundResponse({ description: "Tidak ada pendaftaran" }) @Roles(RoleEnum.S2_MAHASISWA) @Get("/") async getOwnBimbingan( @@ -80,9 +76,7 @@ export class BimbinganController { description: "Waktu bimbingan lebih dari hari ini atau bimbingan berikutnya sebelum waktu bimbingan yang dimasukkan", }) - @ApiNotFoundResponse({ - description: "Tidak ada pendaftaran pada periode sekarang", - }) + @ApiNotFoundResponse({ description: "Tidak ada pendaftaran" }) @Roles(RoleEnum.S2_MAHASISWA) @ApiBody({ type: CreateBimbinganReqDto }) @Post("/") @@ -94,9 +88,7 @@ export class BimbinganController { } @ApiOkResponse({ type: UpdateStatusResDto }) - @ApiNotFoundResponse({ - description: "Bimbingan tidak ditemukan", - }) + @ApiNotFoundResponse({ description: "Bimbingan tidak ditemukan" }) @ApiForbiddenResponse({ description: "Anda tidak memiliki akses untuk mengubah status bimbingan", }) @@ -111,9 +103,7 @@ export class BimbinganController { } @ApiOkResponse({ type: GetByBimbinganIdResDto }) - @ApiNotFoundResponse({ - description: "Bimbingan tidak ditemukan", - }) + @ApiNotFoundResponse({ description: "Bimbingan tidak ditemukan" }) @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN) @Get("/:bimbinganId") async getByBimbinganId( diff --git a/src/bimbingan/bimbingan.dto.ts b/src/bimbingan/bimbingan.dto.ts index f3afcdac8aeeb0380633e5609bc7b35d8c864013..649f9c95366f56738a281689e0520c876ec8cef7 100644 --- a/src/bimbingan/bimbingan.dto.ts +++ b/src/bimbingan/bimbingan.dto.ts @@ -33,7 +33,6 @@ class PickedTopikBimbingan extends PickType(Topik, [ "judul", "deskripsi", "idPengaju", - "periode", ] as const) {} export class GetByMahasiswaIdResDto { diff --git a/src/bimbingan/bimbingan.module.ts b/src/bimbingan/bimbingan.module.ts index 30174c69ff5a33b4a5caa742095ef9f830c5eb8d..b77070fda6099172ac83112b4163af9a742f656d 100644 --- a/src/bimbingan/bimbingan.module.ts +++ b/src/bimbingan/bimbingan.module.ts @@ -4,22 +4,25 @@ import { BimbinganService } from "./bimbingan.service"; import { TypeOrmModule } from "@nestjs/typeorm"; 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.entity"; +import { PenggunaModule } from "src/pengguna/pengguna.module"; +import { PenggunaService } from "src/pengguna/pengguna.service"; +import { Pengguna } from "src/entities/pengguna.entity"; @Module({ imports: [ TypeOrmModule.forFeature([ Bimbingan, PendaftaranTesis, - Konfigurasi, DosenBimbingan, BerkasBimbingan, + Pengguna, ]), + PenggunaModule, ], controllers: [BimbinganController], - providers: [BimbinganService], + providers: [BimbinganService, PenggunaService], exports: [BimbinganService], }) export class BimbinganModule {} diff --git a/src/bimbingan/bimbingan.service.ts b/src/bimbingan/bimbingan.service.ts index 52c1ba28e98f9c71aad2d88e5ebeafb8d1cb1d40..825ce9109b5ac2c4be8f5e68aa0e600b2eda67e0 100644 --- a/src/bimbingan/bimbingan.service.ts +++ b/src/bimbingan/bimbingan.service.ts @@ -9,7 +9,6 @@ import * as dayjs from "dayjs"; import { AuthDto } from "src/auth/auth.dto"; import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity"; import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; -import { Konfigurasi } from "src/entities/konfigurasi.entity"; import { PendaftaranTesis, RegStatus, @@ -25,6 +24,7 @@ import { UpdateStatusResDto, } from "./bimbingan.dto"; import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity"; +import { PenggunaService } from "src/pengguna/pengguna.service"; @Injectable() export class BimbinganService { @@ -33,29 +33,23 @@ export class BimbinganService { private bimbinganRepository: Repository<Bimbingan>, @InjectRepository(PendaftaranTesis) private pendaftaranTesisRepository: Repository<PendaftaranTesis>, - @InjectRepository(Konfigurasi) - private konfigurasiRepository: Repository<Konfigurasi>, @InjectRepository(DosenBimbingan) private dosenBimbinganRepository: Repository<DosenBimbingan>, @InjectRepository(BerkasBimbingan) private berkasBimbinganRepository: Repository<BerkasBimbingan>, + private penggunaService: PenggunaService, ) {} async getByMahasiswaId( mahasiswaId: string, user: AuthDto, ): Promise<GetByMahasiswaIdResDto> { - const currentPeriode = await this.konfigurasiRepository.findOne({ - where: { key: process.env.KONF_PERIODE_KEY }, - }); + await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId); const pendaftaran = await this.pendaftaranTesisRepository.findOne({ where: { mahasiswa: { id: mahasiswaId }, status: RegStatus.APPROVED, - topik: { - periode: currentPeriode.value, - }, }, relations: { mahasiswa: true, @@ -65,9 +59,7 @@ export class BimbinganService { }); if (!pendaftaran) { - throw new NotFoundException( - "Tidak ada pendaftaran yang disetujui pada periode ini", - ); + throw new NotFoundException("Tidak ada pendaftaran yang disetujui"); } // Validate bimbingan data by its dosbim @@ -119,17 +111,10 @@ export class BimbinganService { createDto: CreateBimbinganReqDto, ): Promise<CreateBimbinganResDto> { // Check if user registered in bimbingan - const currentPeriode = await this.konfigurasiRepository.findOne({ - where: { key: process.env.KONF_PERIODE_KEY }, - }); - const pendaftaran = await this.pendaftaranTesisRepository.findOne({ where: { mahasiswa: { id: mahasiswaId }, status: RegStatus.APPROVED, - topik: { - periode: currentPeriode.value, - }, }, relations: { mahasiswa: true, @@ -139,9 +124,7 @@ export class BimbinganService { }); if (!pendaftaran) { - throw new NotFoundException( - "Tidak ada pendaftaran yang disetujui pada periode ini", - ); + throw new NotFoundException("Tidak ada pendaftaran yang disetujui"); } if (dayjs(createDto.waktuBimbingan).isAfter(dayjs(new Date()).endOf("d"))) @@ -180,7 +163,7 @@ export class BimbinganService { user: AuthDto, dto: UpdateStatusDto, ): Promise<UpdateStatusResDto> { - const bimbingan = await this.getByBimbinganId(user, dto.bimbinganId); + const bimbingan = await this.getByBimbinganId(user, dto.bimbinganId); // already check if mahasiswa is aktif await this.bimbinganRepository.update(bimbingan.id, { disahkan: dto.status, @@ -195,19 +178,13 @@ export class BimbinganService { user: AuthDto, bimbinganId: string, ): Promise<GetByBimbinganIdResDto> { - const currentPeriode = await this.konfigurasiRepository.findOne({ - where: { key: process.env.KONF_PERIODE_KEY }, - }); - const bimbinganQuery = this.bimbinganRepository .createQueryBuilder("bimbingan") .leftJoinAndSelect("bimbingan.pendaftaran", "pendaftaran") .leftJoinAndSelect("pendaftaran.dosenBimbingan", "dosenBimbingan") .leftJoinAndSelect("dosenBimbingan.dosen", "dosen") .leftJoinAndSelect("bimbingan.berkas", "berkas") - .leftJoin("pendaftaran.topik", "topik", "topik.periode = :periode", { - periode: currentPeriode.value, - }) + .leftJoin("pendaftaran.topik", "topik") .leftJoinAndSelect("pendaftaran.mahasiswa", "mahasiswa") .where("bimbingan.id = :id", { id: bimbinganId }); const bimbingan = await bimbinganQuery.getOne(); @@ -216,6 +193,10 @@ export class BimbinganService { throw new NotFoundException("Bimbingan tidak ditemukan"); } + if (!bimbingan.pendaftaran.mahasiswa.aktif) { + throw new BadRequestException("Bimbingan milik mahasiswa tidak aktif"); + } + if ( !user.roles.includes(RoleEnum.ADMIN) && !bimbingan.pendaftaran.dosenBimbingan diff --git a/src/dashboard/dashboard.controller.ts b/src/dashboard/dashboard.controller.ts index 31a3a4cc1dc88afe12ba4b05e16bc015fd2a41e9..bb16aebd0a8cb044554d293950f8056baa3b6c4b 100644 --- a/src/dashboard/dashboard.controller.ts +++ b/src/dashboard/dashboard.controller.ts @@ -8,7 +8,6 @@ import { AuthDto } from "src/auth/auth.dto"; import { Request } from "express"; import { DashboardDto, - DashboardMahasiswaResDto, GetDashboardDosbimQueryDto, JalurStatisticDto, } from "./dashboard.dto"; @@ -48,16 +47,4 @@ 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 8d279937d0616eb007400145e622316c5d4b9012..b11d1e6155273b64a4aaf73064f813eada47abd1 100644 --- a/src/dashboard/dashboard.dto.ts +++ b/src/dashboard/dashboard.dto.ts @@ -4,14 +4,9 @@ import { OmitType, PickType, } from "@nestjs/swagger"; -import { - JalurEnum, - PendaftaranTesis, -} from "../entities/pendaftaranTesis.entity"; +import { JalurEnum } 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"; @@ -23,35 +18,10 @@ class PickedMhsDashboard extends PickType(Pengguna, [ "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; @@ -77,38 +47,6 @@ 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 8f8f447d98f6ce5c3d40de228881aa244d364bba..4f310dc17f1ecef4ffba88fe01b46b60cfacddca 100644 --- a/src/dashboard/dashboard.module.ts +++ b/src/dashboard/dashboard.module.ts @@ -4,24 +4,11 @@ import { DashboardController } from "./dashboard.controller"; import { DashboardService } from "./dashboard.service"; 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, - Bimbingan, - PendaftaranSidsem, - DosenBimbingan, - ]), + TypeOrmModule.forFeature([PendaftaranTesis, Pengguna]), BimbinganModule, ], controllers: [DashboardController], diff --git a/src/dashboard/dashboard.service.ts b/src/dashboard/dashboard.service.ts index c2bfcb12e4e7c8bbcbfdfacf6b77a224c475a5b1..f168aa396a89aaf5a062d1c061baffa0ad988f45 100644 --- a/src/dashboard/dashboard.service.ts +++ b/src/dashboard/dashboard.service.ts @@ -6,19 +6,7 @@ import { RegStatus, } from "../entities/pendaftaranTesis.entity"; import { Pengguna } from "../entities/pengguna.entity"; -import { Konfigurasi } from "src/entities/konfigurasi.entity"; -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 { DashboardDto, JalurStatisticDto } from "./dashboard.dto"; import { BimbinganService } from "src/bimbingan/bimbingan.service"; @Injectable() @@ -28,14 +16,6 @@ export class DashboardService { private pendaftaranTesisRepository: Repository<PendaftaranTesis>, @InjectRepository(Pengguna) 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, ) {} @@ -49,14 +29,6 @@ export class DashboardService { dosenId: string, search?: string, ): Promise<DashboardDto[]> { - const currentPeriode = await this.konfigurasiRepository.findOne({ - where: { key: process.env.KONF_PERIODE_KEY }, - }); - - if (!currentPeriode) { - throw new BadRequestException("Periode belum dikonfigurasi"); - } - let pendaftaranTesisQuery = this.pendaftaranTesisRepository .createQueryBuilder("pendaftaranTesis") .leftJoinAndSelect("pendaftaranTesis.mahasiswa", "mahasiswa") @@ -69,10 +41,10 @@ export class DashboardService { dosenId, }, ) + .where("mahasiswa.aktif = true") .andWhere("pendaftaranTesis.status = :status", { status: RegStatus.APPROVED, - }) - .andWhere("topik.periode = :periode", { periode: currentPeriode.value }); + }); if (search) { pendaftaranTesisQuery = pendaftaranTesisQuery.andWhere( @@ -112,15 +84,9 @@ export class DashboardService { async getStatisticsByJalurPilihan( dosenId: string, ): Promise<JalurStatisticDto[]> { - const [currentPeriode, dosen] = await Promise.all([ - this.konfigurasiRepository.findOne({ - where: { key: process.env.KONF_PERIODE_KEY }, - }), - this.penggunaRepository.findOne({ - where: { id: dosenId }, - }), - ]); - + const dosen = await this.penggunaRepository.findOne({ + where: { id: dosenId }, + }); if (!dosen) { throw new BadRequestException("Dosen tidak ditemukan"); } @@ -129,9 +95,7 @@ export class DashboardService { .createQueryBuilder("pendaftaranTesis") .select("pendaftaranTesis.jalurPilihan", "jalurPilihan") .addSelect("COUNT(*)", "count") - .leftJoin("pendaftaranTesis.topik", "topik", "topik.periode = :periode", { - periode: currentPeriode.value, - }) + .leftJoin("pendaftaranTesis.mahasiswa", "mahasiswa") .innerJoin( "pendaftaranTesis.dosenBimbingan", "dosenBimbingan", @@ -140,6 +104,7 @@ export class DashboardService { dosenId, }, ) + .where("mahasiswa.aktif = true") .andWhere("pendaftaranTesis.status = :status", { status: RegStatus.APPROVED, }) @@ -148,126 +113,4 @@ 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.dto.ts b/src/dosen-bimbingan/dosen-bimbingan.dto.ts index 23c823e7389a759b7d532c02543e736d5bbaf0d4..16e493a95a99c7f62c02cf894b7386b3e5974383 100644 --- a/src/dosen-bimbingan/dosen-bimbingan.dto.ts +++ b/src/dosen-bimbingan/dosen-bimbingan.dto.ts @@ -14,4 +14,5 @@ export class GetDosbimResDto extends PickType(Pengguna, [ "id", "email", "nama", + "keahlian", ] as const) {} diff --git a/src/dosen-bimbingan/dosen-bimbingan.module.ts b/src/dosen-bimbingan/dosen-bimbingan.module.ts index 1929f62932c353150ab780e6a6fb785c560a9357..1721d9d65412532695c2c187b62f6aec9e248070 100644 --- a/src/dosen-bimbingan/dosen-bimbingan.module.ts +++ b/src/dosen-bimbingan/dosen-bimbingan.module.ts @@ -3,18 +3,11 @@ import { DosenBimbinganController } from "./dosen-bimbingan.controller"; import { DosenBimbinganService } from "./dosen-bimbingan.service"; import { AuthModule } from "src/auth/auth.module"; import { TypeOrmModule } from "@nestjs/typeorm"; -import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity"; -import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; import { Pengguna } from "src/entities/pengguna.entity"; -import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; import { CustomStrategy } from "src/middlewares/custom.strategy"; @Module({ - imports: [ - TypeOrmModule.forFeature([PendaftaranTesis, DosenBimbingan, Pengguna]), - AuthModule, - KonfigurasiModule, - ], + imports: [TypeOrmModule.forFeature([Pengguna]), AuthModule], controllers: [DosenBimbinganController], providers: [DosenBimbinganService, CustomStrategy], }) diff --git a/src/dosen-bimbingan/dosen-bimbingan.service.ts b/src/dosen-bimbingan/dosen-bimbingan.service.ts index 88f0f9d159f8f0e4949c06a4c5bbcb7ad9a9643a..9d9039a1dbc62780d044cb2fac6f09239e2ebc07 100644 --- a/src/dosen-bimbingan/dosen-bimbingan.service.ts +++ b/src/dosen-bimbingan/dosen-bimbingan.service.ts @@ -16,6 +16,7 @@ export class DosenBimbinganService { id: true, nama: true, email: true, + keahlian: true, }, where: { roles: ArrayContains([RoleEnum.S2_PEMBIMBING]), diff --git a/src/entities/auditLog.entity.ts b/src/entities/auditLog.entity.ts deleted file mode 100644 index c95d05a03fb11fb13d835bc1e6c64b22e107bff6..0000000000000000000000000000000000000000 --- a/src/entities/auditLog.entity.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; -import { Pengguna } from "./pengguna.entity"; - -@Entity() -export class AuditLog { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - pengguna: Pengguna; -} diff --git a/src/entities/berkasSubmisiTugas.entity.ts b/src/entities/berkasSubmisiTugas.entity.ts deleted file mode 100644 index e48d5e8d88f1449b767d3e88f1f9862e8e0f6560..0000000000000000000000000000000000000000 --- a/src/entities/berkasSubmisiTugas.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -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/berkasTugas.entity.ts b/src/entities/berkasTugas.entity.ts deleted file mode 100644 index c18bccfa9c839754ca8ed7bb0f5789f8841e3ff1..0000000000000000000000000000000000000000 --- a/src/entities/berkasTugas.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/kelas.entity.ts b/src/entities/kelas.entity.ts deleted file mode 100644 index deec327ed9a9028f7eb8ddd886826f35a3294c80..0000000000000000000000000000000000000000 --- a/src/entities/kelas.entity.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, -} from "typeorm"; -import { MataKuliah } from "./mataKuliah.entity"; -import { ApiProperty } from "@nestjs/swagger"; -import { PengajarKelas } from "./pengajarKelas.entity"; -import { MahasiswaKelas } from "./mahasiswaKelas.entity"; -import { - IsPositive, - IsString, - IsUUID, - Length, - MaxLength, -} from "@nestjs/class-validator"; -import { Tugas } from "./tugas.entity"; - -@Entity() -export class Kelas { - @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) - @IsUUID() - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column({ type: "smallint" }) - @ApiProperty({ example: 1 }) - @IsPositive() - nomor: number; - - @ApiProperty() - @IsString() - @Column({ type: "text" }) - periode: string; - - @ApiProperty({ type: MataKuliah }) - @ManyToOne(() => MataKuliah, (mataKuliah) => mataKuliah.kode) - @JoinColumn({ name: "mataKuliahKode" }) - mataKuliah: MataKuliah; - - @ApiProperty({ minLength: 6, maxLength: 6, example: "IF4031" }) - @IsString() - @Length(6) - @Column({ nullable: true }) - mataKuliahKode: string; - - @ApiProperty({ example: "bg-blue-600/20" }) - @IsString() - @MaxLength(24) - @Column({ type: "varchar", length: 24 }) - warna: string; - - @OneToMany(() => PengajarKelas, (pengajar) => pengajar.kelas) - pengajar: PengajarKelas[]; - - @OneToMany(() => MahasiswaKelas, (mahasiswa) => mahasiswa.kelas) - mahasiswa: MahasiswaKelas[]; - - @OneToMany(() => Tugas, (tugas) => tugas.kelas) - tugas: Tugas[]; -} diff --git a/src/entities/konfigurasi.entity.ts b/src/entities/konfigurasi.entity.ts index 538a9fa944cad11819c8323dafa99decc9801908..26ee3cee556d63ef6ae248c1f957afe0c56254f8 100644 --- a/src/entities/konfigurasi.entity.ts +++ b/src/entities/konfigurasi.entity.ts @@ -1,10 +1,27 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, IsString } from "class-validator"; import { Column, Entity, PrimaryColumn } from "typeorm"; +export enum KonfigurasiKeyEnum { + AWAL_PENDAFTARAN = "AWAL_PENDAFTARAN", + AKHIR_PENDAFTARAN = "AKHIR_PENDAFTARAN", + AWAL_SEMPRO = "AWAL_SEMPRO", + AKHIR_SEMPRO = "AKHIR_SEMPRO", + AWAL_SEM_TESIS = "AWAL_SEM_TESIS", + AKHIR_SEM_TESIS = "AKHIR_SEM_TESIS", + AWAL_SIDANG = "AWAL_SIDANG", + AKHIR_SIDANG = "AKHIR_SIDANG", +} + @Entity() export class Konfigurasi { - @PrimaryColumn({ type: "text" }) - key: string; + @ApiProperty({ enum: KonfigurasiKeyEnum }) + @IsEnum(KonfigurasiKeyEnum) + @PrimaryColumn({ type: "enum", enum: KonfigurasiKeyEnum }) + key: KonfigurasiKeyEnum; + @ApiProperty() + @IsString() @Column({ type: "text" }) value: string; } diff --git a/src/entities/mahasiswaKelas.entity.ts b/src/entities/mahasiswaKelas.entity.ts deleted file mode 100644 index 0753aa6f75f865913b7e42209248310948dc67c2..0000000000000000000000000000000000000000 --- a/src/entities/mahasiswaKelas.entity.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from "typeorm"; -import { Kelas } from "./kelas.entity"; -import { Pengguna } from "./pengguna.entity"; - -@Entity() -export class MahasiswaKelas { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Kelas, (kelas) => kelas.id) - @JoinColumn({ name: "kelasId" }) - kelas: Kelas; - - @Column() - kelasId: string; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - @JoinColumn({ name: "mahasiswaId" }) - mahasiswa: Pengguna; - - @Column() - mahasiswaId: string; - - @Column({ type: "real", nullable: true }) - nilaiAkhir?: number; -} diff --git a/src/entities/mataKuliah.entity.ts b/src/entities/mataKuliah.entity.ts deleted file mode 100644 index 9e1fed5d807a176b89288144623ec72575ef5e33..0000000000000000000000000000000000000000 --- a/src/entities/mataKuliah.entity.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsString, Length, MaxLength } from "class-validator"; -import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm"; -import { Kelas } from "./kelas.entity"; - -@Entity() -export class MataKuliah { - @ApiProperty({ minLength: 6, maxLength: 6, example: "IF4031" }) - @IsString() - @Length(6) - @PrimaryColumn({ type: "varchar", length: 6 }) - kode: string; - - @ApiProperty({ maxLength: 256 }) - @IsString() - @MaxLength(256) - @Column({ type: "varchar", length: 256 }) - nama: string; - - @OneToMany(() => Kelas, (kelas) => kelas.mataKuliah) - kelas: Kelas[]; -} diff --git a/src/entities/pendaftaranTesis.entity.ts b/src/entities/pendaftaranTesis.entity.ts index 637a05e71e7be84615568bcaef45f0adc2d14e2c..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, @@ -56,13 +57,17 @@ export class PendaftaranTesis { status: RegStatus; @ApiProperty({ type: Topik }) - @ManyToOne(() => Topik, (topik) => topik.id) + @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/pengajarKelas.entity.ts b/src/entities/pengajarKelas.entity.ts deleted file mode 100644 index 61a314c4f21548b3b48e9aeb510b2d195d9a3662..0000000000000000000000000000000000000000 --- a/src/entities/pengajarKelas.entity.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - PrimaryGeneratedColumn, -} from "typeorm"; -import { Kelas } from "./kelas.entity"; -import { Pengguna } from "./pengguna.entity"; - -@Entity() -export class PengajarKelas { - @PrimaryGeneratedColumn("uuid") - id: string; - - @ManyToOne(() => Kelas, (kelas) => kelas.id) - @JoinColumn({ name: "kelasId" }) - kelas: Kelas; - - @Column() - kelasId: string; - - @ManyToOne(() => Pengguna, (pengguna) => pengguna.id) - @JoinColumn({ name: "pengajarId" }) - pengajar: Pengguna; - - @Column() - pengajarId: string; -} diff --git a/src/entities/pengguna.entity.ts b/src/entities/pengguna.entity.ts index c28a606ea4031436e36fc7958c0756a78f898cbe..c6011ee1dc0ec6adb74010bc2a21bae668198d23 100644 --- a/src/entities/pengguna.entity.ts +++ b/src/entities/pengguna.entity.ts @@ -1,4 +1,3 @@ -import { IsString } from "@nestjs/class-validator"; import { ApiHideProperty, ApiProperty, @@ -6,21 +5,18 @@ import { } from "@nestjs/swagger"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { PendaftaranTesis } from "./pendaftaranTesis.entity"; -import { SubmisiTugas } from "./submisiTugas.entity"; +import { DosenBimbingan } from "./dosenBimbingan.entity"; export enum RoleEnum { ADMIN = "ADMIN", - TU = "TU", S2_MAHASISWA = "S2_MAHASISWA", S2_PEMBIMBING = "S2_PEMBIMBING", S2_PENGUJI = "S2_PENGUJI", S2_TIM_TESIS = "S2_TIM_TESIS", - S2_KULIAH = "S2_KULIAH", S1_MAHASISWA = "S1_MAHASISWA", S1_PEMBIMBING = "S1_PEMBIMBING", S1_PENGUJI = "S1_PENGUJI", S1_TIM_TA = "S1_TIM_TA", - S1_KULIAH = "S1_KULIAH", } @Entity() @@ -55,13 +51,36 @@ export class Pengguna { roles: RoleEnum[]; @ApiPropertyOptional() - @IsString() @Column({ type: "text", nullable: true }) - kontak: string; + kontakWhatsApp: string; + + @ApiPropertyOptional() + @Column({ type: "text", nullable: true }) + kontakMsTeams: string; + + @ApiPropertyOptional() + @Column({ type: "text", nullable: true }) + kontakEmail: string; + + @ApiPropertyOptional() + @Column({ type: "text", nullable: true }) + kontakTelp: string; + + @ApiPropertyOptional() + @Column({ type: "text", nullable: true }) + kontakLainnya: string; + + @ApiPropertyOptional() + @Column({ type: "text", nullable: true }) + keahlian: string; + + @ApiHideProperty() + @Column({ type: "boolean", default: true }) + aktif: boolean; @OneToMany(() => PendaftaranTesis, (pendaftaran) => pendaftaran.mahasiswa) pendaftaranTesis: PendaftaranTesis[]; - @OneToMany(() => SubmisiTugas, (submisiTugas) => submisiTugas.mahasiswa) - submisiTugas: SubmisiTugas[]; + @OneToMany(() => DosenBimbingan, (dosen) => dosen.dosen) + dosenBimbingan: DosenBimbingan[]; } diff --git a/src/entities/ruangan.entity.ts b/src/entities/ruangan.entity.ts deleted file mode 100644 index 86dfaf804683751d50f397df3a55f00488dc0bfa..0000000000000000000000000000000000000000 --- a/src/entities/ruangan.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -// import { ApiProperty } from "@nestjs/swagger"; -// import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; - -// @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/submisiTugas.entity.ts b/src/entities/submisiTugas.entity.ts deleted file mode 100644 index be380d37d6c0b4dadf641f794735a343990e13b5..0000000000000000000000000000000000000000 --- a/src/entities/submisiTugas.entity.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, -} from "typeorm"; -import { Pengguna } from "./pengguna.entity"; -import { Tugas } from "./tugas.entity"; -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", - nullable: true, - }) - 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/topik.entity.ts b/src/entities/topik.entity.ts index 127540cc0619fa7863227bf11029f78160287a84..17eb3bf158b175d37e93a9a0684a16f943bc5ba1 100644 --- a/src/entities/topik.entity.ts +++ b/src/entities/topik.entity.ts @@ -1,4 +1,3 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Column, Entity, @@ -7,6 +6,7 @@ import { PrimaryGeneratedColumn, } from "typeorm"; import { Pengguna } from "./pengguna.entity"; +import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; @Entity() export class Topik { @@ -31,7 +31,7 @@ export class Topik { @Column({ nullable: true }) idPengaju: string; - @ApiProperty() - @Column({ type: "text" }) - periode: string; + @ApiHideProperty() + @Column({ type: "boolean", default: true }) + aktif: boolean; } diff --git a/src/entities/tugas.entity.ts b/src/entities/tugas.entity.ts deleted file mode 100644 index 2424b87973fd01fe854e40ca50079dbc40e0f4ee..0000000000000000000000000000000000000000 --- a/src/entities/tugas.entity.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { - Column, - Entity, - JoinColumn, - ManyToOne, - OneToMany, - PrimaryGeneratedColumn, -} from "typeorm"; -import { Kelas } from "./kelas.entity"; -import { Pengguna } from "./pengguna.entity"; -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; - - @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/env.validation.ts b/src/env.validation.ts index e14a7cc35fd80b3e9bd3c0dbb3fbcbee7c31e157..1d5e6d037f02d9f54e2c3c7a0af08adc63785ff6 100644 --- a/src/env.validation.ts +++ b/src/env.validation.ts @@ -34,36 +34,6 @@ class EnvironmentVariables { @IsUrl({ require_tld: false }) FE_URL: string; - @IsString() - KONF_PERIODE_KEY: string; - - @IsString() - KONF_MIN_BIMBINGAN_KEY: string; - - @IsString() - KONF_AWAL_PENDAFTARAN_KEY: string; - - @IsString() - KONF_AKHIR_PENDAFTARAN_KEY: string; - - @IsString() - KONF_AWAL_SEMPRO_KEY: string; - - @IsString() - KONF_AKHIR_SEMPRO_KEY: string; - - @IsString() - KONF_AWAL_SEM_TESIS_KEY: string; - - @IsString() - KONF_AKHIR_SEM_TESIS_KEY: string; - - @IsString() - KONF_AWAL_SIDANG_KEY: string; - - @IsString() - KONF_AKHIR_SIDANG_KEY: string; - @IsString() COOKIE_NAME: string; } diff --git a/src/helper/roles.ts b/src/helper/roles.ts new file mode 100644 index 0000000000000000000000000000000000000000..690f29dd520df4c348a4520cfc75aee48540dca5 --- /dev/null +++ b/src/helper/roles.ts @@ -0,0 +1,7 @@ +import { RoleEnum } from "src/entities/pengguna.entity"; + +export const HIGH_AUTHORITY_ROLES = [RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS]; + +export function isHighAuthority(roles: RoleEnum[]) { + return roles.some((role) => HIGH_AUTHORITY_ROLES.includes(role)); +} diff --git a/src/helper/validation.ts b/src/helper/validation.ts deleted file mode 100644 index cfc65eb722c4859bd1569749bccb0e8785721b77..0000000000000000000000000000000000000000 --- a/src/helper/validation.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NotFoundException } from "@nestjs/common"; -import { validate as uuidValidate } from "uuid"; - -interface ID { - id: string; - object: string; -} - -export function validateId(items: ID[]) { - for (const item of items) { - const isValidUUID = uuidValidate(item.id); - - if (!isValidUUID) { - throw new NotFoundException(`${item.object} not found.`); - } - } -} diff --git a/src/kelas/kelas.constant.ts b/src/kelas/kelas.constant.ts deleted file mode 100644 index 050cbbca5914f9eb24c9e646e62e2629d047a51b..0000000000000000000000000000000000000000 --- a/src/kelas/kelas.constant.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const CARD_COLORS = [ - "bg-blue-600/20", - "bg-yellow-600/20", - "bg-green-600/20", - "bg-red-600/20", - "bg-purple-600/20", - "bg-pink-600/20", - "bg-indigo-600/20", - "bg-cyan-600/20", - "bg-amber-600/20", - "bg-lime-600/20", - "bg-emerald-600/20", - "bg-teal-600/20", - "bg-cyan-600/20", - "bg-violet-600/20", - "bg-fuchsia-600/20", - "bg-rose-600/20", - "bg-sky-600/20", - "bg-cyan-600/20", -]; diff --git a/src/kelas/kelas.controller.ts b/src/kelas/kelas.controller.ts deleted file mode 100644 index ae942b029bd3415ece79a2d5df0b3b4ad5befd3a..0000000000000000000000000000000000000000 --- a/src/kelas/kelas.controller.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { - Body, - Controller, - Delete, - ForbiddenException, - Get, - Param, - Post, - Put, - Query, - Req, - UseGuards, -} from "@nestjs/common"; -import { - ByIdKelasDto, - CreateKelasDto, - DeleteKelasDto, - GetKelasDetailRespDto, - GetKelasQueryDto, - GetKelasRespDto, - GetNextNomorResDto, - IdKelasResDto, - KodeRespDto, - UpdateKelasDto, -} from "./kelas.dto"; -import { Request } from "express"; -import { AuthDto } from "src/auth/auth.dto"; -import { RoleEnum } from "src/entities/pengguna.entity"; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiCookieAuth, - ApiCreatedResponse, - 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.entity"; -import { Kelas } from "src/entities/kelas.entity"; - -@ApiTags("Kelas") -@ApiBearerAuth() -@ApiCookieAuth() -@Controller("kelas") -@UseGuards(CustomAuthGuard, RolesGuard) -export class KelasController { - constructor(private readonly kelasServ: KelasService) {} - - @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, - RoleEnum.S2_TIM_TESIS, - RoleEnum.ADMIN, - ) - @Get() - async getListKelas(@Query() query: GetKelasQueryDto, @Req() req: Request) { - let idMahasiswa = undefined; - let idPengajar = undefined; - - const { id, roles } = req.user as AuthDto; - - if (!roles.includes(query.view)) { - throw new ForbiddenException(); - } - - if (query.view === RoleEnum.S2_KULIAH) { - idPengajar = id; - } - - if (query.view === RoleEnum.S2_MAHASISWA) { - idMahasiswa = id; - } - - return await this.kelasServ.getListKelas( - idMahasiswa, - idPengajar, - query.kodeMatkul, - query.search, - ); - } - - @ApiOkResponse({ type: MataKuliah, isArray: true }) - @Get("/mata-kuliah") - async getAllMatkul() { - return await this.kelasServ.getAllMatkul(); - } - - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) - @ApiOkResponse({ type: GetNextNomorResDto }) - @Get("/next-nomor/:kodeMatkul") - async getNextNomor( - @Param("kodeMatkul") kodeMatkul: string, - ): Promise<GetNextNomorResDto> { - const nomor = await this.kelasServ.getNextNomorKelas(kodeMatkul); - - return { - nomor, - }; - } - - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) - @ApiOkResponse({ type: IdKelasResDto }) - @ApiBadRequestResponse({ - description: "Nomor kelas sudah ada", - }) - @ApiInternalServerErrorResponse({ - description: "Gagal membuat kelas", - }) - @Post() - async createKelas(@Body() body: CreateKelasDto): Promise<IdKelasResDto> { - return await this.kelasServ.create(body); - } - - // TODO: restrict payload - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) - @ApiOkResponse({ type: IdKelasResDto }) - @ApiNotFoundResponse({ - description: "Kelas dengan id (dan nomor) yang terkait tidak ditemukan", - }) - @ApiBadRequestResponse({ - description: - "(Saat pembuatan kelas) nomor kelas sudah ada atau mataKuliahKode tidak ada", - }) - @ApiInternalServerErrorResponse({ - description: "Gagal memperbarui kelas atau gagal membuat kelas", - }) - @Put() - async updateKelas(@Body() body: UpdateKelasDto): Promise<IdKelasResDto> { - return await this.kelasServ.updateOrCreate(body); - } - - @ApiCreatedResponse({ type: KodeRespDto }) - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) - @Post("mata-kuliah") - async createMataKuliah(@Body() body: MataKuliah) { - return await this.kelasServ.createMatkul(body); - } - - @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) - @ApiOkResponse({ type: Kelas }) - @ApiNotFoundResponse({ description: "Kelas tidak ditemukan" }) - @ApiInternalServerErrorResponse({ description: "Gagal menghapus kelas" }) - @Delete() - 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 deleted file mode 100644 index 352ae36ea1a1d0c0e45ee7e9eb6eb778cd460439..0000000000000000000000000000000000000000 --- a/src/kelas/kelas.dto.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { IsEnum, IsOptional, IsPositive } from "@nestjs/class-validator"; -import { - ApiProperty, - PickType, - PartialType, - ApiPropertyOptional, -} from "@nestjs/swagger"; -import { Kelas } from "src/entities/kelas.entity"; -import { MataKuliah } from "src/entities/mataKuliah.entity"; -import { Pengguna, RoleEnum } from "src/entities/pengguna.entity"; - -export class CreateKelasDto extends PickType(Kelas, [ - "mataKuliahKode", -] as const) { - @ApiPropertyOptional({ example: 1 }) - @IsOptional() - @IsPositive() - nomor: number; -} - -export class UpdateKelasDto extends PartialType(Kelas) {} - -export class IdKelasResDto extends PickType(Kelas, ["id"] as const) {} - -export class DeleteKelasDto extends PickType(Kelas, [ - "mataKuliahKode", - "nomor", -] as const) {} - -export class GetKelasQueryDto { - @IsEnum([RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA, RoleEnum.S2_TIM_TESIS]) - @ApiProperty({ - enum: [RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA, RoleEnum.S2_TIM_TESIS], - }) - view: RoleEnum.S2_KULIAH | RoleEnum.S2_MAHASISWA | RoleEnum.S2_TIM_TESIS; - - @ApiPropertyOptional({ example: "IF3270" }) - @IsOptional() - kodeMatkul: string; - - @ApiPropertyOptional({ example: "Intelegensi Buatan" }) - @IsOptional() - search: string; -} - -export class ByIdKelasDto extends PickType(Kelas, ["id"] as const) {} - -export class GetKelasRespDto { - @ApiProperty() - id: string; - - @ApiProperty({ example: "K02" }) - nomor: 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) {} - -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 deleted file mode 100644 index 38dfa27f8690f2608b1450c134536f7d3d920651..0000000000000000000000000000000000000000 --- a/src/kelas/kelas.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from "@nestjs/common"; -import { KelasService } from "./kelas.service"; -import { TypeOrmModule } from "@nestjs/typeorm"; -import { Kelas } from "src/entities/kelas.entity"; -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.entity"; - -@Module({ - imports: [ - TypeOrmModule.forFeature([Kelas, MataKuliah]), - AuthModule, - KonfigurasiModule, - ], - controllers: [KelasController], - providers: [KelasService, CustomStrategy], -}) -export class KelasModule {} diff --git a/src/kelas/kelas.service.ts b/src/kelas/kelas.service.ts deleted file mode 100644 index fa62a4707fe36efeca92ef76b8074dd392e7a92b..0000000000000000000000000000000000000000 --- a/src/kelas/kelas.service.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { - BadRequestException, - Injectable, - InternalServerErrorException, - NotFoundException, -} from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Kelas } from "src/entities/kelas.entity"; -import { Brackets, Repository } from "typeorm"; -import { - CreateKelasDto, - DeleteKelasDto, - GetKelasDetailRespDto, - GetKelasRespDto, - IdKelasResDto, - UpdateKelasDto, -} from "./kelas.dto"; -import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; -import { MataKuliah } from "src/entities/mataKuliah.entity"; -import { CARD_COLORS } from "./kelas.constant"; - -@Injectable() -export class KelasService { - constructor( - @InjectRepository(Kelas) - private kelasRepo: Repository<Kelas>, - @InjectRepository(MataKuliah) - private mataKuliahRepo: Repository<MataKuliah>, - private konfService: KonfigurasiService, - ) {} - async getListKelas( - idMahasiswa?: string, - idPengajar?: string, - kodeMatkul?: string, - search?: 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", - ]) - .orderBy("mataKuliah.kode", "ASC") - .addOrderBy("kelas.nomor", "ASC") - .where("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, - }); - } - - if (kodeMatkul) { - baseQuery = baseQuery.andWhere("mataKuliah.kode = :kodeMatkul", { - kodeMatkul, - }); - } - - if (search) { - baseQuery = baseQuery.andWhere( - new Brackets((qb) => { - qb.where("mataKuliah.kode ILIKE :search", { - search: `%${search}%`, - }).orWhere("mataKuliah.nama ILIKE :search", { - search: `%${search}%`, - }); - }), - ); - } - - const result = await baseQuery - .groupBy("kelas.id, mataKuliah.kode") - .getRawMany(); - - const mapped: GetKelasRespDto[] = result.map((r) => ({ - id: r.id, - nomor: "K" + `${r.nomor}`.padStart(2, "0"), - 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 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, - }; - - 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 - .createQueryBuilder("kelas") - .where("kelas.nomor = :nomor", { nomor }) - .andWhere("kelas.mataKuliahKode = :mataKuliahKode", { - mataKuliahKode: createDto.mataKuliahKode, - }) - .andWhere("kelas.periode = :periode", { periode: currPeriod }); - - const checkClass = await checkClassQueary.getOne(); - - if (checkClass) { - throw new BadRequestException(`Kelas dengan nomor ${nomor} sudah ada`); - } - } else { - nomor = await this.getNextNomorKelas(createDto.mataKuliahKode); - } - - const colorIdx = Math.floor(Math.random() * CARD_COLORS.length); - const kelas = this.kelasRepo.create({ - ...createDto, - nomor, - periode: currPeriod, - warna: CARD_COLORS[colorIdx], - }); - - try { - await this.kelasRepo.save(kelas); - } catch { - throw new InternalServerErrorException("Gagal membuat kelas baru"); - } - - return { - id: kelas.id, - }; - } - - async createMatkul(createDto: MataKuliah) { - await this.mataKuliahRepo.insert(createDto); - - return { kode: createDto.kode }; - } - - async updateOrCreate(dto: UpdateKelasDto): Promise<IdKelasResDto> { - const currPeriod = await this.konfService.getPeriodeOrFail(); - - if (!dto.id) { - // Create kelas - if (!dto.mataKuliahKode) { - throw new BadRequestException("Kode mata kuliah tidak boleh kosong"); - } - - return await this.create({ - mataKuliahKode: dto.mataKuliahKode, - nomor: dto.nomor, - }); - } else { - // Update kelas - const kelasQuery = this.kelasRepo - .createQueryBuilder("kelas") - .where("kelas.id = :id", { id: dto.id }) - .andWhere("kelas.periode = :periode", { periode: currPeriod }); - - if (dto.nomor) { - kelasQuery.andWhere("kelas.nomor = :nomor", { nomor: dto.nomor }); - } - - const kelas = await kelasQuery.getOne(); - - if (!kelas) { - throw new NotFoundException("Kelas tidak ditemukan"); - } - - try { - await this.kelasRepo.update(kelas.id, dto); - } catch { - throw new InternalServerErrorException("Gagal memperbarui kelas"); - } - - return { - id: kelas.id, - }; - } - } - - async delete(dto: DeleteKelasDto): Promise<Kelas> { - const currPeriod = await this.konfService.getPeriodeOrFail(); - - const kelasQuery = this.kelasRepo - .createQueryBuilder("kelas") - .where("kelas.nomor = :nomor", { nomor: dto.nomor }) - .andWhere("kelas.mataKuliahKode = :mataKuliahKode", { - mataKuliahKode: dto.mataKuliahKode, - }) - .andWhere("kelas.periode = :periode", { periode: currPeriod }); - - const kelas = await kelasQuery.getOne(); - - if (!kelas) { - throw new BadRequestException("Kelas tidak ditemukan"); - } - - try { - await this.kelasRepo.delete(kelas.id); - } catch { - throw new InternalServerErrorException("Gagal menghapus kelas"); - } - - return kelas; - } - - async getAllMatkul(): Promise<MataKuliah[]> { - return await this.mataKuliahRepo.find(); - } - - async getNextNomorKelas(kodeMatkul: string): Promise<number> { - const currPeriod = await this.konfService.getPeriodeOrFail(); - - const maxClass = await this.kelasRepo.findOne({ - where: { - mataKuliahKode: kodeMatkul, - periode: currPeriod, - }, - order: { - nomor: "DESC", - }, - }); - - return maxClass ? maxClass.nomor + 1 : 1; - } -} diff --git a/src/konfigurasi/konfigurasi.controller.ts b/src/konfigurasi/konfigurasi.controller.ts index 8f5b99b300caba5f8c9d09d5b032feaa08f613ab..caf6a97a7950a431351f9d130913c7be03783420 100644 --- a/src/konfigurasi/konfigurasi.controller.ts +++ b/src/konfigurasi/konfigurasi.controller.ts @@ -26,8 +26,7 @@ export class KonfigurasiController { async updateKonfigurasi( @Body() data: KonfigurasiArrDto, ): Promise<UpdateKonfigurasiResDto> { - await this.konfigurasiService.updateKonfigurasi(data); - return { message: "success" }; + return await this.konfigurasiService.updateKonfigurasi(data); } @ApiOkResponse({ type: KonfigurasiArrDto }) diff --git a/src/konfigurasi/konfigurasi.dto.ts b/src/konfigurasi/konfigurasi.dto.ts index 0de2f06b9f82d10df61c2c475ab1e56a26d3342a..1ebdd10da2f421f1aabdd53afe02b26466269032 100644 --- a/src/konfigurasi/konfigurasi.dto.ts +++ b/src/konfigurasi/konfigurasi.dto.ts @@ -1,24 +1,19 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsString, ValidateNested } from "class-validator"; +import { ValidateNested } from "class-validator"; +import { + Konfigurasi, + KonfigurasiKeyEnum, +} from "src/entities/konfigurasi.entity"; -export class KonfigurasiDto { - @ApiProperty() - @IsString() - key: string; - - @ApiProperty() - @IsString() - value: string; -} export class KonfigurasiArrDto { - @ApiProperty({ type: [KonfigurasiDto] }) + @ApiProperty({ type: [Konfigurasi] }) @ValidateNested({ each: true }) - @Type(() => KonfigurasiDto) - data: KonfigurasiDto[]; + @Type(() => Konfigurasi) + data: Konfigurasi[]; } export class UpdateKonfigurasiResDto { - @ApiProperty() - message: string; + @ApiProperty({ enum: KonfigurasiKeyEnum, isArray: true }) + keys: KonfigurasiKeyEnum[]; } diff --git a/src/konfigurasi/konfigurasi.service.ts b/src/konfigurasi/konfigurasi.service.ts index a8e7edaccbcafeeab8aa59f9039ce69ae88a21c2..6702c21184cc55ec1124dc165b0abd80abf4ad43 100644 --- a/src/konfigurasi/konfigurasi.service.ts +++ b/src/konfigurasi/konfigurasi.service.ts @@ -1,8 +1,11 @@ -import { BadRequestException, Injectable } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { Konfigurasi } from "src/entities/konfigurasi.entity"; +import { + Konfigurasi, + KonfigurasiKeyEnum, +} from "src/entities/konfigurasi.entity"; import { Repository } from "typeorm"; -import { KonfigurasiArrDto } from "./konfigurasi.dto"; +import { KonfigurasiArrDto, UpdateKonfigurasiResDto } from "./konfigurasi.dto"; @Injectable() export class KonfigurasiService { @@ -11,16 +14,25 @@ export class KonfigurasiService { private konfigurasiRepository: Repository<Konfigurasi>, ) {} - async updateKonfigurasi({ data }: KonfigurasiArrDto) { - return await this.konfigurasiRepository.upsert(data, ["key"]); + async updateKonfigurasi({ + data, + }: KonfigurasiArrDto): Promise<UpdateKonfigurasiResDto> { + await this.konfigurasiRepository.upsert(data, ["key"]); + const res = { + keys: data.map((d) => d.key), + }; + return res; } async getKonfigurasi(): Promise<KonfigurasiArrDto> { const data = await this.konfigurasiRepository.find(); + return { data }; } - async getKonfigurasiByKey(key: string): Promise<string | undefined> { + async getKonfigurasiByKey( + key: KonfigurasiKeyEnum, + ): Promise<string | undefined> { const data = await this.konfigurasiRepository.findOne({ where: { key, @@ -29,16 +41,4 @@ 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 fdce89e3372e4a1ea2d1a7bf5006ee2054c3f487..8f39d1c33675cd4f871fe6271f9bfbd301979f3a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,12 +25,8 @@ async function bootstrap() { .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/nilai/nilai.controller.ts b/src/nilai/nilai.controller.ts deleted file mode 100644 index 6bb1cb659a4ad0347e37c69df0ad85750da2d1ad..0000000000000000000000000000000000000000 --- a/src/nilai/nilai.controller.ts +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 1b933d07468212dbfacd8fc41d441227b5084418..0000000000000000000000000000000000000000 --- a/src/nilai/nilai.dto.ts +++ /dev/null @@ -1,78 +0,0 @@ -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 deleted file mode 100644 index f9aca923b411670865c235c946b28d698a099a52..0000000000000000000000000000000000000000 --- a/src/nilai/nilai.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index 81ebba4d7151fc7b42f495d542af94057fd4f66e..0000000000000000000000000000000000000000 --- a/src/nilai/nilai.service.ts +++ /dev/null @@ -1,98 +0,0 @@ -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/pengguna/pengguna.module.ts b/src/pengguna/pengguna.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e5276cbb4849fcc7453b7b2618834fcd3a2f3c7 --- /dev/null +++ b/src/pengguna/pengguna.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { PenggunaService } from "./pengguna.service"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { Pengguna } from "src/entities/pengguna.entity"; + +@Module({ + imports: [TypeOrmModule.forFeature([Pengguna])], + providers: [PenggunaService], + exports: [PenggunaService], +}) +export class PenggunaModule {} diff --git a/src/pengguna/pengguna.service.ts b/src/pengguna/pengguna.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..28281715431f789c616d7213ec8a8cea18b7c80c --- /dev/null +++ b/src/pengguna/pengguna.service.ts @@ -0,0 +1,24 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Pengguna, RoleEnum } from "src/entities/pengguna.entity"; +import { ArrayContains, Repository } from "typeorm"; + +@Injectable() +export class PenggunaService { + constructor( + @InjectRepository(Pengguna) + private penggunaRepo: Repository<Pengguna>, + ) {} + + async isMahasiswaAktifOrFail(id: string) { + const mhs = await this.penggunaRepo.findOneBy({ + id, + aktif: true, + roles: ArrayContains([RoleEnum.S2_MAHASISWA]), + }); + + if (!mhs) { + throw new NotFoundException("Mahasiswa aktif tidak ditemukan"); + } + } +} diff --git a/src/registrasi-tesis/registrasi-tesis.controller.ts b/src/registrasi-tesis/registrasi-tesis.controller.ts index e41920cdafcd7e91a4378bd0224bfdbcb9605ec7..0ac28aece930cb6f6c9ea7ec037540d9ec3d5f1e 100644 --- a/src/registrasi-tesis/registrasi-tesis.controller.ts +++ b/src/registrasi-tesis/registrasi-tesis.controller.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Body, Controller, ForbiddenException, @@ -12,8 +11,11 @@ import { UseGuards, } from "@nestjs/common"; import { + ApiBadRequestResponse, ApiBearerAuth, ApiCookieAuth, + ApiCreatedResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, @@ -21,7 +23,6 @@ import { import { Request } from "express"; import { AuthDto } from "src/auth/auth.dto"; import { RoleEnum } from "src/entities/pengguna.entity"; -import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; import { CustomAuthGuard } from "src/middlewares/custom-auth.guard"; import { Roles } from "src/middlewares/roles.decorator"; import { RolesGuard } from "src/middlewares/roles.guard"; @@ -48,20 +49,24 @@ import { RegistrasiTesisService } from "./registrasi-tesis.service"; export class RegistrasiTesisController { constructor( private readonly registrasiTesisService: RegistrasiTesisService, - private readonly konfService: KonfigurasiService, ) {} @ApiOperation({ - summary: - "Create new registration. Roles: S2_MAHASISWA, ADMIN, S2_TIM_TESIS", + summary: "Create new registration. Roles: S2_MAHASISWA", + }) + @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) + @Roles(RoleEnum.S2_MAHASISWA) @Post() async createTopicRegistration( @Body() topicRegistrationDto: RegDto, @Req() req: Request, - ) { + ): Promise<IdDto> { const { id } = req.user as AuthDto; return this.registrasiTesisService.createTopicRegistration( @@ -91,17 +96,8 @@ export class RegistrasiTesisController { } } - 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, ); @@ -130,17 +126,8 @@ export class RegistrasiTesisController { 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, ); @@ -163,12 +150,7 @@ export class RegistrasiTesisController { throw new ForbiddenException(); } - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - return this.registrasiTesisService.getRegsStatistics({ - periode, idPenerima: query.view == RoleEnum.S2_PEMBIMBING ? idPenerima : undefined, }); } @@ -194,20 +176,11 @@ export class RegistrasiTesisController { throw new ForbiddenException(); } - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) { - throw new BadRequestException("Periode belum dikonfigurasi."); - } - return await this.registrasiTesisService.findAllRegs({ ...query, page: query.page || 1, idPenerima: query.view === RoleEnum.S2_PEMBIMBING ? idPenerima : undefined, - periode, }); } @@ -224,14 +197,6 @@ export class RegistrasiTesisController { @Body() body: UpdateInterviewBodyDto, @Req() req: Request, ) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) { - throw new BadRequestException("Periode belum dikonfigurasi."); - } - const { id, roles } = req.user as AuthDto; let idPenerima = undefined; @@ -245,7 +210,6 @@ export class RegistrasiTesisController { return await this.registrasiTesisService.updateInterviewDate( params.mhsId, - periode, body, idPenerima, ); @@ -264,14 +228,6 @@ export class RegistrasiTesisController { @Body() body: UpdateStatusBodyDto, @Req() req: Request, ) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) { - throw new BadRequestException("Periode belum dikonfigurasi."); - } - const { id, roles } = req.user as AuthDto; let idPenerima = undefined; @@ -285,7 +241,6 @@ export class RegistrasiTesisController { return await this.registrasiTesisService.updateStatus( params.mhsId, - periode, body, idPenerima, ); @@ -303,17 +258,8 @@ export class RegistrasiTesisController { @Param() params: UpdateByMhsParamsDto, @Body() body: UpdatePembimbingBodyDto, ) { - const periode = await this.konfService.getKonfigurasiByKey( - process.env.KONF_PERIODE_KEY, - ); - - if (!periode) { - throw new BadRequestException("Periode belum dikonfigurasi."); - } - return await this.registrasiTesisService.updatePembimbingList( params.mhsId, - periode, body, ); } diff --git a/src/registrasi-tesis/registrasi-tesis.dto.ts b/src/registrasi-tesis/registrasi-tesis.dto.ts index 330f31d2bf8ea6118fa74403d210c1cf87e13921..7d5d1bf284f8957d52f75af71505fad198e74b52 100644 --- a/src/registrasi-tesis/registrasi-tesis.dto.ts +++ b/src/registrasi-tesis/registrasi-tesis.dto.ts @@ -16,25 +16,28 @@ import { 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 { @@ -168,7 +171,11 @@ export class UpdatePembimbingBodyDto { class DosenPembimbingDto extends PickType(Pengguna, [ "id", "nama", - "kontak", + "kontakWhatsApp", + "kontakMsTeams", + "kontakEmail", + "kontakTelp", + "kontakLainnya", ] as const) {} export class GetByIdRespDto extends PickType(PendaftaranTesis, [ diff --git a/src/registrasi-tesis/registrasi-tesis.module.ts b/src/registrasi-tesis/registrasi-tesis.module.ts index 65926b077ae351036dcd06e380b17bec135f42bc..0b64d44cec70b955ad05a008eedd82f63760e1c3 100644 --- a/src/registrasi-tesis/registrasi-tesis.module.ts +++ b/src/registrasi-tesis/registrasi-tesis.module.ts @@ -8,7 +8,8 @@ import { RegistrasiTesisService } from "./registrasi-tesis.service"; import { Topik } from "src/entities/topik.entity"; import { CustomStrategy } from "src/middlewares/custom.strategy"; import { AuthModule } from "src/auth/auth.module"; -import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; +import { PenggunaModule } from "src/pengguna/pengguna.module"; +import { PenggunaService } from "src/pengguna/pengguna.service"; @Module({ imports: [ @@ -19,9 +20,9 @@ import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module"; Topik, ]), AuthModule, - KonfigurasiModule, + PenggunaModule, ], controllers: [RegistrasiTesisController], - providers: [RegistrasiTesisService, CustomStrategy], + providers: [RegistrasiTesisService, CustomStrategy, PenggunaService], }) export class RegistrasiTesisModule {} diff --git a/src/registrasi-tesis/registrasi-tesis.service.ts b/src/registrasi-tesis/registrasi-tesis.service.ts index 74afb3e99311335adae3e191090ce7b4902964cb..17e59c6096fcf7f65aff48a21dda444938e8515e 100644 --- a/src/registrasi-tesis/registrasi-tesis.service.ts +++ b/src/registrasi-tesis/registrasi-tesis.service.ts @@ -6,6 +6,7 @@ import { NotFoundException, } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; +import * as dayjs from "dayjs"; import { DosenBimbingan } from "src/entities/dosenBimbingan.entity"; import { PendaftaranTesis, @@ -14,7 +15,6 @@ 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, Brackets, DataSource, In, Repository } from "typeorm"; import { FindAllNewestRegRespDto, @@ -25,7 +25,7 @@ import { UpdatePembimbingBodyDto, UpdateStatusBodyDto, } from "./registrasi-tesis.dto"; -import * as dayjs from "dayjs"; +import { PenggunaService } from "src/pengguna/pengguna.service"; @Injectable() export class RegistrasiTesisService { @@ -39,82 +39,132 @@ export class RegistrasiTesisService { @InjectRepository(DosenBimbingan) private dosenBimbinganRepository: Repository<DosenBimbingan>, private dataSource: DataSource, + private penggunaService: PenggunaService, ) {} 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 }, + ): Promise<IdDto> { + const queries: ( + | Promise<void | PendaftaranTesis> + | Promise<Pengguna> + | Promise<Topik> + )[] = [ + this.getNewestRegByMhsOrFail(userId).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 (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 (!user) { - throw new NotFoundException("User not found."); - } else if (!supervisor) { - throw new NotFoundException("Supervisor not found."); - } else if (!topic) { - throw new NotFoundException("Topic not found."); + if (topicRegistrationDto.idTopik) { + if (!topik) { + throw new NotFoundException("Topic not found."); + } + + if (!topik.aktif) { + throw new BadRequestException("Topic is not active."); + } + } + + 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, + }); } // 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, - periode: string, isNewestOnly: boolean, idPenerima?: string, ) { + await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId); + 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") + .select([ + "pt.id", + "pt.jadwalInterview", + "pt.status", + "pt.jalurPilihan", + "pt.waktuPengiriman", + "topik.judul", + "topik.deskripsi", + "penerima.id", + "penerima.nama", + "penerima.kontakWhatsApp", + "penerima.kontakMsTeams", + "penerima.kontakEmail", + "penerima.kontakTelp", + "penerima.kontakLainnya", + "dosenBimbingan", + "dosen.id", + "dosen.nama", + "dosen.kontakWhatsApp", + "dosen.kontakMsTeams", + "dosen.kontakEmail", + "dosen.kontakTelp", + "dosen.kontakLainnya", + ]) .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(); @@ -157,10 +207,9 @@ export class RegistrasiTesisService { } async getRegsStatistics(options: { - periode: string; idPenerima?: string; }): Promise<RegStatisticsRespDto> { - const totalMahasiswa = this.penggunaRepository.count({ + let totalMahasiswa = this.penggunaRepository.count({ where: { roles: ArrayContains([RoleEnum.S2_MAHASISWA]) }, }); @@ -179,13 +228,15 @@ export class RegistrasiTesisService { "latest", "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman", ) - .innerJoinAndSelect("pt.topik", "topik") - .where("topik.periode = :periode", { periode: options.periode }); + .innerJoin("pt.mahasiswa", "mahasiswa") + .where("mahasiswa.aktif = true"); if (options.idPenerima) { baseQuery.andWhere("pt.penerimaId = :idPenerima", { idPenerima: options.idPenerima, }); + + totalMahasiswa = baseQuery.getCount(); } const totalDiterima = baseQuery @@ -236,7 +287,6 @@ export class RegistrasiTesisService { search?: string; order_by?: "nim"; sort?: "ASC" | "DESC"; - periode: string; }) { const baseQuery = this.pendaftaranTesisRepository .createQueryBuilder("pt") @@ -258,10 +308,9 @@ export class RegistrasiTesisService { ); baseQuery - .innerJoinAndSelect("pt.topik", "topik") .innerJoinAndSelect("pt.penerima", "penerima") .innerJoinAndSelect("pt.mahasiswa", "mahasiswa") - .where("topik.periode = :periode", { periode: options.periode }); + .where("mahasiswa.aktif = true"); if (options.idPenerima) { baseQuery.andWhere("pt.penerimaId = :idPenerima", { @@ -269,7 +318,7 @@ export class RegistrasiTesisService { }); } - if (options.search) + if (options.search) { baseQuery.andWhere( new Brackets((qb) => qb @@ -281,11 +330,11 @@ export class RegistrasiTesisService { }), ), ); + } - if (options.status) - baseQuery.andWhere("pt.status = :status", { - status: options.status, - }); + if (options.status) { + baseQuery.andWhere("pt.status = :status", { status: options.status }); + } if (options.order_by) { const orderByMapping = { @@ -324,7 +373,7 @@ export class RegistrasiTesisService { return resData; } - private async getNewestRegByMhs(mahasiswaId: string, periode: string) { + private async getNewestRegByMhsOrFail(mahasiswaId: string) { const mahasiswa = await this.penggunaRepository.findOne({ select: { id: true, @@ -348,7 +397,6 @@ export class RegistrasiTesisService { topik: { judul: true, deskripsi: true, - periode: true, }, penerima: { id: true, @@ -361,9 +409,6 @@ export class RegistrasiTesisService { }, where: { mahasiswa: mahasiswa, - topik: { - periode, - }, }, order: { waktuPengiriman: "DESC", @@ -380,10 +425,11 @@ export class RegistrasiTesisService { async updateInterviewDate( mahasiswaId: string, - periode: string, dto: UpdateInterviewBodyDto, idPenerima?: string, ) { + await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId); + const minDate = new Date(); minDate.setDate(minDate.getDate() + 2); @@ -393,7 +439,7 @@ export class RegistrasiTesisService { ); } - const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode); + const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId); if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) { throw new ForbiddenException(); @@ -423,11 +469,12 @@ export class RegistrasiTesisService { async updateStatus( mahasiswaId: string, - periode: string, dto: UpdateStatusBodyDto, idPenerima?: string, ) { - const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode); + await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId); + + const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId); if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) { throw new ForbiddenException(); @@ -470,10 +517,11 @@ export class RegistrasiTesisService { async updatePembimbingList( mahasiswaId: string, - periode: string, { pembimbing_ids: dosen_ids }: UpdatePembimbingBodyDto, ) { - const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode); + await this.penggunaService.isMahasiswaAktifOrFail(mahasiswaId); + + const newestReg = await this.getNewestRegByMhsOrFail(mahasiswaId); if (newestReg.status !== RegStatus.APPROVED) throw new BadRequestException( diff --git a/src/submisi-tugas/submisi-tugas.controller.ts b/src/submisi-tugas/submisi-tugas.controller.ts deleted file mode 100644 index cb4f45a1b1cdb0d5535344ac9d8ccee62e4db8b9..0000000000000000000000000000000000000000 --- a/src/submisi-tugas/submisi-tugas.controller.ts +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index daa8486087d0e417d1ef0c72f9b74e8b8cf15900..0000000000000000000000000000000000000000 --- a/src/submisi-tugas/submisi-tugas.dto.ts +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 1fd15428a499fe276ed0aeaef1b8168bd532b105..0000000000000000000000000000000000000000 --- a/src/submisi-tugas/submisi-tugas.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index d3c1c11a98b638aecbf96c5b67c1b8326073ef93..0000000000000000000000000000000000000000 --- a/src/submisi-tugas/submisi-tugas.service.ts +++ /dev/null @@ -1,257 +0,0 @@ -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 deleted file mode 100644 index b66f4efe167c7a14fea84ac9c710f4f559155186..0000000000000000000000000000000000000000 --- a/src/tugas/tugas.controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -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 deleted file mode 100644 index afbb6eff67825b726e20169db6dee9e73d79f0ce..0000000000000000000000000000000000000000 --- a/src/tugas/tugas.dto.ts +++ /dev/null @@ -1,109 +0,0 @@ -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 deleted file mode 100644 index 3a8d49258c546ffaa9d9d95014f4d10af9190442..0000000000000000000000000000000000000000 --- a/src/tugas/tugas.module.ts +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 0f99625c045bdc011de895afd60bd01371658ecb..0000000000000000000000000000000000000000 --- a/src/tugas/tugas.service.ts +++ /dev/null @@ -1,277 +0,0 @@ -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 }; - } -}