diff --git a/src/entities/kelas.entity.ts b/src/entities/kelas.entity.ts index deec327ed9a9028f7eb8ddd886826f35a3294c80..7070b47519d5ed99f52281c454c32e04f01ef2f3 100644 --- a/src/entities/kelas.entity.ts +++ b/src/entities/kelas.entity.ts @@ -26,6 +26,7 @@ export class Kelas { @PrimaryGeneratedColumn("uuid") id: string; + @ApiProperty({ example: 1 }) @Column({ type: "smallint" }) @ApiProperty({ example: 1 }) @IsPositive() diff --git a/src/entities/pengguna.entity.ts b/src/entities/pengguna.entity.ts index c28a606ea4031436e36fc7958c0756a78f898cbe..7e4899c2764d11b3871fa2d7e502aed033a04406 100644 --- a/src/entities/pengguna.entity.ts +++ b/src/entities/pengguna.entity.ts @@ -5,6 +5,8 @@ import { ApiPropertyOptional, } from "@nestjs/swagger"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { MahasiswaKelas } from "./mahasiswaKelas.entity"; +import { PengajarKelas } from "./pengajarKelas.entity"; import { PendaftaranTesis } from "./pendaftaranTesis.entity"; import { SubmisiTugas } from "./submisiTugas.entity"; @@ -54,6 +56,11 @@ export class Pengguna { }) roles: RoleEnum[]; + @OneToMany(() => MahasiswaKelas, (mahasiswaKelas) => mahasiswaKelas.mahasiswa) + mahasiswaKelas: MahasiswaKelas[]; + + @OneToMany(() => PengajarKelas, (pengajarKelas) => pengajarKelas.pengajar) + pengajarKelas: PengajarKelas[]; @ApiPropertyOptional() @IsString() @Column({ type: "text", nullable: true }) diff --git a/src/kelas/kelas.controller.ts b/src/kelas/kelas.controller.ts index ae942b029bd3415ece79a2d5df0b3b4ad5befd3a..3c64b3d4c8c22e85d157f750ee961ee852f9cf6d 100644 --- a/src/kelas/kelas.controller.ts +++ b/src/kelas/kelas.controller.ts @@ -12,6 +12,7 @@ import { UseGuards, } from "@nestjs/common"; import { + AssignKelasDto, ByIdKelasDto, CreateKelasDto, DeleteKelasDto, @@ -21,7 +22,12 @@ import { GetNextNomorResDto, IdKelasResDto, KodeRespDto, + MessageResDto, + UnassignKelasDto, + UserKelasResDto, UpdateKelasDto, + SearchQueryDto, + UpdateKelasPenggunaDto, } from "./kelas.dto"; import { Request } from "express"; import { AuthDto } from "src/auth/auth.dto"; @@ -147,7 +153,6 @@ export class KelasController { 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" }) @@ -156,6 +161,60 @@ export class KelasController { return await this.kelasServ.delete(body); } + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOkResponse({ type: UserKelasResDto, isArray: true }) + @Get("/mahasiswa") + async getMahasiswa( + @Query() query: SearchQueryDto, + ): Promise<UserKelasResDto[]> { + return await this.kelasServ.getKelasPengguna("MAHASISWA", query.search); + } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiCreatedResponse({ type: MessageResDto }) + @ApiInternalServerErrorResponse({ description: "Gagal menambahkan kelas" }) + @Post("/mahasiswa/assign") + async assignKelasMahasiswa( + @Body() body: AssignKelasDto, + ): Promise<MessageResDto> { + return await this.kelasServ.assignKelasMahasiswa(body); + } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOkResponse({ type: MessageResDto }) + @ApiInternalServerErrorResponse({ description: "Gagal menghapus kelas" }) + @Delete("/mahasiswa/unassign") + async unassignKelasMahasiswa( + @Body() body: UnassignKelasDto, + ): Promise<MessageResDto> { + return await this.kelasServ.unassignKelasMahasiswa(body); + } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOkResponse({ type: UserKelasResDto, isArray: true }) + @Get("/dosen") + async getDosen(@Query() query: SearchQueryDto): Promise<UserKelasResDto[]> { + return await this.kelasServ.getKelasPengguna("DOSEN", query.search); + } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiCreatedResponse({ type: MessageResDto }) + @ApiInternalServerErrorResponse({ description: "Gagal menambahkan kelas" }) + @Post("/dosen/assign") + async assignKelasDosen(@Body() body: AssignKelasDto): Promise<MessageResDto> { + return await this.kelasServ.assignKelasDosen(body); + } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOkResponse({ type: MessageResDto }) + @ApiInternalServerErrorResponse({ description: "Gagal menghapus kelas" }) + @Delete("/dosen/unassign") + async unassignKelasDosen( + @Body() body: UnassignKelasDto, + ): Promise<MessageResDto> { + return await this.kelasServ.unassignKelasDosen(body); + } + @Roles( RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN, @@ -228,4 +287,28 @@ export class KelasController { idPengajar, ); } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Update kelas mahasiswa. Roles: S2_TIM_TESIS, ADMIN", + }) + @ApiOkResponse({ type: ByIdKelasDto }) + @Put("/mahasiswa") + async updateKelasMahasiswa( + @Body() body: UpdateKelasPenggunaDto, + ): Promise<ByIdKelasDto> { + return await this.kelasServ.updateKelas(body, "MAHASISWA"); + } + + @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN) + @ApiOperation({ + summary: "Update kelas dosen. Roles: S2_TIM_TESIS, ADMIN", + }) + @ApiOkResponse({ type: ByIdKelasDto }) + @Put("/dosen") + async updateKelasDosen( + @Body() body: UpdateKelasPenggunaDto, + ): Promise<ByIdKelasDto> { + return await this.kelasServ.updateKelas(body, "DOSEN"); + } } diff --git a/src/kelas/kelas.dto.ts b/src/kelas/kelas.dto.ts index 352ae36ea1a1d0c0e45ee7e9eb6eb778cd460439..3647164851b0a08f38fda3c651db4cc17587c086 100644 --- a/src/kelas/kelas.dto.ts +++ b/src/kelas/kelas.dto.ts @@ -1,4 +1,9 @@ -import { IsEnum, IsOptional, IsPositive } from "@nestjs/class-validator"; +import { + IsEnum, + IsOptional, + IsPositive, + IsUUID, +} from "@nestjs/class-validator"; import { ApiProperty, PickType, @@ -43,6 +48,12 @@ export class GetKelasQueryDto { search: string; } +export class SearchQueryDto { + @ApiPropertyOptional({ example: "Intelegensi Buatan" }) + @IsOptional() + search: string; +} + export class ByIdKelasDto extends PickType(Kelas, ["id"] as const) {} export class GetKelasRespDto { @@ -67,6 +78,42 @@ export class GetKelasRespDto { export class KodeRespDto extends PickType(MataKuliah, ["kode"] as const) {} +export class AssignKelasDto { + @ApiProperty() + @IsUUID("all", { each: true }) + kelasIds: string[]; + + @ApiProperty() + @IsUUID("all", { each: true }) + penggunaIds: string[]; +} + +export class UnassignKelasDto extends PickType(AssignKelasDto, [ + "penggunaIds", +] as const) {} + +export class MessageResDto { + @ApiProperty() + message: string; +} + +class KelasUser extends PickType(Kelas, [ + "id", + "nomor", + "mataKuliahKode", +] as const) { + @ApiProperty() + mataKuliahNama: string; +} + +export class UserKelasResDto extends PickType(Pengguna, [ + "id", + "nama", + "email", +] as const) { + @ApiProperty({ type: [KelasUser] }) + kelas: KelasUser[]; +} export class GetNextNomorResDto { @ApiProperty({ example: 2 }) nomor: number; @@ -90,3 +137,11 @@ export class GetKelasDetailRespDto extends PickType(Kelas, ["id"] as const) { @ApiProperty({ type: [PickedMahasiswaKelasDto] }) mahasiswa: PickedMahasiswaKelasDto[]; } + +export class UpdateKelasPenggunaDto extends PickType(AssignKelasDto, [ + "kelasIds" as const, +]) { + @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" }) + @IsUUID() + penggunaId: string; +} diff --git a/src/kelas/kelas.module.ts b/src/kelas/kelas.module.ts index 38dfa27f8690f2608b1450c134536f7d3d920651..dde13c556b1ffb854621c5394945ace1ed81225c 100644 --- a/src/kelas/kelas.module.ts +++ b/src/kelas/kelas.module.ts @@ -7,10 +7,19 @@ 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"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; +import { PengajarKelas } from "src/entities/pengajarKelas.entity"; @Module({ imports: [ - TypeOrmModule.forFeature([Kelas, MataKuliah]), + TypeOrmModule.forFeature([ + Kelas, + MataKuliah, + Pengguna, + MahasiswaKelas, + PengajarKelas, + ]), AuthModule, KonfigurasiModule, ], diff --git a/src/kelas/kelas.service.ts b/src/kelas/kelas.service.ts index fa62a4707fe36efeca92ef76b8074dd392e7a92b..f55b832ce066eb60d33e6e2ce1900ddbd9de5cbe 100644 --- a/src/kelas/kelas.service.ts +++ b/src/kelas/kelas.service.ts @@ -6,18 +6,26 @@ import { } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Kelas } from "src/entities/kelas.entity"; -import { Brackets, Repository } from "typeorm"; +import { Brackets, DataSource, Repository } from "typeorm"; import { CreateKelasDto, + AssignKelasDto, DeleteKelasDto, GetKelasDetailRespDto, GetKelasRespDto, IdKelasResDto, UpdateKelasDto, + MessageResDto, + UnassignKelasDto, + UpdateKelasPenggunaDto, + ByIdKelasDto, } from "./kelas.dto"; import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service"; import { MataKuliah } from "src/entities/mataKuliah.entity"; import { CARD_COLORS } from "./kelas.constant"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity"; +import { PengajarKelas } from "src/entities/pengajarKelas.entity"; @Injectable() export class KelasService { @@ -27,6 +35,13 @@ export class KelasService { @InjectRepository(MataKuliah) private mataKuliahRepo: Repository<MataKuliah>, private konfService: KonfigurasiService, + @InjectRepository(Pengguna) + private penggunaRepo: Repository<Pengguna>, + @InjectRepository(MahasiswaKelas) + private mahasiswaKelasRepo: Repository<MahasiswaKelas>, + @InjectRepository(PengajarKelas) + private pengajarKelasRepo: Repository<PengajarKelas>, + private datasource: DataSource, ) {} async getListKelas( idMahasiswa?: string, @@ -271,6 +286,253 @@ export class KelasService { return { kode: createDto.kode }; } + async getKelasPengguna( + mode: "MAHASISWA" | "DOSEN", + search?: string, + id?: string, + ) { + const currPeriod = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + const relation = mode === "MAHASISWA" ? "mahasiswaKelas" : "pengajarKelas"; + + let penggunaQuery = this.penggunaRepo + .createQueryBuilder("pengguna") + .select(["pengguna.id", "pengguna.nama", "pengguna.email"]) + .leftJoinAndSelect(`pengguna.${relation}`, relation) + .leftJoinAndSelect( + `${relation}.kelas`, + "kelas", + "kelas.periode = :periode", + { + periode: currPeriod, + }, + ) + .leftJoinAndSelect("kelas.mataKuliah", "mataKuliah") + .where("pengguna.roles @> :role", { + role: [mode === "MAHASISWA" ? "S2_MAHASISWA" : "S2_KULIAH"], + }); + + if (search) { + penggunaQuery = penggunaQuery.andWhere( + new Brackets((qb) => { + qb.where("pengguna.nama ILIKE :search", { search: `%${search}%` }); + qb.orWhere("pengguna.email ILIKE :search", { search: `%${search}%` }); + }), + ); + } + + if (id) { + penggunaQuery = penggunaQuery.andWhere("pengguna.id = :id", { id }); + } + + const mhs = await penggunaQuery.getMany(); + + return mhs.map((m) => ({ + id: m.id, + nama: m.nama, + email: m.email, + kelas: m?.[relation].map((k) => ({ + id: k.kelas.id, + nomor: k.kelas.nomor, + mataKuliahKode: k.kelas.mataKuliahKode, + mataKuliahNama: k.kelas.mataKuliah.nama, + })), + })); + } + + async assignKelasMahasiswa(dto: AssignKelasDto): Promise<MessageResDto> { + const currPeriod = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + const queryRunner = this.datasource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + for (const mhsId of dto.penggunaIds) { + const currKelasQuery = queryRunner.manager + .createQueryBuilder(MahasiswaKelas, "mahasiswaKelas") + .leftJoinAndSelect("mahasiswaKelas.kelas", "kelas") + .where("mahasiswaKelas.mahasiswaId = :mhsId", { mhsId }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + const currKelas = (await currKelasQuery.getMany()).map( + (k) => k.kelasId, + ); + + for (const kelasId of dto.kelasIds) { + if (currKelas.includes(kelasId)) { + continue; + } + + await queryRunner.manager.insert(MahasiswaKelas, { + mahasiswaId: mhsId, + kelasId, + }); + } + } + + await queryRunner.commitTransaction(); + } catch { + await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException("Gagal menambahkan kelas"); + } finally { + await queryRunner.release(); + } + + return { message: "Kelas berhasil diassign" }; + } + + async unassignKelasMahasiswa(dto: UnassignKelasDto): Promise<MessageResDto> { + const currPeriod = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + const queryRunner = this.datasource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + for (const mhsId of dto.penggunaIds) { + const currKelasQuery = queryRunner.manager + .createQueryBuilder(MahasiswaKelas, "mahasiswaKelas") + .leftJoinAndSelect("mahasiswaKelas.kelas", "kelas") + .where("mahasiswaKelas.mahasiswaId = :mhsId", { mhsId }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + const currKelas = (await currKelasQuery.getMany()).map( + (k) => k.kelasId, + ); + + for (const kelasId of currKelas) { + await queryRunner.manager.delete(MahasiswaKelas, { + mahasiswaId: mhsId, + kelasId, + }); + } + } + + await queryRunner.commitTransaction(); + } catch { + await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException("Gagal menghapus kelas"); + } finally { + await queryRunner.release(); + } + + return { message: "Kelas berhasil dihapus" }; + } + + async assignKelasDosen(dto: AssignKelasDto): Promise<MessageResDto> { + const currPeriod = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + const queryRunner = this.datasource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + for (const dosenId of dto.penggunaIds) { + const currKelasQuery = queryRunner.manager + .createQueryBuilder(PengajarKelas, "pengajarKelas") + .leftJoinAndSelect("pengajarKelas.kelas", "kelas") + .where("pengajarKelas.pengajarId = :dosenId", { dosenId }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + const currKelas = (await currKelasQuery.getMany()).map( + (k) => k.kelasId, + ); + + for (const kelasId of dto.kelasIds) { + if (currKelas.includes(kelasId)) { + continue; + } + + await queryRunner.manager.insert(PengajarKelas, { + pengajarId: dosenId, + kelasId, + }); + } + } + + await queryRunner.commitTransaction(); + } catch { + await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException("Gagal menambahkan kelas"); + } finally { + await queryRunner.release(); + } + + return { message: "Kelas berhasil diassign" }; + } + + async unassignKelasDosen(dto: UnassignKelasDto): Promise<MessageResDto> { + const currPeriod = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + const queryRunner = this.datasource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + for (const dosenId of dto.penggunaIds) { + const currKelasQuery = queryRunner.manager + .createQueryBuilder(PengajarKelas, "pengajarKelas") + .leftJoinAndSelect("pengajarKelas.kelas", "kelas") + .where("pengajarKelas.pengajarId = :dosenId", { dosenId }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + const currKelas = (await currKelasQuery.getMany()).map( + (k) => k.kelasId, + ); + + for (const kelasId of currKelas) { + await queryRunner.manager.delete(PengajarKelas, { + pengajarId: dosenId, + kelasId, + }); + } + } + + await queryRunner.commitTransaction(); + } catch { + await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException("Gagal menghapus kelas"); + } finally { + await queryRunner.release(); + } + + return { message: "Kelas berhasil dihapus" }; + } + async updateOrCreate(dto: UpdateKelasDto): Promise<IdKelasResDto> { const currPeriod = await this.konfService.getPeriodeOrFail(); @@ -358,4 +620,89 @@ export class KelasService { return maxClass ? maxClass.nomor + 1 : 1; } + + async updateKelas( + dto: UpdateKelasPenggunaDto, + mode: "MAHASISWA" | "DOSEN", + ): Promise<ByIdKelasDto> { + const currPeriod = await this.konfService.getKonfigurasiByKey( + process.env.KONF_PERIODE_KEY, + ); + + if (!currPeriod) { + throw new BadRequestException("Periode belum dikonfigurasi"); + } + + const queryRunner = this.datasource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + if (mode === "MAHASISWA") { + const currKelasQuery = queryRunner.manager + .createQueryBuilder(MahasiswaKelas, "mahasiswaKelas") + .leftJoinAndSelect("mahasiswaKelas.kelas", "kelas") + .where("mahasiswaKelas.mahasiswaId = :mhsId", { + mhsId: dto.penggunaId, + }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + const currKelas = (await currKelasQuery.getMany()).map( + (k) => k.kelasId, + ); + + // Delete all kelas mahasiswa + for (const kelasId of currKelas) { + await queryRunner.manager.delete(MahasiswaKelas, { + mahasiswaId: dto.penggunaId, + kelasId, + }); + } + + // Assign kelas mahasiswa + for (const kelasId of dto.kelasIds) { + await queryRunner.manager.insert(MahasiswaKelas, { + mahasiswaId: dto.penggunaId, + kelasId, + }); + } + } else { + const currKelasQuery = queryRunner.manager + .createQueryBuilder(PengajarKelas, "pengajarKelas") + .leftJoinAndSelect("pengajarKelas.kelas", "kelas") + .where("pengajarKelas.pengajarId = :dosenId", { + dosenId: dto.penggunaId, + }) + .andWhere("kelas.periode = :periode", { periode: currPeriod }); + + const currKelas = (await currKelasQuery.getMany()).map( + (k) => k.kelasId, + ); + + for (const kelasId of currKelas) { + await queryRunner.manager.delete(PengajarKelas, { + pengajarId: dto.penggunaId, + kelasId, + }); + } + + for (const kelasId of dto.kelasIds) { + await queryRunner.manager.insert(PengajarKelas, { + pengajarId: dto.penggunaId, + kelasId, + }); + } + } + + await queryRunner.commitTransaction(); + } catch { + await queryRunner.rollbackTransaction(); + + throw new InternalServerErrorException("Gagal mengupdate kelas pengguna"); + } finally { + await queryRunner.release(); + } + + return { id: dto.penggunaId }; + } } diff --git a/src/nilai/nilai.module.ts b/src/nilai/nilai.module.ts index f9aca923b411670865c235c946b28d698a099a52..c0a7c9c19ed393373632c2a8f3f8f5c6cc5610a8 100644 --- a/src/nilai/nilai.module.ts +++ b/src/nilai/nilai.module.ts @@ -12,10 +12,19 @@ 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"; +import { Pengguna } from "src/entities/pengguna.entity"; +import { PengajarKelas } from "src/entities/pengajarKelas.entity"; @Module({ imports: [ - TypeOrmModule.forFeature([MahasiswaKelas, Kelas, MataKuliah, Konfigurasi]), + TypeOrmModule.forFeature([ + MahasiswaKelas, + Kelas, + MataKuliah, + Konfigurasi, + Pengguna, + PengajarKelas, + ]), AuthModule, KonfigurasiModule, KelasModule,