diff --git a/src/app.module.ts b/src/app.module.ts
index 7d86d78321e38b5e7ee89251508aa951410c824d..e2718653c5b8f7856d0baf4d258e4cacf378060b 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -4,22 +4,16 @@ import { AppService } from "./app.service";
 import { TypeOrmModule } from "@nestjs/typeorm";
 import { Bimbingan } from "./entities/bimbingan.entity";
 import { Pengguna } from "./entities/pengguna.entity";
-import { RangeJadwalSeminar } from "./entities/rangeJadwalSeminar.entity";
-import { Seminar } from "./entities/seminar.entity";
 import { Topik } from "./entities/topik.entity";
 import { AuditLog } from "./entities/auditLog.entity";
 import { DosenBimbingan } from "./entities/dosenBimbingan.entity";
 import { Kelas } from "./entities/kelas.entity";
-import { MahasiswaKelas } from "./entities/mahasiswaKelas";
+import { MahasiswaKelas } from "./entities/mahasiswaKelas.entity";
 import { PengajarKelas } from "./entities/pengajarKelas.entity";
 import { PendaftaranTesis } from "./entities/pendaftaranTesis.entity";
-import { RangeJadwalSidang } from "./entities/rangeJadwalSidang.entity";
-import { Ruangan } from "./entities/ruangan.entity";
-import { Sidang } from "./entities/sidang.entity";
+// import { Ruangan } from "./entities/ruangan.entity";
 import { Tugas } from "./entities/tugas.entity";
-import { PembimbingSeminar } from "./entities/pembimbingSeminar.entity";
-import { PembimbingSidang } from "./entities/pembimbingSidang.entity";
-import { PengujiSidang } from "./entities/pengujiSidang.entity";
+import { PengujiSidsem } from "./entities/pengujiSidsem.entity";
 import { RegistrasiTesisModule } from "./registrasi-tesis/registrasi-tesis.module";
 import { ConfigModule } from "@nestjs/config";
 import { AuthModule } from "./auth/auth.module";
@@ -28,14 +22,18 @@ import { DashboardModule } from "./dashboard/dashboard.module";
 import { BimbinganModule } from "./bimbingan/bimbingan.module";
 import { Konfigurasi } from "./entities/konfigurasi.entity";
 import { KonfigurasiModule } from "./konfigurasi/konfigurasi.module";
-import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module";
-import { ApprovalModule } from "./approval/approval.module";
 import { validate } from "./env.validation";
-import { MataKuliah } from "./entities/mataKuliah";
-import { SubmisiTugas } from "./entities/submisiTugas";
+import { BerkasBimbingan } from "./entities/berkasBimbingan.entity";
+import { MataKuliah } from "./entities/mataKuliah.entity";
+import { SubmisiTugas } from "./entities/submisiTugas.entity";
+import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas.entity";
+import { BerkasTugas } from "./entities/berkasTugas.entity";
+import { TugasModule } from "./tugas/tugas.module";
 import { KelasModule } from "./kelas/kelas.module";
-import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas";
-import { BerkasTugas } from "./entities/berkasTugas";
+import { SubmisiTugasModule } from "./submisi-tugas/submisi-tugas.module";
+import { NilaiModule } from "./nilai/nilai.module";
+import { PendaftaranSidsem } from "./entities/pendaftaranSidsem";
+import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module";
 
 @Module({
   imports: [
@@ -49,10 +47,10 @@ import { BerkasTugas } from "./entities/berkasTugas";
       database: process.env.POSTGRES_DATABASE,
       ssl: process.env.POSTGRES_HOST !== "localhost",
       entities: [
+        BerkasBimbingan,
         Bimbingan,
         Pengguna,
-        RangeJadwalSeminar,
-        Seminar,
+        PendaftaranSidsem,
         Topik,
         AuditLog,
         DosenBimbingan,
@@ -60,13 +58,9 @@ import { BerkasTugas } from "./entities/berkasTugas";
         MahasiswaKelas,
         PengajarKelas,
         PendaftaranTesis,
-        RangeJadwalSidang,
-        Ruangan,
-        Sidang,
+        // Ruangan,
         Tugas,
-        PembimbingSeminar,
-        PembimbingSidang,
-        PengujiSidang,
+        PengujiSidsem,
         Konfigurasi,
         MataKuliah,
         SubmisiTugas,
@@ -81,9 +75,11 @@ import { BerkasTugas } from "./entities/berkasTugas";
     DashboardModule,
     BimbinganModule,
     KonfigurasiModule,
-    DosenBimbinganModule,
-    ApprovalModule,
     KelasModule,
+    TugasModule,
+    SubmisiTugasModule,
+    NilaiModule,
+    DosenBimbinganModule,
   ],
   controllers: [AppController],
   providers: [AppService],
diff --git a/src/approval/approval.controller.ts b/src/approval/approval.controller.ts
deleted file mode 100644
index 866410794f5f767cd84ad5489fdcdc5817c9024b..0000000000000000000000000000000000000000
--- a/src/approval/approval.controller.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Controller, Patch, Param, UseGuards } from "@nestjs/common";
-import { ApprovalService } from "./approval.service";
-import {
-  PendaftaranTesis,
-  RegStatus,
-} from "src/entities/pendaftaranTesis.entity";
-import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
-import { RolesGuard } from "src/middlewares/roles.guard";
-import { RoleEnum } from "src/entities/pengguna.entity";
-import { Roles } from "src/middlewares/roles.decorator";
-import {
-  ApiBearerAuth,
-  ApiCookieAuth,
-  ApiOkResponse,
-  ApiTags,
-} from "@nestjs/swagger";
-import { ByIdParamDto } from "./approval.dto";
-
-@ApiTags("Approval")
-@ApiCookieAuth()
-@ApiBearerAuth()
-@Controller("approval")
-@UseGuards(CustomAuthGuard, RolesGuard)
-@Roles(RoleEnum.S2_PEMBIMBING)
-export class ApprovalController {
-  constructor(private readonly approvalService: ApprovalService) {}
-
-  @ApiOkResponse({ type: PendaftaranTesis })
-  @Patch(":id/approve")
-  async approvePendaftaran(
-    @Param() param: ByIdParamDto,
-  ): Promise<PendaftaranTesis> {
-    return this.approvalService.approvePendaftaran(
-      param.id,
-      RegStatus.APPROVED,
-    );
-  }
-
-  @ApiOkResponse({ type: PendaftaranTesis })
-  @Patch(":id/reject")
-  async declinePendaftaran(
-    @Param() param: ByIdParamDto,
-  ): Promise<PendaftaranTesis> {
-    return this.approvalService.approvePendaftaran(
-      param.id,
-      RegStatus.REJECTED,
-    );
-  }
-
-  @ApiOkResponse({ type: PendaftaranTesis })
-  @Patch(":id/interview")
-  async interviewPendaftaran(
-    @Param() param: ByIdParamDto,
-  ): Promise<PendaftaranTesis> {
-    return this.approvalService.approvePendaftaran(
-      param.id,
-      RegStatus.INTERVIEW,
-    );
-  }
-}
diff --git a/src/approval/approval.dto.ts b/src/approval/approval.dto.ts
deleted file mode 100644
index b9fea475cfd34a9705b8b8fcd66f0a69ca1995d2..0000000000000000000000000000000000000000
--- a/src/approval/approval.dto.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { IsUUID } from "@nestjs/class-validator";
-import { ApiProperty } from "@nestjs/swagger";
-
-export class ByIdParamDto {
-  @IsUUID()
-  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
-  id: string;
-}
diff --git a/src/approval/approval.module.ts b/src/approval/approval.module.ts
deleted file mode 100644
index c28790252c59277b51e473a33ab7d8b38ae32065..0000000000000000000000000000000000000000
--- a/src/approval/approval.module.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Module } from "@nestjs/common";
-import { TypeOrmModule } from "@nestjs/typeorm";
-import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
-import { ApprovalController } from "./approval.controller";
-import { ApprovalService } from "./approval.service";
-
-@Module({
-  imports: [TypeOrmModule.forFeature([PendaftaranTesis])],
-  controllers: [ApprovalController],
-  providers: [ApprovalService],
-})
-export class ApprovalModule {}
diff --git a/src/approval/approval.service.ts b/src/approval/approval.service.ts
deleted file mode 100644
index 8163efeda7b43052e82b6a5441b5590f384f1b6a..0000000000000000000000000000000000000000
--- a/src/approval/approval.service.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Injectable } from "@nestjs/common";
-import { InjectRepository } from "@nestjs/typeorm";
-import { FindOneOptions, Repository } from "typeorm";
-import {
-  PendaftaranTesis,
-  RegStatus,
-} from "src/entities/pendaftaranTesis.entity";
-
-@Injectable()
-export class ApprovalService {
-  constructor(
-    @InjectRepository(PendaftaranTesis)
-    private readonly pendaftaranRepository: Repository<PendaftaranTesis>,
-  ) {}
-
-  async approvePendaftaran(
-    id: string,
-    status: RegStatus,
-  ): Promise<PendaftaranTesis> {
-    try {
-      const findOneOptions: FindOneOptions<PendaftaranTesis> = {
-        where: { id },
-      };
-      const pendaftaran =
-        await this.pendaftaranRepository.findOneOrFail(findOneOptions);
-      pendaftaran.status = status;
-
-      return await this.pendaftaranRepository.save(pendaftaran);
-    } catch (error) {
-      throw new Error("Pendaftaran not found");
-    }
-  }
-}
diff --git a/src/bimbingan/bimbingan.controller.ts b/src/bimbingan/bimbingan.controller.ts
index c7c7f678c42446ed9f1fe518eccb7e8320359daf..99eab5d006d76622b2d8cf3a7699c41f93e4ead2 100644
--- a/src/bimbingan/bimbingan.controller.ts
+++ b/src/bimbingan/bimbingan.controller.ts
@@ -3,14 +3,18 @@ import {
   Controller,
   Get,
   Param,
+  Patch,
   Post,
   Req,
   UseGuards,
 } from "@nestjs/common";
 import {
+  ApiBadRequestResponse,
   ApiBearerAuth,
   ApiBody,
   ApiCookieAuth,
+  ApiForbiddenResponse,
+  ApiNotFoundResponse,
   ApiOkResponse,
   ApiResponse,
   ApiTags,
@@ -25,7 +29,10 @@ import {
   ByMhsIdDto,
   CreateBimbinganReqDto,
   CreateBimbinganResDto,
+  GetByBimbinganIdResDto,
   GetByMahasiswaIdResDto,
+  UpdateStatusDto,
+  UpdateStatusResDto,
 } from "./bimbingan.dto";
 import { BimbinganService } from "./bimbingan.service";
 
@@ -38,8 +45,11 @@ export class BimbinganController {
   constructor(private readonly bimbinganService: BimbinganService) {}
 
   @ApiOkResponse({ type: GetByMahasiswaIdResDto })
+  @ApiNotFoundResponse({
+    description: "Tidak ada pendaftaran pada periode sekarang",
+  })
   @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
-  @Get("/:mahasiswaId")
+  @Get("/mahasiswa/:mahasiswaId")
   async getByMahasiswaId(
     @Param() param: ByMhsIdDto,
     @Req() request: Request,
@@ -51,6 +61,9 @@ export class BimbinganController {
   }
 
   @ApiOkResponse({ type: GetByMahasiswaIdResDto })
+  @ApiNotFoundResponse({
+    description: "Tidak ada pendaftaran pada periode sekarang",
+  })
   @Roles(RoleEnum.S2_MAHASISWA)
   @Get("/")
   async getOwnBimbingan(
@@ -62,15 +75,54 @@ export class BimbinganController {
     );
   }
 
-  // TODO handle file upload
   @ApiResponse({ status: 201, type: CreateBimbinganResDto })
+  @ApiBadRequestResponse({
+    description:
+      "Waktu bimbingan lebih dari hari ini atau bimbingan berikutnya sebelum waktu bimbingan yang dimasukkan",
+  })
+  @ApiNotFoundResponse({
+    description: "Tidak ada pendaftaran pada periode sekarang",
+  })
   @Roles(RoleEnum.S2_MAHASISWA)
   @ApiBody({ type: CreateBimbinganReqDto })
   @Post("/")
-  async getBimbinganLogs(
+  async createBimbinganLog(
     @Req() request: Request,
     @Body() body: CreateBimbinganReqDto,
   ): Promise<CreateBimbinganResDto> {
     return this.bimbinganService.create((request.user as AuthDto).id, body);
   }
+
+  @ApiOkResponse({ type: UpdateStatusResDto })
+  @ApiNotFoundResponse({
+    description: "Bimbingan tidak ditemukan",
+  })
+  @ApiForbiddenResponse({
+    description: "Anda tidak memiliki akses untuk mengubah status bimbingan",
+  })
+  @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN)
+  @ApiBody({ type: UpdateStatusDto })
+  @Patch("/pengesahan")
+  async updateStatus(
+    @Req() request: Request,
+    @Body() body: UpdateStatusDto,
+  ): Promise<UpdateStatusResDto> {
+    return this.bimbinganService.updateStatus(request.user as AuthDto, body);
+  }
+
+  @ApiOkResponse({ type: GetByBimbinganIdResDto })
+  @ApiNotFoundResponse({
+    description: "Bimbingan tidak ditemukan",
+  })
+  @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN)
+  @Get("/:bimbinganId")
+  async getByBimbinganId(
+    @Req() request: Request,
+    @Param("bimbinganId") bimbinganId: string,
+  ): Promise<GetByBimbinganIdResDto> {
+    return this.bimbinganService.getByBimbinganId(
+      request.user as AuthDto,
+      bimbinganId,
+    );
+  }
 }
diff --git a/src/bimbingan/bimbingan.dto.ts b/src/bimbingan/bimbingan.dto.ts
index 3d41ff0017cd71e2cd996908671baea4809107b2..f3afcdac8aeeb0380633e5609bc7b35d8c864013 100644
--- a/src/bimbingan/bimbingan.dto.ts
+++ b/src/bimbingan/bimbingan.dto.ts
@@ -1,7 +1,25 @@
-import { IsDateString, IsString } from "@nestjs/class-validator";
-import { ApiProperty, PickType } from "@nestjs/swagger";
-import { Bimbingan } from "src/entities/bimbingan.entity";
-import { JalurEnum } from "src/entities/pendaftaranTesis.entity";
+import {
+  IsBoolean,
+  IsDateString,
+  IsDefined,
+  IsOptional,
+  IsString,
+  IsUUID,
+  ValidateNested,
+} from "@nestjs/class-validator";
+import {
+  ApiProperty,
+  IntersectionType,
+  OmitType,
+  PickType,
+} from "@nestjs/swagger";
+import { Type } from "class-transformer";
+import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
+import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity";
+import {
+  JalurEnum,
+  PendaftaranTesis,
+} from "src/entities/pendaftaranTesis.entity";
 import { Pengguna } from "src/entities/pengguna.entity";
 import { Topik } from "src/entities/topik.entity";
 
@@ -27,8 +45,13 @@ export class GetByMahasiswaIdResDto {
 
   @ApiProperty({ type: PickedTopikBimbingan })
   topik: PickedTopikBimbingan;
+
+  @ApiProperty({ enum: BimbinganStatus })
+  status: BimbinganStatus;
 }
 
+class BerkasWithoutId extends OmitType(BerkasBimbingan, ["id"] as const) {}
+
 export class CreateBimbinganReqDto {
   @ApiProperty({ type: Date })
   @IsDateString()
@@ -42,19 +65,45 @@ export class CreateBimbinganReqDto {
   @IsString()
   todo: string;
 
-  // TODO file upload
-
   @ApiProperty({ type: Date })
   @IsDateString()
+  @IsOptional()
   bimbinganBerikutnya: string;
+
+  @ApiProperty({ type: [BerkasWithoutId] })
+  @ValidateNested({ each: true })
+  @Type(() => BerkasWithoutId)
+  @IsDefined()
+  berkas: BerkasWithoutId[];
 }
 
 export class CreateBimbinganResDto {
   @ApiProperty()
-  message: string;
+  id: string;
 }
 
 export class ByMhsIdDto {
   @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
   mahasiswaId: string;
 }
+
+export class UpdateStatusDto {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  bimbinganId: string;
+
+  @ApiProperty()
+  @IsBoolean()
+  status: boolean;
+}
+
+export class UpdateStatusResDto {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  id: string;
+}
+
+export class GetByBimbinganIdResDto extends IntersectionType(
+  OmitType(Bimbingan, ["pendaftaran"] as const),
+  PickType(PendaftaranTesis, ["jalurPilihan"] as const),
+) {}
diff --git a/src/bimbingan/bimbingan.module.ts b/src/bimbingan/bimbingan.module.ts
index 7a8482ad25d69326a57905cfa0ab927048f7caed..30174c69ff5a33b4a5caa742095ef9f830c5eb8d 100644
--- a/src/bimbingan/bimbingan.module.ts
+++ b/src/bimbingan/bimbingan.module.ts
@@ -6,6 +6,7 @@ import { Bimbingan } from "src/entities/bimbingan.entity";
 import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
 import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
+import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
 
 @Module({
   imports: [
@@ -14,9 +15,11 @@ import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
       PendaftaranTesis,
       Konfigurasi,
       DosenBimbingan,
+      BerkasBimbingan,
     ]),
   ],
   controllers: [BimbinganController],
   providers: [BimbinganService],
+  exports: [BimbinganService],
 })
 export class BimbinganModule {}
diff --git a/src/bimbingan/bimbingan.service.ts b/src/bimbingan/bimbingan.service.ts
index b1e047a39b5ab5a6b06e5db6206dbc0af3159a28..52c1ba28e98f9c71aad2d88e5ebeafb8d1cb1d40 100644
--- a/src/bimbingan/bimbingan.service.ts
+++ b/src/bimbingan/bimbingan.service.ts
@@ -7,7 +7,7 @@ import {
 import { InjectRepository } from "@nestjs/typeorm";
 import * as dayjs from "dayjs";
 import { AuthDto } from "src/auth/auth.dto";
-import { Bimbingan } from "src/entities/bimbingan.entity";
+import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity";
 import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
 import {
@@ -19,8 +19,12 @@ import { Repository } from "typeorm";
 import {
   CreateBimbinganReqDto,
   CreateBimbinganResDto,
+  GetByBimbinganIdResDto,
   GetByMahasiswaIdResDto,
+  UpdateStatusDto,
+  UpdateStatusResDto,
 } from "./bimbingan.dto";
+import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
 
 @Injectable()
 export class BimbinganService {
@@ -33,6 +37,8 @@ export class BimbinganService {
     private konfigurasiRepository: Repository<Konfigurasi>,
     @InjectRepository(DosenBimbingan)
     private dosenBimbinganRepository: Repository<DosenBimbingan>,
+    @InjectRepository(BerkasBimbingan)
+    private berkasBimbinganRepository: Repository<BerkasBimbingan>,
   ) {}
 
   async getByMahasiswaId(
@@ -88,8 +94,13 @@ export class BimbinganService {
           id: pendaftaran.id,
         },
       },
+      relations: {
+        berkas: true,
+      },
     });
 
+    const status = await this.getBimbinganStatus(pendaftaran);
+
     return {
       bimbingan,
       mahasiswa: {
@@ -99,10 +110,10 @@ export class BimbinganService {
         jalurPilihan: pendaftaran.jalurPilihan,
       },
       topik: pendaftaran.topik,
+      status,
     };
   }
 
-  // TODO handle file upload
   async create(
     mahasiswaId: string,
     createDto: CreateBimbinganReqDto,
@@ -133,8 +144,6 @@ export class BimbinganService {
       );
     }
 
-    console.log(dayjs(createDto.waktuBimbingan).toDate());
-
     if (dayjs(createDto.waktuBimbingan).isAfter(dayjs(new Date()).endOf("d")))
       throw new BadRequestException(
         "Tanggal bimbingan yang dimasukkan tidak boleh melebihi tanggal hari ini",
@@ -149,14 +158,116 @@ export class BimbinganService {
         "Bimbingan berikutnya harus setelah bimbingan yang dimasukkan",
       );
 
+    const berkasBimbingan = createDto.berkas.map((berkas) =>
+      this.berkasBimbinganRepository.create(berkas),
+    );
+
     const createdBimbinganLog = this.bimbinganRepository.create({
-      ...createDto,
-      berkasLinks: [],
+      waktuBimbingan: createDto.waktuBimbingan,
+      laporanKemajuan: createDto.laporanKemajuan,
+      todo: createDto.todo,
+      bimbinganBerikutnya: createDto.bimbinganBerikutnya,
+      berkas: berkasBimbingan,
       pendaftaran,
     });
 
     await this.bimbinganRepository.save(createdBimbinganLog);
 
-    return { message: "Successfully added log" };
+    return { id: createdBimbinganLog.id };
+  }
+
+  async updateStatus(
+    user: AuthDto,
+    dto: UpdateStatusDto,
+  ): Promise<UpdateStatusResDto> {
+    const bimbingan = await this.getByBimbinganId(user, dto.bimbinganId);
+
+    await this.bimbinganRepository.update(bimbingan.id, {
+      disahkan: dto.status,
+    });
+
+    return {
+      id: bimbingan.id,
+    };
+  }
+
+  async getByBimbinganId(
+    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,
+      })
+      .leftJoinAndSelect("pendaftaran.mahasiswa", "mahasiswa")
+      .where("bimbingan.id = :id", { id: bimbinganId });
+    const bimbingan = await bimbinganQuery.getOne();
+
+    if (!bimbingan) {
+      throw new NotFoundException("Bimbingan tidak ditemukan");
+    }
+
+    if (
+      !user.roles.includes(RoleEnum.ADMIN) &&
+      !bimbingan.pendaftaran.dosenBimbingan
+        .map((d) => d.dosen.id)
+        .includes(user.id)
+    ) {
+      throw new ForbiddenException();
+    }
+
+    return {
+      id: bimbingan.id,
+      waktuBimbingan: bimbingan.waktuBimbingan,
+      laporanKemajuan: bimbingan.laporanKemajuan,
+      todo: bimbingan.todo,
+      bimbinganBerikutnya: bimbingan.bimbinganBerikutnya,
+      disahkan: bimbingan.disahkan,
+      berkas: bimbingan.berkas,
+      jalurPilihan: bimbingan.pendaftaran.jalurPilihan,
+    };
+  }
+
+  async getBimbinganStatus(
+    pendaftaran: PendaftaranTesis,
+  ): Promise<BimbinganStatus> {
+    const lastBimbinganQuery = this.bimbinganRepository
+      .createQueryBuilder("bimbingan")
+      .where("bimbingan.pendaftaranId = :pendaftaranId", {
+        pendaftaranId: pendaftaran.id,
+      })
+      .orderBy("bimbingan.waktuBimbingan", "DESC")
+      .limit(1);
+
+    const lastBimbingan = await lastBimbinganQuery.getOne();
+
+    if (!lastBimbingan) {
+      return dayjs(pendaftaran.waktuPengiriman).isBefore(
+        dayjs().subtract(3, "month"),
+      )
+        ? BimbinganStatus.TERKENDALA
+        : BimbinganStatus.LANCAR;
+    }
+
+    if (
+      dayjs(lastBimbingan.waktuBimbingan).isBefore(dayjs().subtract(3, "month"))
+    ) {
+      return BimbinganStatus.TERKENDALA;
+    } else if (
+      dayjs(lastBimbingan.waktuBimbingan).isBefore(dayjs().subtract(1, "month"))
+    ) {
+      return BimbinganStatus.BUTUH_BIMBINGAN;
+    } else {
+      return BimbinganStatus.LANCAR;
+    }
   }
 }
diff --git a/src/dashboard/dashboard.controller.ts b/src/dashboard/dashboard.controller.ts
index 0da85958efd66d633cb5051480213703629e9cd1..31a3a4cc1dc88afe12ba4b05e16bc015fd2a41e9 100644
--- a/src/dashboard/dashboard.controller.ts
+++ b/src/dashboard/dashboard.controller.ts
@@ -1,4 +1,4 @@
-import { Controller, Get, Req, UseGuards } from "@nestjs/common";
+import { Controller, Get, Query, Req, UseGuards } from "@nestjs/common";
 import { DashboardService } from "./dashboard.service";
 import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
 import { RolesGuard } from "src/middlewares/roles.guard";
@@ -6,7 +6,12 @@ import { RoleEnum } from "src/entities/pengguna.entity";
 import { Roles } from "src/middlewares/roles.decorator";
 import { AuthDto } from "src/auth/auth.dto";
 import { Request } from "express";
-import { DashboardDto, JalurStatisticDto } from "./dashboard.dto";
+import {
+  DashboardDto,
+  DashboardMahasiswaResDto,
+  GetDashboardDosbimQueryDto,
+  JalurStatisticDto,
+} from "./dashboard.dto";
 import {
   ApiBearerAuth,
   ApiCookieAuth,
@@ -25,8 +30,14 @@ export class DashboardController {
   @ApiOkResponse({ type: [DashboardDto] })
   @Roles(RoleEnum.S2_PEMBIMBING)
   @Get("/dosbim")
-  async findByPenerimaId(@Req() request: Request): Promise<DashboardDto[]> {
-    return this.dashboardService.findByPenerimaId((request.user as AuthDto).id);
+  async findByPenerimaId(
+    @Req() request: Request,
+    @Query() query: GetDashboardDosbimQueryDto,
+  ): Promise<DashboardDto[]> {
+    return this.dashboardService.findByDosenId(
+      (request.user as AuthDto).id,
+      query.search,
+    );
   }
 
   @ApiOkResponse({ type: [JalurStatisticDto] })
@@ -37,4 +48,16 @@ export class DashboardController {
       (request.user as AuthDto).id,
     );
   }
+
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.S2_MAHASISWA)
+  @ApiOkResponse({ type: DashboardMahasiswaResDto })
+  @Get("/mahasiswa")
+  async getDashboardMahasiswa(
+    @Req() request: Request,
+  ): Promise<DashboardMahasiswaResDto> {
+    return this.dashboardService.getDashboardMahasiswa(
+      (request.user as AuthDto).id,
+    );
+  }
 }
diff --git a/src/dashboard/dashboard.dto.ts b/src/dashboard/dashboard.dto.ts
index 39ec3e41504d87deb6faf1cb44e35376f63ad8ad..8d279937d0616eb007400145e622316c5d4b9012 100644
--- a/src/dashboard/dashboard.dto.ts
+++ b/src/dashboard/dashboard.dto.ts
@@ -1,15 +1,57 @@
-import { ApiProperty, PickType } from "@nestjs/swagger";
-import { JalurEnum } from "../entities/pendaftaranTesis.entity";
+import {
+  ApiProperty,
+  ApiPropertyOptional,
+  OmitType,
+  PickType,
+} from "@nestjs/swagger";
+import {
+  JalurEnum,
+  PendaftaranTesis,
+} from "../entities/pendaftaranTesis.entity";
 import { Topik } from "src/entities/topik.entity";
 import { Pengguna } from "src/entities/pengguna.entity";
+import { Bimbingan } from "src/entities/bimbingan.entity";
+import { PendaftaranSidsem } from "src/entities/pendaftaranSidsem";
+import { IsOptional } from "class-validator";
+import { BimbinganStatus } from "src/entities/bimbingan.entity";
 
 class PickedTopikDashboard extends PickType(Topik, ["id", "judul"] as const) {}
 class PickedMhsDashboard extends PickType(Pengguna, [
   "id",
   "nama",
   "nim",
+  "email",
 ] as const) {}
 
+class OmittedTopikMhsDashboard extends OmitType(Topik, ["pengaju"] as const) {}
+
+class NoEmailUserDashboard extends OmitType(PickedMhsDashboard, [
+  "email",
+] as const) {}
+
+export class NoNIMUserDashboard extends OmitType(PickedMhsDashboard, [
+  "nim",
+] as const) {}
+
+class OmittedPendaftaranTesisMhsDashboard extends OmitType(PendaftaranTesis, [
+  "mahasiswa",
+  "topik",
+  "penerima",
+] as const) {
+  @ApiProperty()
+  topik: OmittedTopikMhsDashboard;
+
+  @ApiProperty()
+  penerima: NoEmailUserDashboard;
+}
+
+class SidsemWithPenguji extends OmitType(PendaftaranSidsem, [
+  "penguji",
+] as const) {
+  @ApiProperty({ type: [NoNIMUserDashboard] })
+  penguji: NoNIMUserDashboard[];
+}
+
 export class DashboardDto {
   @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
   id: string;
@@ -17,14 +59,14 @@ export class DashboardDto {
   @ApiProperty({ enum: JalurEnum })
   jalurPilihan: JalurEnum;
 
-  @ApiProperty()
-  status: string;
+  @ApiProperty({ enum: BimbinganStatus })
+  status: BimbinganStatus;
 
   @ApiProperty()
   topik: PickedTopikDashboard;
 
   @ApiProperty()
-  mahasiswa: PickedMhsDashboard;
+  mahasiswa: NoEmailUserDashboard;
 }
 
 export class JalurStatisticDto {
@@ -34,3 +76,41 @@ export class JalurStatisticDto {
   @ApiProperty()
   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()
+  search: string;
+}
diff --git a/src/dashboard/dashboard.module.ts b/src/dashboard/dashboard.module.ts
index e9f3d361fb9237419b3ba47e7ac835a3d0fab6f6..8f8f447d98f6ce5c3d40de228881aa244d364bba 100644
--- a/src/dashboard/dashboard.module.ts
+++ b/src/dashboard/dashboard.module.ts
@@ -6,10 +6,23 @@ import { PendaftaranTesis } from "../entities/pendaftaranTesis.entity";
 import { Pengguna } from "../entities/pengguna.entity";
 import { Topik } from "../entities/topik.entity";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
+import { Bimbingan } from "src/entities/bimbingan.entity";
+import { PendaftaranSidsem } from "src/entities/pendaftaranSidsem";
+import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
+import { BimbinganModule } from "src/bimbingan/bimbingan.module";
 
 @Module({
   imports: [
-    TypeOrmModule.forFeature([PendaftaranTesis, Pengguna, Topik, Konfigurasi]),
+    TypeOrmModule.forFeature([
+      PendaftaranTesis,
+      Pengguna,
+      Topik,
+      Konfigurasi,
+      Bimbingan,
+      PendaftaranSidsem,
+      DosenBimbingan,
+    ]),
+    BimbinganModule,
   ],
   controllers: [DashboardController],
   providers: [DashboardService],
diff --git a/src/dashboard/dashboard.service.ts b/src/dashboard/dashboard.service.ts
index 19b4ff4b924bfad717bff5498eab582daa5c91ae..c2bfcb12e4e7c8bbcbfdfacf6b77a224c475a5b1 100644
--- a/src/dashboard/dashboard.service.ts
+++ b/src/dashboard/dashboard.service.ts
@@ -1,13 +1,25 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
 import { InjectRepository } from "@nestjs/typeorm";
-import { Repository } from "typeorm";
+import { Brackets, Repository } from "typeorm";
 import {
   PendaftaranTesis,
   RegStatus,
 } from "../entities/pendaftaranTesis.entity";
 import { Pengguna } from "../entities/pengguna.entity";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
-import { DashboardDto, JalurStatisticDto } from "./dashboard.dto";
+import { Bimbingan } from "src/entities/bimbingan.entity";
+import {
+  DashboardDto,
+  DashboardMahasiswaResDto,
+  JalurStatisticDto,
+  NoNIMUserDashboard,
+} from "./dashboard.dto";
+import {
+  PendaftaranSidsem,
+  TipeSidsemEnum,
+} from "src/entities/pendaftaranSidsem";
+import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
+import { BimbinganService } from "src/bimbingan/bimbingan.service";
 
 @Injectable()
 export class DashboardService {
@@ -18,6 +30,13 @@ export class DashboardService {
     private penggunaRepository: Repository<Pengguna>,
     @InjectRepository(Konfigurasi)
     private konfigurasiRepository: Repository<Konfigurasi>,
+    @InjectRepository(Bimbingan)
+    private bimbinganRepository: Repository<Bimbingan>,
+    @InjectRepository(PendaftaranSidsem)
+    private pendaftaranSidsemRepository: Repository<PendaftaranSidsem>,
+    @InjectRepository(DosenBimbingan)
+    private dosenBimbinganRepository: Repository<DosenBimbingan>,
+    private bimbinganService: BimbinganService,
   ) {}
 
   async findAll(): Promise<PendaftaranTesis[]> {
@@ -26,73 +45,229 @@ export class DashboardService {
     });
   }
 
-  async findByPenerimaId(penerimaId: string): Promise<DashboardDto[]> {
+  async findByDosenId(
+    dosenId: string,
+    search?: string,
+  ): Promise<DashboardDto[]> {
     const currentPeriode = await this.konfigurasiRepository.findOne({
       where: { key: process.env.KONF_PERIODE_KEY },
     });
 
-    const pendaftaranTesis = await this.pendaftaranTesisRepository.find({
-      where: {
-        status: RegStatus.APPROVED,
-        penerima: {
-          id: penerimaId,
+    if (!currentPeriode) {
+      throw new BadRequestException("Periode belum dikonfigurasi");
+    }
+
+    let pendaftaranTesisQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pendaftaranTesis")
+      .leftJoinAndSelect("pendaftaranTesis.mahasiswa", "mahasiswa")
+      .leftJoinAndSelect("pendaftaranTesis.topik", "topik")
+      .innerJoin(
+        "pendaftaranTesis.dosenBimbingan",
+        "dosenBimbingan",
+        "dosenBimbingan.idDosen = :dosenId",
+        {
+          dosenId,
         },
+      )
+      .andWhere("pendaftaranTesis.status = :status", {
+        status: RegStatus.APPROVED,
+      })
+      .andWhere("topik.periode = :periode", { periode: currentPeriode.value });
+
+    if (search) {
+      pendaftaranTesisQuery = pendaftaranTesisQuery.andWhere(
+        new Brackets((qb) => {
+          qb.where("mahasiswa.nama ILIKE :search", {
+            search: `%${search}%`,
+          }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` });
+        }),
+      );
+    }
+    const pendaftaranTesis = await pendaftaranTesisQuery.getMany();
+
+    const statusMap = await Promise.all(
+      pendaftaranTesis.map(async (pendaftaran) => {
+        return await this.bimbinganService.getBimbinganStatus(pendaftaran);
+      }),
+    );
+
+    return pendaftaranTesis.map((pendaftaran, index) => {
+      return {
+        id: pendaftaran.id,
+        jalurPilihan: pendaftaran.jalurPilihan,
+        status: statusMap[index],
         topik: {
-          periode: currentPeriode.value,
+          id: pendaftaran.topik.id,
+          judul: pendaftaran.topik.judul,
         },
-      },
-      relations: {
-        mahasiswa: true,
-        topik: true,
-      },
+        mahasiswa: {
+          id: pendaftaran.mahasiswa.id,
+          nama: pendaftaran.mahasiswa.nama,
+          nim: pendaftaran.mahasiswa.nim,
+        },
+      };
     });
-
-    return pendaftaranTesis.map((pendaftaran) => ({
-      id: pendaftaran.id,
-      jalurPilihan: pendaftaran.jalurPilihan,
-      status: "LANCAR",
-      topik: {
-        id: pendaftaran.topik.id,
-        judul: pendaftaran.topik.judul,
-      },
-      mahasiswa: {
-        id: pendaftaran.mahasiswa.id,
-        nama: pendaftaran.mahasiswa.nama,
-        nim: pendaftaran.mahasiswa.nim,
-      },
-    }));
   }
 
   async getStatisticsByJalurPilihan(
-    penerimaId: string,
+    dosenId: string,
   ): Promise<JalurStatisticDto[]> {
-    const [currentPeriode, penerima] = await Promise.all([
+    const [currentPeriode, dosen] = await Promise.all([
       this.konfigurasiRepository.findOne({
         where: { key: process.env.KONF_PERIODE_KEY },
       }),
       this.penggunaRepository.findOne({
-        where: { id: penerimaId },
+        where: { id: dosenId },
       }),
     ]);
 
-    if (!penerima) {
-      return [];
+    if (!dosen) {
+      throw new BadRequestException("Dosen tidak ditemukan");
     }
+
     const statistics = await this.pendaftaranTesisRepository
       .createQueryBuilder("pendaftaranTesis")
       .select("pendaftaranTesis.jalurPilihan", "jalurPilihan")
       .addSelect("COUNT(*)", "count")
-      .leftJoin("pendaftaranTesis.topik", "topik")
-      .where("pendaftaranTesis.penerima = :penerima", { penerima: penerima.id })
+      .leftJoin("pendaftaranTesis.topik", "topik", "topik.periode = :periode", {
+        periode: currentPeriode.value,
+      })
+      .innerJoin(
+        "pendaftaranTesis.dosenBimbingan",
+        "dosenBimbingan",
+        "dosenBimbingan.idDosen = :dosenId",
+        {
+          dosenId,
+        },
+      )
       .andWhere("pendaftaranTesis.status = :status", {
         status: RegStatus.APPROVED,
       })
       .groupBy("pendaftaranTesis.jalurPilihan")
-      .addGroupBy("topik.id")
-      .having("topik.periode = :periode", {
-        periode: currentPeriode.value,
-      })
       .getRawMany();
+
     return statistics as JalurStatisticDto[];
   }
+
+  async getDashboardMahasiswa(
+    mahasiswaId: string,
+  ): Promise<DashboardMahasiswaResDto> {
+    const currentPeriode = await this.konfigurasiRepository.findOne({
+      where: { key: process.env.KONF_PERIODE_KEY },
+    });
+
+    const mahasiswaQuery = this.penggunaRepository
+      .createQueryBuilder("pengguna")
+      .select([
+        "pengguna.id",
+        "pengguna.nama",
+        "pengguna.email",
+        "pengguna.nim",
+      ])
+      .where("pengguna.id = :id", { id: mahasiswaId });
+    const pendaftaranTesisQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pendaftaranTesis")
+      .select([
+        "pendaftaranTesis.id",
+        "pendaftaranTesis.jalurPilihan",
+        "pendaftaranTesis.waktuPengiriman",
+        "pendaftaranTesis.jadwalInterview",
+        "pendaftaranTesis.waktuKeputusan",
+        "pendaftaranTesis.status",
+        "penerima.id",
+        "penerima.nama",
+        "penerima.email",
+      ])
+      .leftJoin("pendaftaranTesis.mahasiswa", "mahasiswa")
+      .leftJoinAndSelect("pendaftaranTesis.topik", "topik")
+      .leftJoin("pendaftaranTesis.penerima", "penerima")
+      .where("mahasiswa.id = :id", { id: mahasiswaId })
+      .andWhere("topik.periode = :periode", { periode: currentPeriode.value })
+      .orderBy("pendaftaranTesis.waktuPengiriman", "DESC");
+
+    const [mahasiswa, pendaftaranTesis] = await Promise.all([
+      mahasiswaQuery.getOne(),
+      pendaftaranTesisQuery.getOne(),
+    ]);
+
+    let dosenBimbingan: DosenBimbingan[] = [];
+    let bimbingan: Bimbingan[] = [];
+    let seminarSatu: PendaftaranSidsem | null = null;
+    let seminarDua: PendaftaranSidsem | null = null;
+    let sidang: PendaftaranSidsem | null = null;
+
+    if (pendaftaranTesis) {
+      const dosenBimbinganQuery = this.dosenBimbinganRepository
+        .createQueryBuilder("dosenBimbingan")
+        .select(["dosen.id", "dosen.nama", "dosen.email"])
+        .leftJoin("dosenBimbingan.dosen", "dosen")
+        .where("dosenBimbingan.idPendaftaran = :id", {
+          id: pendaftaranTesis.id,
+        });
+      const bimbinganQuery = this.bimbinganRepository
+        .createQueryBuilder("bimbingan")
+        .leftJoinAndSelect("bimbingan.berkas", "berkas")
+        .where("bimbingan.pendaftaranId = :id", {
+          id: pendaftaranTesis.id,
+        });
+      const [seminarSatuQuery, seminarDuaQuery, sidangQuery] = Object.values(
+        TipeSidsemEnum,
+      ).map((tipe) => {
+        let temp = this.pendaftaranSidsemRepository
+          .createQueryBuilder("pendaftaranSidsem")
+          .leftJoinAndSelect("pendaftaranSidsem.ruangan", "ruangan")
+          .where("pendaftaranSidsem.pendaftaranTesisId = :id", {
+            id: pendaftaranTesis.id,
+          })
+          .andWhere("pendaftaranSidsem.tipe = :tipe", {
+            tipe,
+          })
+          .andWhere("NOT pendaftaranSidsem.ditolak");
+
+        if (tipe !== TipeSidsemEnum.SEMINAR_1) {
+          temp = temp
+            .leftJoinAndSelect("pendaftaranSidsem.penguji", "penguji")
+            .leftJoinAndSelect("penguji.dosen", "dosen");
+        }
+
+        return temp;
+      });
+
+      [dosenBimbingan, bimbingan, seminarSatu, seminarDua, sidang] =
+        await Promise.all([
+          dosenBimbinganQuery.getMany(),
+          bimbinganQuery.getMany(),
+          seminarSatuQuery.getOne(),
+          seminarDuaQuery.getOne(),
+          sidangQuery.getOne(),
+        ]);
+    }
+
+    return {
+      mahasiswa,
+      pendaftaranTesis,
+      dosenBimbingan:
+        dosenBimbingan.length > 0
+          ? (dosenBimbingan as any as NoNIMUserDashboard[])
+          : [pendaftaranTesis.penerima],
+      bimbingan,
+      seminarSatu,
+      seminarDua: {
+        ...seminarDua,
+        penguji: seminarDua?.penguji.map((p) => ({
+          id: p.dosen.id,
+          nama: p.dosen.nama,
+          email: p.dosen.email,
+        })),
+      },
+      sidang: {
+        ...sidang,
+        penguji: sidang?.penguji.map((p) => ({
+          id: p.dosen.id,
+          nama: p.dosen.nama,
+          email: p.dosen.email,
+        })),
+      },
+    };
+  }
 }
diff --git a/src/dosen-bimbingan/dosen-bimbingan.controller.ts b/src/dosen-bimbingan/dosen-bimbingan.controller.ts
index ef0b6acae18dc2d24c5e25dbdf88eaa8158a46df..81a31679c5443f03c6469b478be79566aa2e80b0 100644
--- a/src/dosen-bimbingan/dosen-bimbingan.controller.ts
+++ b/src/dosen-bimbingan/dosen-bimbingan.controller.ts
@@ -1,30 +1,16 @@
-import {
-  Body,
-  Controller,
-  Delete,
-  Get,
-  NotFoundException,
-  Put,
-  Query,
-  UseGuards,
-} from "@nestjs/common";
+import { Controller, Get, UseGuards } from "@nestjs/common";
 import {
   ApiBearerAuth,
   ApiCookieAuth,
   ApiOkResponse,
+  ApiOperation,
   ApiTags,
 } from "@nestjs/swagger";
 import { RoleEnum } from "src/entities/pengguna.entity";
 import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
 import { Roles } from "src/middlewares/roles.decorator";
 import { RolesGuard } from "src/middlewares/roles.guard";
-import {
-  DosbimOptQueryDto,
-  DosbimQueryDto,
-  GetDosbimResDto,
-  SuccessResDto,
-  UpdateDosbimDto,
-} from "./dosen-bimbingan.dto";
+import { GetDosbimResDto } from "./dosen-bimbingan.dto";
 import { DosenBimbinganService } from "./dosen-bimbingan.service";
 
 @ApiTags("Dosen Bimbingan")
@@ -32,49 +18,17 @@ import { DosenBimbinganService } from "./dosen-bimbingan.service";
 @ApiBearerAuth()
 @Controller("dosen-bimbingan")
 @UseGuards(CustomAuthGuard, RolesGuard)
+@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_MAHASISWA)
 export class DosenBimbinganController {
   constructor(private readonly dosbimService: DosenBimbinganService) {}
 
   @ApiOkResponse({ type: [GetDosbimResDto] })
-  @Roles(
-    RoleEnum.ADMIN,
-    RoleEnum.S2_TIM_TESIS,
-    RoleEnum.S2_MAHASISWA,
-    RoleEnum.S2_TIM_TESIS,
-  )
+  @ApiOperation({
+    summary:
+      "Get all available dosen bimbingan. Roles: ADMIN, S2_TIM_TESIS, S2_MAHASISWA",
+  })
   @Get()
-  async get(@Query() query: DosbimOptQueryDto) {
-    if (!query.regId) return await this.dosbimService.getAll();
-
-    const res = await this.dosbimService.findByRegId(query.regId);
-    const mappedRes: GetDosbimResDto[] = res.map((r) => r.dosen);
-    return mappedRes;
-  }
-
-  @ApiOkResponse({ type: SuccessResDto })
-  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
-  @Put()
-  async updateByRegId(
-    @Query() query: DosbimQueryDto,
-    @Body() body: UpdateDosbimDto,
-  ): Promise<SuccessResDto> {
-    await this.dosbimService.updateByRegId(query.regId, body.dosbimIds);
-
-    return {
-      status: "ok",
-    };
-  }
-
-  @ApiOkResponse({ type: SuccessResDto })
-  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
-  @Delete()
-  async deleteByRegId(@Query() query: DosbimQueryDto): Promise<SuccessResDto> {
-    const res = await this.dosbimService.removeByRegId(query.regId);
-
-    if (!res.affected) throw new NotFoundException();
-
-    return {
-      status: "ok",
-    };
+  async get() {
+    return await this.dosbimService.getAll();
   }
 }
diff --git a/src/dosen-bimbingan/dosen-bimbingan.dto.ts b/src/dosen-bimbingan/dosen-bimbingan.dto.ts
index 11d56b1135d145102b6ac69a56474ae66975dac5..23c823e7389a759b7d532c02543e736d5bbaf0d4 100644
--- a/src/dosen-bimbingan/dosen-bimbingan.dto.ts
+++ b/src/dosen-bimbingan/dosen-bimbingan.dto.ts
@@ -1,12 +1,6 @@
-import {
-  ArrayMaxSize,
-  IsArray,
-  IsUUID,
-  ArrayMinSize,
-  ArrayUnique,
-  IsOptional,
-} from "@nestjs/class-validator";
-import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger";
+import { IsOptional } from "@nestjs/class-validator";
+import { ApiPropertyOptional, PickType } from "@nestjs/swagger";
+import { IsUUID } from "class-validator";
 import { Pengguna } from "src/entities/pengguna.entity";
 
 export class DosbimOptQueryDto {
@@ -16,30 +10,6 @@ export class DosbimOptQueryDto {
   regId?: string;
 }
 
-export class DosbimQueryDto {
-  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
-  @IsUUID()
-  regId: string;
-}
-
-export class UpdateDosbimDto {
-  @ApiProperty({
-    type: [String],
-    example: ["550e8400-e29b-41d4-a716-446655440000"],
-  })
-  @IsArray()
-  @IsUUID("all", { each: true })
-  @ArrayMinSize(1)
-  @ArrayMaxSize(3)
-  @ArrayUnique()
-  dosbimIds: string[];
-}
-
-export class SuccessResDto {
-  @ApiProperty()
-  status: string;
-}
-
 export class GetDosbimResDto extends PickType(Pengguna, [
   "id",
   "email",
diff --git a/src/dosen-bimbingan/dosen-bimbingan.service.ts b/src/dosen-bimbingan/dosen-bimbingan.service.ts
index 456364ee4bc51946474c2cd5e54e92fc26909fe8..88f0f9d159f8f0e4949c06a4c5bbcb7ad9a9643a 100644
--- a/src/dosen-bimbingan/dosen-bimbingan.service.ts
+++ b/src/dosen-bimbingan/dosen-bimbingan.service.ts
@@ -1,29 +1,13 @@
-import {
-  BadRequestException,
-  Injectable,
-  InternalServerErrorException,
-} from "@nestjs/common";
+import { Injectable } from "@nestjs/common";
 import { InjectRepository } from "@nestjs/typeorm";
-import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
-import {
-  PendaftaranTesis,
-  RegStatus,
-} from "src/entities/pendaftaranTesis.entity";
 import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
-import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
-import { ArrayContains, DataSource, Repository } from "typeorm";
+import { ArrayContains, Repository } from "typeorm";
 
 @Injectable()
 export class DosenBimbinganService {
   constructor(
-    @InjectRepository(DosenBimbingan)
-    private dosbimRepo: Repository<DosenBimbingan>,
     @InjectRepository(Pengguna)
     private penggunaRepo: Repository<Pengguna>,
-    @InjectRepository(PendaftaranTesis)
-    private pendaftaranRepo: Repository<PendaftaranTesis>,
-    private konfService: KonfigurasiService,
-    private dataSource: DataSource,
   ) {}
 
   async getAll() {
@@ -38,97 +22,4 @@ export class DosenBimbinganService {
       },
     });
   }
-
-  async findByRegId(regId: string) {
-    return await this.dosbimRepo.find({
-      select: {
-        id: true,
-        dosen: {
-          id: true,
-          nama: true,
-          email: true,
-        },
-      },
-      relations: {
-        dosen: true,
-      },
-      where: {
-        pendaftaran: {
-          id: regId,
-        },
-      },
-    });
-  }
-
-  async updateByRegId(regId: string, dosbimIds: string[]) {
-    const [reg, currPeriod] = await Promise.all([
-      this.pendaftaranRepo.findOne({
-        select: { id: true, status: true },
-        where: { id: regId },
-        relations: { topik: true },
-      }),
-      this.konfService.getKonfigurasiByKey(process.env.KONF_PERIODE_KEY),
-    ]);
-
-    if (!reg || reg.status !== RegStatus.APPROVED) {
-      throw new BadRequestException(
-        "Registrasi tidak ditemukan atau tidak disetujui.",
-      );
-    }
-
-    if (!currPeriod || currPeriod !== reg.topik.periode) {
-      throw new BadRequestException(
-        "Periode belum dikonfigurasi atau tidak sesuai dengan periode sekarang.",
-      );
-    }
-
-    for (const dosbimId of dosbimIds) {
-      const res = await this.penggunaRepo.findOne({
-        select: {
-          id: true,
-        },
-        where: {
-          id: dosbimId,
-          roles: ArrayContains([RoleEnum.S2_PEMBIMBING]),
-        },
-      });
-
-      if (!res) {
-        throw new BadRequestException("Invalid pembimbing id");
-      }
-    }
-
-    const queryRunner = this.dataSource.createQueryRunner();
-
-    await queryRunner.connect();
-    await queryRunner.startTransaction();
-    try {
-      await this.removeByRegId(regId);
-
-      for (const dosbimId of dosbimIds) {
-        await queryRunner.manager.getRepository(DosenBimbingan).insert({
-          idPendaftaran: regId,
-          idDosen: dosbimId,
-        });
-      }
-
-      await queryRunner.commitTransaction();
-    } catch (err) {
-      await queryRunner.rollbackTransaction();
-
-      throw new InternalServerErrorException();
-    } finally {
-      await queryRunner.release();
-    }
-  }
-
-  async removeByRegId(regId: string) {
-    return await this.dosbimRepo
-      .createQueryBuilder()
-      .delete()
-      .where("idPendaftaran = :idPendaftaran", {
-        idPendaftaran: regId,
-      })
-      .execute();
-  }
 }
diff --git a/src/entities/berkasBimbingan.entity.ts b/src/entities/berkasBimbingan.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..35b44ace3f7a70db3a1bcbd210c3a95be701c99a
--- /dev/null
+++ b/src/entities/berkasBimbingan.entity.ts
@@ -0,0 +1,26 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { Bimbingan } from "./bimbingan.entity";
+import { IsString, IsUrl } from "@nestjs/class-validator";
+import { ApiProperty } from "@nestjs/swagger";
+
+@Entity()
+export class BerkasBimbingan {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id, {
+    orphanedRowAction: "delete",
+  })
+  bimbingan: Bimbingan;
+
+  @Column({ type: "text" })
+  @IsString()
+  @ApiProperty()
+  nama: string;
+
+  @Column({ type: "text" })
+  @IsUrl()
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  url: string;
+}
diff --git a/src/entities/berkasSubmisiTugas.entity.ts b/src/entities/berkasSubmisiTugas.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e48d5e8d88f1449b767d3e88f1f9862e8e0f6560
--- /dev/null
+++ b/src/entities/berkasSubmisiTugas.entity.ts
@@ -0,0 +1,28 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { SubmisiTugas } from "./submisiTugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsString, IsUUID } from "@nestjs/class-validator";
+import { IsUrl } from "class-validator";
+
+@Entity()
+export class BerkasSubmisiTugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(() => SubmisiTugas, (submisi) => submisi.id, {
+    orphanedRowAction: "delete",
+  })
+  submisiTugas: SubmisiTugas;
+
+  @ApiProperty()
+  @IsString()
+  @Column({ type: "text" })
+  nama: string;
+
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  @IsUrl()
+  @Column({ type: "text" })
+  url: string;
+}
diff --git a/src/entities/berkasSubmisiTugas.ts b/src/entities/berkasSubmisiTugas.ts
deleted file mode 100644
index 9694eb7069548036483845915c5a61b8fc7e2a75..0000000000000000000000000000000000000000
--- a/src/entities/berkasSubmisiTugas.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { SubmisiTugas } from "./submisiTugas";
-
-@Entity()
-export class BerkasSubmisiTugas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => SubmisiTugas, (submisi) => submisi.id)
-  submisiTugas: SubmisiTugas;
-
-  @Column({ type: "text" })
-  nama: string;
-
-  @Column({ type: "text" })
-  url: string;
-}
diff --git a/src/entities/berkasTugas.entity.ts b/src/entities/berkasTugas.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c18bccfa9c839754ca8ed7bb0f5789f8841e3ff1
--- /dev/null
+++ b/src/entities/berkasTugas.entity.ts
@@ -0,0 +1,26 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { Tugas } from "./tugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsUrl, IsUUID } from "class-validator";
+import { IsString } from "@nestjs/class-validator";
+
+@Entity()
+export class BerkasTugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(() => Tugas, (tugas) => tugas.id, { orphanedRowAction: "delete" })
+  tugas: Tugas;
+
+  @ApiProperty()
+  @IsString()
+  @Column({ type: "text" })
+  nama: string;
+
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  @IsUrl()
+  @Column({ type: "text" })
+  url: string;
+}
diff --git a/src/entities/berkasTugas.ts b/src/entities/berkasTugas.ts
deleted file mode 100644
index 91cf3ec86a5b6d8f3b243e6d076e41bfa39adcbd..0000000000000000000000000000000000000000
--- a/src/entities/berkasTugas.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Tugas } from "./tugas.entity";
-
-@Entity()
-export class BerkasTugas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Tugas, (tugas) => tugas.id)
-  tugas: Tugas;
-
-  @Column({ type: "text" })
-  nama: string;
-
-  @Column({ type: "text" })
-  url: string;
-}
diff --git a/src/entities/bimbingan.entity.ts b/src/entities/bimbingan.entity.ts
index 2eeaf674286ff4b2f178dccdf19601088393d59c..a846e824d3d98389e6a2402efad351f49188f9fb 100644
--- a/src/entities/bimbingan.entity.ts
+++ b/src/entities/bimbingan.entity.ts
@@ -1,6 +1,19 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import {
+  Column,
+  Entity,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+} from "typeorm";
 import { PendaftaranTesis } from "./pendaftaranTesis.entity";
 import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { BerkasBimbingan } from "./berkasBimbingan.entity";
+
+export enum BimbinganStatus {
+  LANCAR = "LANCAR",
+  BUTUH_BIMBINGAN = "BUTUH_BIMBINGAN",
+  TERKENDALA = "TERKENDALA",
+}
 
 @Entity()
 export class Bimbingan {
@@ -24,10 +37,20 @@ export class Bimbingan {
   @Column({ type: "date", nullable: true })
   bimbinganBerikutnya: string;
 
-  @ApiProperty({ type: [String] })
-  @Column({ type: "simple-array" })
-  berkasLinks: string[];
+  @ApiProperty()
+  @Column({ type: "boolean", default: true })
+  disahkan: boolean;
 
   @ManyToOne(() => PendaftaranTesis, (pendaftaranTesis) => pendaftaranTesis.id)
   pendaftaran: PendaftaranTesis;
+
+  @ApiProperty({ type: [BerkasBimbingan] })
+  @OneToMany(
+    () => BerkasBimbingan,
+    (berkasBimbingan) => berkasBimbingan.bimbingan,
+    {
+      cascade: true,
+    },
+  )
+  berkas: BerkasBimbingan[];
 }
diff --git a/src/entities/kelas.entity.ts b/src/entities/kelas.entity.ts
index d1038130a50631f8e71d05d11af6584e77183685..7070b47519d5ed99f52281c454c32e04f01ef2f3 100644
--- a/src/entities/kelas.entity.ts
+++ b/src/entities/kelas.entity.ts
@@ -6,25 +6,38 @@ import {
   OneToMany,
   PrimaryGeneratedColumn,
 } from "typeorm";
-import { MataKuliah } from "./mataKuliah";
+import { MataKuliah } from "./mataKuliah.entity";
 import { ApiProperty } from "@nestjs/swagger";
-import { IsString, Length } from "class-validator";
 import { PengajarKelas } from "./pengajarKelas.entity";
-import { MahasiswaKelas } from "./mahasiswaKelas";
+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: "d290f1ee-6c54-4b01-90e6-d701748f0851" })
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
   @ApiProperty({ example: 1 })
   @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;
@@ -35,6 +48,9 @@ export class Kelas {
   @Column({ nullable: true })
   mataKuliahKode: string;
 
+  @ApiProperty({ example: "bg-blue-600/20" })
+  @IsString()
+  @MaxLength(24)
   @Column({ type: "varchar", length: 24 })
   warna: string;
 
@@ -43,4 +59,7 @@ export class Kelas {
 
   @OneToMany(() => MahasiswaKelas, (mahasiswa) => mahasiswa.kelas)
   mahasiswa: MahasiswaKelas[];
+
+  @OneToMany(() => Tugas, (tugas) => tugas.kelas)
+  tugas: Tugas[];
 }
diff --git a/src/entities/mahasiswaKelas.ts b/src/entities/mahasiswaKelas.entity.ts
similarity index 96%
rename from src/entities/mahasiswaKelas.ts
rename to src/entities/mahasiswaKelas.entity.ts
index 7d47e0abfa6461f68fc0fa11ef736601cd397c93..0753aa6f75f865913b7e42209248310948dc67c2 100644
--- a/src/entities/mahasiswaKelas.ts
+++ b/src/entities/mahasiswaKelas.entity.ts
@@ -28,5 +28,5 @@ export class MahasiswaKelas {
   mahasiswaId: string;
 
   @Column({ type: "real", nullable: true })
-  nilaiAkhir: number;
+  nilaiAkhir?: number;
 }
diff --git a/src/entities/mataKuliah.ts b/src/entities/mataKuliah.entity.ts
similarity index 70%
rename from src/entities/mataKuliah.ts
rename to src/entities/mataKuliah.entity.ts
index 6f78c692074b3a7c80123deac69c84643c1c7a11..9e1fed5d807a176b89288144623ec72575ef5e33 100644
--- a/src/entities/mataKuliah.ts
+++ b/src/entities/mataKuliah.entity.ts
@@ -1,6 +1,7 @@
 import { ApiProperty } from "@nestjs/swagger";
 import { IsString, Length, MaxLength } from "class-validator";
-import { Column, Entity, PrimaryColumn } from "typeorm";
+import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
+import { Kelas } from "./kelas.entity";
 
 @Entity()
 export class MataKuliah {
@@ -15,4 +16,7 @@ export class MataKuliah {
   @MaxLength(256)
   @Column({ type: "varchar", length: 256 })
   nama: string;
+
+  @OneToMany(() => Kelas, (kelas) => kelas.mataKuliah)
+  kelas: Kelas[];
 }
diff --git a/src/entities/pembimbingSeminar.entity.ts b/src/entities/pembimbingSeminar.entity.ts
deleted file mode 100644
index 024ebe5b57e6835441faa33d687137fb82565cbb..0000000000000000000000000000000000000000
--- a/src/entities/pembimbingSeminar.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Seminar } from "./seminar.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PembimbingSeminar {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Seminar, (seminar) => seminar.id)
-  seminar: Seminar;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
-}
diff --git a/src/entities/pendaftaranSidsem.ts b/src/entities/pendaftaranSidsem.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a733c9cd7aedf8825202a92c3b509e271486847f
--- /dev/null
+++ b/src/entities/pendaftaranSidsem.ts
@@ -0,0 +1,62 @@
+import {
+  Column,
+  Entity,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+} from "typeorm";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
+// import { Ruangan } from "./ruangan.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { PengujiSidsem } from "./pengujiSidsem.entity";
+
+export enum TipeSidsemEnum {
+  SEMINAR_1 = "SEMINAR_1",
+  SEMINAR_2 = "SEMINAR_2",
+  SIDANG = "SIDANG",
+}
+
+@Entity()
+export class PendaftaranSidsem {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ApiProperty({ enum: TipeSidsemEnum })
+  @Column({ type: "enum", enum: TipeSidsemEnum })
+  tipe: TipeSidsemEnum;
+
+  @ApiProperty()
+  @Column({ type: "boolean", default: false })
+  ditolak: boolean;
+
+  @ApiProperty()
+  @Column({ type: "boolean", nullable: true })
+  lulus: boolean;
+
+  @ApiProperty()
+  @Column({ type: "timestamptz", nullable: true })
+  waktuMulai: Date;
+
+  @ApiProperty()
+  @Column({ type: "timestamptz", nullable: true })
+  waktuSelesai: Date;
+
+  @ApiProperty()
+  @Column({ type: "text", nullable: true })
+  linkw2m: string;
+
+  @ManyToOne(() => PendaftaranTesis, (pendaftaranTesis) => pendaftaranTesis.id)
+  pendaftaranTesis: PendaftaranTesis;
+
+  // @ApiProperty({ type: Ruangan, nullable: true })
+  // @ManyToOne(() => Ruangan, (ruangan) => ruangan.id)
+  // ruangan: Ruangan;
+
+  @ApiProperty()
+  @Column({ type: "text", nullable: true })
+  ruangan: string;
+
+  @OneToMany(() => PengujiSidsem, (pengujiSidsem) => pengujiSidsem.sidsem)
+  penguji: PengujiSidsem[];
+}
diff --git a/src/entities/pendaftaranTesis.entity.ts b/src/entities/pendaftaranTesis.entity.ts
index 8750a05bea0955336a4d92425dc601874d4e29c8..637a05e71e7be84615568bcaef45f0adc2d14e2c 100644
--- a/src/entities/pendaftaranTesis.entity.ts
+++ b/src/entities/pendaftaranTesis.entity.ts
@@ -1,7 +1,14 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import {
+  Column,
+  Entity,
+  ManyToOne,
+  OneToMany,
+  PrimaryGeneratedColumn,
+} from "typeorm";
 import { Pengguna } from "./pengguna.entity";
 import { Topik } from "./topik.entity";
 import { ApiProperty } from "@nestjs/swagger";
+import { DosenBimbingan } from "./dosenBimbingan.entity";
 
 export enum RegStatus {
   NOT_ASSIGNED = "NOT_ASSIGNED",
@@ -48,12 +55,21 @@ export class PendaftaranTesis {
   @Column({ type: "enum", enum: RegStatus, default: RegStatus.NOT_ASSIGNED })
   status: RegStatus;
 
+  @ApiProperty({ type: Topik })
   @ManyToOne(() => Topik, (topik) => topik.id)
   topik: Topik;
 
+  @ApiProperty()
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
   mahasiswa: Pengguna;
 
+  @ApiProperty()
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
   penerima: Pengguna;
+
+  @OneToMany(
+    () => DosenBimbingan,
+    (dosenBimbingan) => dosenBimbingan.pendaftaran,
+  )
+  dosenBimbingan: DosenBimbingan[];
 }
diff --git a/src/entities/pengguna.entity.ts b/src/entities/pengguna.entity.ts
index db1cf81be9dad9a126077af2c9cb7cf2ded1a7e1..7e4899c2764d11b3871fa2d7e502aed033a04406 100644
--- a/src/entities/pengguna.entity.ts
+++ b/src/entities/pengguna.entity.ts
@@ -1,11 +1,14 @@
+import { IsString } from "@nestjs/class-validator";
 import {
   ApiHideProperty,
   ApiProperty,
   ApiPropertyOptional,
 } from "@nestjs/swagger";
 import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
-import { MahasiswaKelas } from "./mahasiswaKelas";
+import { MahasiswaKelas } from "./mahasiswaKelas.entity";
 import { PengajarKelas } from "./pengajarKelas.entity";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
+import { SubmisiTugas } from "./submisiTugas.entity";
 
 export enum RoleEnum {
   ADMIN = "ADMIN",
@@ -58,4 +61,14 @@ export class Pengguna {
 
   @OneToMany(() => PengajarKelas, (pengajarKelas) => pengajarKelas.pengajar)
   pengajarKelas: PengajarKelas[];
+  @ApiPropertyOptional()
+  @IsString()
+  @Column({ type: "text", nullable: true })
+  kontak: string;
+
+  @OneToMany(() => PendaftaranTesis, (pendaftaran) => pendaftaran.mahasiswa)
+  pendaftaranTesis: PendaftaranTesis[];
+
+  @OneToMany(() => SubmisiTugas, (submisiTugas) => submisiTugas.mahasiswa)
+  submisiTugas: SubmisiTugas[];
 }
diff --git a/src/entities/pengujiSidang.entity.ts b/src/entities/pengujiSidang.entity.ts
deleted file mode 100644
index dfcd36b5c2f5e06bebad302b3488aedff86338c7..0000000000000000000000000000000000000000
--- a/src/entities/pengujiSidang.entity.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Sidang } from "./sidang.entity";
-import { Pengguna } from "./pengguna.entity";
-
-@Entity()
-export class PengujiSidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Sidang, (sidang) => sidang.id)
-  sidang: Sidang;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  dosen: Pengguna;
-}
diff --git a/src/entities/pembimbingSidang.entity.ts b/src/entities/pengujiSidsem.entity.ts
similarity index 54%
rename from src/entities/pembimbingSidang.entity.ts
rename to src/entities/pengujiSidsem.entity.ts
index a3d9dbec55b4d3e94d7a1e6d8b14262b10752e3d..09aebe7c16acbb068ae8a26c341b02e996e6a7a3 100644
--- a/src/entities/pembimbingSidang.entity.ts
+++ b/src/entities/pengujiSidsem.entity.ts
@@ -1,14 +1,17 @@
 import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Sidang } from "./sidang.entity";
 import { Pengguna } from "./pengguna.entity";
+import { PendaftaranSidsem } from "./pendaftaranSidsem";
 
 @Entity()
-export class PembimbingSidang {
+export class PengujiSidsem {
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
-  @ManyToOne(() => Sidang, (sidang) => sidang.id)
-  sidang: Sidang;
+  @ManyToOne(
+    () => PendaftaranSidsem,
+    (pendaftaranSidsem) => pendaftaranSidsem.id,
+  )
+  sidsem: PendaftaranSidsem;
 
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
   dosen: Pengguna;
diff --git a/src/entities/rangeJadwalSeminar.entity.ts b/src/entities/rangeJadwalSeminar.entity.ts
deleted file mode 100644
index fa16bf749d40cdfeff7253f61bc098063a7348a3..0000000000000000000000000000000000000000
--- a/src/entities/rangeJadwalSeminar.entity.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
-
-@Entity()
-export class RangeJadwalSeminar {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
diff --git a/src/entities/rangeJadwalSidang.entity.ts b/src/entities/rangeJadwalSidang.entity.ts
deleted file mode 100644
index 6479aef875d9d7b23cd9db1c00317af5e60756cd..0000000000000000000000000000000000000000
--- a/src/entities/rangeJadwalSidang.entity.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
-
-@Entity()
-export class RangeJadwalSidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
diff --git a/src/entities/ruangan.entity.ts b/src/entities/ruangan.entity.ts
index 97df4c092f30bd329d8c3ece86d385bd0e6300c2..86dfaf804683751d50f397df3a55f00488dc0bfa 100644
--- a/src/entities/ruangan.entity.ts
+++ b/src/entities/ruangan.entity.ts
@@ -1,7 +1,13 @@
-import { Entity, PrimaryGeneratedColumn } from "typeorm";
+// import { ApiProperty } from "@nestjs/swagger";
+// import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
 
-@Entity()
-export class Ruangan {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-}
+// @Entity()
+// export class Ruangan {
+//   @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+//   @PrimaryGeneratedColumn("uuid")
+//   id: string;
+
+//   @ApiProperty()
+//   @Column({ type: "text" })
+//   nama: string;
+// }
diff --git a/src/entities/seminar.entity.ts b/src/entities/seminar.entity.ts
deleted file mode 100644
index 7b0e30b783be1e1401448e73f5fd078f7d4cdd0c..0000000000000000000000000000000000000000
--- a/src/entities/seminar.entity.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Pengguna } from "./pengguna.entity";
-import { RangeJadwalSeminar } from "./rangeJadwalSeminar.entity";
-import { Ruangan } from "./ruangan.entity";
-
-@Entity()
-export class Seminar {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-
-  @ManyToOne(
-    () => RangeJadwalSeminar,
-    (rangeJadwalSeminar) => rangeJadwalSeminar.id,
-  )
-  rangeJadwal: RangeJadwalSeminar;
-
-  @ManyToOne(() => Ruangan, (ruangan) => ruangan.id)
-  ruangan: Ruangan;
-}
diff --git a/src/entities/sidang.entity.ts b/src/entities/sidang.entity.ts
deleted file mode 100644
index c2dc57ecdca72af17a7496883e74a716f46dbf1c..0000000000000000000000000000000000000000
--- a/src/entities/sidang.entity.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Pengguna } from "./pengguna.entity";
-import { RangeJadwalSidang } from "./rangeJadwalSidang.entity";
-import { Ruangan } from "./ruangan.entity";
-
-@Entity()
-export class Sidang {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
-  mahasiswa: Pengguna;
-
-  @ManyToOne(
-    () => RangeJadwalSidang,
-    (rangeJadwalSidang) => rangeJadwalSidang.id,
-  )
-  rangeJadwal: RangeJadwalSidang;
-
-  @ManyToOne(() => Ruangan, (ruangan) => ruangan.id)
-  ruangan: Ruangan;
-}
diff --git a/src/entities/submisiTugas.ts b/src/entities/submisiTugas.entity.ts
similarity index 50%
rename from src/entities/submisiTugas.ts
rename to src/entities/submisiTugas.entity.ts
index ef0c8da1a854dc6b985b23b313a65eaba2aa7d22..be380d37d6c0b4dadf641f794735a343990e13b5 100644
--- a/src/entities/submisiTugas.ts
+++ b/src/entities/submisiTugas.entity.ts
@@ -1,34 +1,53 @@
 import {
   Column,
   Entity,
+  JoinColumn,
   ManyToOne,
   OneToMany,
   PrimaryGeneratedColumn,
 } from "typeorm";
 import { Pengguna } from "./pengguna.entity";
 import { Tugas } from "./tugas.entity";
-import { BerkasSubmisiTugas } from "./berkasSubmisiTugas";
+import { BerkasSubmisiTugas } from "./berkasSubmisiTugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsString, IsUUID } from "class-validator";
+import { IsBoolean } from "@nestjs/class-validator";
 
 @Entity()
 export class SubmisiTugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "mahasiswaId" })
   mahasiswa: Pengguna;
 
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  mahasiswaId: string;
+
+  @ApiProperty()
+  @IsString()
   @Column({ type: "text" })
   jawaban: string;
 
+  @ApiProperty({ description: "true means submitted, false means draft" })
+  @IsBoolean()
   @Column({ type: "boolean" })
   isSubmitted: boolean; // false means draft (saved), true means submitted
 
+  @ApiProperty({ type: [BerkasSubmisiTugas] })
   @OneToMany(
     () => BerkasSubmisiTugas,
     (berkasSubmisiTugas) => berkasSubmisiTugas.submisiTugas,
+    { cascade: true },
   )
   berkasSubmisiTugas: BerkasSubmisiTugas[];
 
+  @ApiProperty()
   @Column({
     type: "timestamptz",
     default: () => "CURRENT_TIMESTAMP",
@@ -37,5 +56,11 @@ export class SubmisiTugas {
   submittedAt: Date;
 
   @ManyToOne(() => Tugas, (tugas) => tugas.id)
+  @JoinColumn({ name: "tugasId" })
   tugas: Tugas;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  tugasId: string;
 }
diff --git a/src/entities/tugas.entity.ts b/src/entities/tugas.entity.ts
index 563bcb32a0d12b55e6e3a30232bea75a162a233d..2424b87973fd01fe854e40ca50079dbc40e0f4ee 100644
--- a/src/entities/tugas.entity.ts
+++ b/src/entities/tugas.entity.ts
@@ -1,46 +1,88 @@
 import {
   Column,
   Entity,
+  JoinColumn,
   ManyToOne,
   OneToMany,
   PrimaryGeneratedColumn,
 } from "typeorm";
 import { Kelas } from "./kelas.entity";
 import { Pengguna } from "./pengguna.entity";
-import { BerkasTugas } from "./berkasTugas";
+import { BerkasTugas } from "./berkasTugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsDateString, IsUUID, MaxLength } from "class-validator";
+import { IsString } from "@nestjs/class-validator";
+import { SubmisiTugas } from "./submisiTugas.entity";
 
 @Entity()
 export class Tugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "pembuatId" })
   pembuat: Pengguna;
 
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  pembuatId: string;
+
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "pengubahId" })
   pengubah: Pengguna;
 
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  pengubahId: string;
+
+  @ApiProperty()
+  @IsString()
+  @MaxLength(256)
   @Column({ type: "varchar", length: 256 })
   judul: string;
 
+  @ApiProperty()
+  @IsDateString()
   @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
   waktuMulai: Date;
 
+  @ApiProperty()
+  @IsDateString()
   @Column({ type: "timestamptz" })
   waktuSelesai: Date;
 
+  @ApiProperty()
+  @IsString()
   @Column({ type: "text" })
   deskripsi: string;
 
-  @OneToMany(() => BerkasTugas, (berkasTugas) => berkasTugas.tugas)
+  @ApiProperty({ type: [BerkasTugas] })
+  @OneToMany(() => BerkasTugas, (berkasTugas) => berkasTugas.tugas, {
+    cascade: true,
+  })
   berkasTugas: BerkasTugas[];
 
+  @ApiProperty()
   @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
   createdAt: Date;
 
+  @ApiProperty()
   @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
   updatedAt: Date;
 
   @ManyToOne(() => Kelas, (kelas) => kelas.id)
+  @JoinColumn({ name: "kelasId" })
   kelas: Kelas;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  kelasId: string;
+
+  @OneToMany(() => SubmisiTugas, (submisiTugas) => submisiTugas.tugas)
+  submisiTugas: SubmisiTugas[];
 }
diff --git a/src/kelas/kelas.constant.ts b/src/kelas/kelas.constant.ts
new file mode 100644
index 0000000000000000000000000000000000000000..050cbbca5914f9eb24c9e646e62e2629d047a51b
--- /dev/null
+++ b/src/kelas/kelas.constant.ts
@@ -0,0 +1,20 @@
+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
index 7469c69ce7f7dcc1a1abb67838ea8f68de67dfc8..1ba7c70bdae05a77341f67b1ce6a31d61248219a 100644
--- a/src/kelas/kelas.controller.ts
+++ b/src/kelas/kelas.controller.ts
@@ -4,37 +4,51 @@ import {
   Delete,
   ForbiddenException,
   Get,
+  Param,
   Post,
+  Put,
   Query,
   Req,
   UseGuards,
 } from "@nestjs/common";
 import {
   AssignKelasDto,
+  ByIdKelasDto,
   CreateKelasDto,
+  DeleteKelasDto,
+  GetKelasDetailRespDto,
   GetKelasQueryDto,
-  GetListKelasRespDto,
+  GetKelasRespDto,
+  GetNextNomorResDto,
+  IdKelasResDto,
   KodeRespDto,
   MessageResDto,
   UnassignKelasDto,
   UserKelasResDto,
+  UpdateKelasDto,
+  SearchQueryDto,
+  UpdateKelasPenggunaDto,
 } 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";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
+import { Kelas } from "src/entities/kelas.entity";
 
 @ApiTags("Kelas")
 @ApiBearerAuth()
@@ -44,8 +58,17 @@ import { MataKuliah } from "src/entities/mataKuliah";
 export class KelasController {
   constructor(private readonly kelasServ: KelasService) {}
 
-  @ApiOkResponse({ type: GetListKelasRespDto, isArray: true })
-  @Roles(RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA, RoleEnum.S2_TIM_TESIS)
+  @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;
@@ -65,17 +88,66 @@ export class KelasController {
       idMahasiswa = id;
     }
 
-    return await this.kelasServ.getListKelas(idMahasiswa, idPengajar);
+    return await this.kelasServ.getListKelas(
+      idMahasiswa,
+      idPengajar,
+      query.kodeMatkul,
+      query.search,
+    );
   }
 
-  @Roles(RoleEnum.S2_TIM_TESIS)
+  @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) {
+  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)
+  @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN)
   @Post("mata-kuliah")
   async createMataKuliah(@Body() body: MataKuliah) {
     return await this.kelasServ.createMatkul(body);
@@ -84,8 +156,10 @@ export class KelasController {
   @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN)
   @ApiOkResponse({ type: UserKelasResDto, isArray: true })
   @Get("/mahasiswa")
-  async getMahasiswa(): Promise<UserKelasResDto[]> {
-    return await this.kelasServ.getKelasPengguna("MAHASISWA");
+  async getMahasiswa(
+    @Query() query: SearchQueryDto,
+  ): Promise<UserKelasResDto[]> {
+    return await this.kelasServ.getKelasPengguna("MAHASISWA", query.search);
   }
 
   @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN)
@@ -111,8 +185,8 @@ export class KelasController {
   @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN)
   @ApiOkResponse({ type: UserKelasResDto, isArray: true })
   @Get("/dosen")
-  async getDosen(): Promise<UserKelasResDto[]> {
-    return await this.kelasServ.getKelasPengguna("DOSEN");
+  async getDosen(@Query() query: SearchQueryDto): Promise<UserKelasResDto[]> {
+    return await this.kelasServ.getKelasPengguna("DOSEN", query.search);
   }
 
   @Roles(RoleEnum.S2_TIM_TESIS, RoleEnum.ADMIN)
@@ -132,4 +206,109 @@ export class KelasController {
   ): Promise<MessageResDto> {
     return await this.kelasServ.unassignKelasDosen(body);
   }
+
+  @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,
+    );
+  }
+
+  @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 32f9add0a49e331b1e8917d1cab5e1041bea5c8c..3647164851b0a08f38fda3c651db4cc17587c086 100644
--- a/src/kelas/kelas.dto.ts
+++ b/src/kelas/kelas.dto.ts
@@ -1,12 +1,35 @@
-import { IsUUID } from "@nestjs/class-validator";
-import { ApiProperty, PickType } from "@nestjs/swagger";
-import { IsEnum } from "class-validator";
+import {
+  IsEnum,
+  IsOptional,
+  IsPositive,
+  IsUUID,
+} 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";
+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 {
@@ -15,20 +38,42 @@ export class GetKelasQueryDto {
     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 SearchQueryDto {
+  @ApiPropertyOptional({ example: "Intelegensi Buatan" })
+  @IsOptional()
+  search: string;
 }
 
-export class GetListKelasRespDto {
+export class ByIdKelasDto extends PickType(Kelas, ["id"] as const) {}
+
+export class GetKelasRespDto {
   @ApiProperty()
   id: string;
 
   @ApiProperty({ example: "K02" })
   nomor: string;
 
-  @ApiProperty({ example: "IF4031 Pengembangan Aplikasi Terdistribusi" })
-  mata_kuliah: string;
+  @ApiProperty({ example: "IF3270" })
+  kode_mata_kuliah: string;
+
+  @ApiProperty({ example: "Pengembangan Aplikasi Terdistribusi" })
+  nama_mata_kuliah: string;
 
   @ApiProperty()
   jumlah_mahasiswa: number;
+
+  @ApiProperty({ example: "bg-blue-600/20" })
+  warna: string;
 }
 
 export class KodeRespDto extends PickType(MataKuliah, ["kode"] as const) {}
@@ -56,7 +101,10 @@ class KelasUser extends PickType(Kelas, [
   "id",
   "nomor",
   "mataKuliahKode",
-] as const) {}
+] as const) {
+  @ApiProperty()
+  mataKuliahNama: string;
+}
 
 export class UserKelasResDto extends PickType(Pengguna, [
   "id",
@@ -66,3 +114,34 @@ export class UserKelasResDto extends PickType(Pengguna, [
   @ApiProperty({ type: [KelasUser] })
   kelas: KelasUser[];
 }
+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[];
+}
+
+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 c0411307dd06faf40d710ac28e378af4eccdba24..dde13c556b1ffb854621c5394945ace1ed81225c 100644
--- a/src/kelas/kelas.module.ts
+++ b/src/kelas/kelas.module.ts
@@ -6,9 +6,9 @@ import { AuthModule } from "src/auth/auth.module";
 import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module";
 import { KelasController } from "./kelas.controller";
 import { CustomStrategy } from "src/middlewares/custom.strategy";
-import { MataKuliah } from "src/entities/mataKuliah";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
 import { Pengguna } from "src/entities/pengguna.entity";
-import { MahasiswaKelas } from "src/entities/mahasiswaKelas";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
 import { PengajarKelas } from "src/entities/pengajarKelas.entity";
 
 @Module({
diff --git a/src/kelas/kelas.service.ts b/src/kelas/kelas.service.ts
index 908d52e1e442b6f396a9986464efa9ad616920a0..f55b832ce066eb60d33e6e2ce1900ddbd9de5cbe 100644
--- a/src/kelas/kelas.service.ts
+++ b/src/kelas/kelas.service.ts
@@ -2,21 +2,29 @@ import {
   BadRequestException,
   Injectable,
   InternalServerErrorException,
+  NotFoundException,
 } from "@nestjs/common";
 import { InjectRepository } from "@nestjs/typeorm";
 import { Kelas } from "src/entities/kelas.entity";
 import { Brackets, DataSource, Repository } from "typeorm";
 import {
-  AssignKelasDto,
   CreateKelasDto,
-  GetListKelasRespDto,
+  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";
+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";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
 import { PengajarKelas } from "src/entities/pengajarKelas.entity";
 
 @Injectable()
@@ -35,15 +43,13 @@ export class KelasService {
     private pengajarKelasRepo: Repository<PengajarKelas>,
     private datasource: DataSource,
   ) {}
-
-  async getListKelas(idMahasiswa?: string, idPengajar?: string) {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
-
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
-    }
+  async getListKelas(
+    idMahasiswa?: string,
+    idPengajar?: string,
+    kodeMatkul?: string,
+    search?: string,
+  ) {
+    const currPeriod = await this.konfService.getPeriodeOrFail();
 
     let baseQuery = this.kelasRepo
       .createQueryBuilder("kelas")
@@ -52,6 +58,7 @@ export class KelasService {
       .select([
         "kelas.id AS id",
         "kelas.nomor AS nomor",
+        "kelas.warna AS warna",
         "mataKuliah.kode AS kode_mata_kuliah",
         "mataKuliah.nama AS nama_mata_kuliah",
         "COUNT(mahasiswa) AS jumlah_mahasiswa",
@@ -76,47 +83,201 @@ export class KelasService {
         });
     }
 
+    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: GetListKelasRespDto[] = result.map((r) => ({
+    const mapped: GetKelasRespDto[] = result.map((r) => ({
       id: r.id,
       nomor: "K" + `${r.nomor}`.padStart(2, "0"),
-      mata_kuliah: `${r.kode_mata_kuliah} ${r.nama_mata_kuliah}`,
+      kode_mata_kuliah: r.kode_mata_kuliah,
+      nama_mata_kuliah: r.nama_mata_kuliah,
       jumlah_mahasiswa: parseInt(r.jumlah_mahasiswa),
+      warna: r.warna,
     }));
 
     return mapped;
   }
 
-  async create(createDto: CreateKelasDto) {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
+  async getById(id: string, idMahasiswa?: string, idPengajar?: string) {
+    const currPeriod = await this.konfService.getPeriodeOrFail();
 
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
+    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,
+        });
     }
 
-    const maxClass = await this.kelasRepo.findOne({
-      where: {
-        mataKuliahKode: createDto.mataKuliahKode,
-        periode: currPeriod,
-      },
-      order: {
-        nomor: "DESC",
-      },
-    });
+    if (idPengajar) {
+      baseQuery = baseQuery
+        .innerJoin("kelas.pengajar", "pengajar")
+        .andWhere("pengajar.pengajarId = :idPengajar", {
+          idPengajar,
+        });
+    }
+
+    const result = await baseQuery
+      .groupBy("kelas.id, mataKuliah.kode")
+      .getRawOne();
 
-    const num = maxClass ? maxClass.nomor + 1 : 1;
+    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 await this.kelasRepo.insert({
+    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: num,
+      nomor,
       periode: currPeriod,
-      warna: "gray", // TODO: random color, maybe need some adjustment for tailwind dynamic binding
+      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) {
@@ -125,7 +286,11 @@ export class KelasService {
     return { kode: createDto.kode };
   }
 
-  async getKelasPengguna(mode: "MAHASISWA" | "DOSEN", search?: string) {
+  async getKelasPengguna(
+    mode: "MAHASISWA" | "DOSEN",
+    search?: string,
+    id?: string,
+  ) {
     const currPeriod = await this.konfService.getKonfigurasiByKey(
       process.env.KONF_PERIODE_KEY,
     );
@@ -148,6 +313,7 @@ export class KelasService {
           periode: currPeriod,
         },
       )
+      .leftJoinAndSelect("kelas.mataKuliah", "mataKuliah")
       .where("pengguna.roles @> :role", {
         role: [mode === "MAHASISWA" ? "S2_MAHASISWA" : "S2_KULIAH"],
       });
@@ -161,6 +327,10 @@ export class KelasService {
       );
     }
 
+    if (id) {
+      penggunaQuery = penggunaQuery.andWhere("pengguna.id = :id", { id });
+    }
+
     const mhs = await penggunaQuery.getMany();
 
     return mhs.map((m) => ({
@@ -171,6 +341,7 @@ export class KelasService {
         id: k.kelas.id,
         nomor: k.kelas.nomor,
         mataKuliahKode: k.kelas.mataKuliahKode,
+        mataKuliahNama: k.kelas.mataKuliah.nama,
       })),
     }));
   }
@@ -361,4 +532,177 @@ export class KelasService {
 
     return { message: "Kelas berhasil dihapus" };
   }
+
+  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;
+  }
+
+  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/konfigurasi/konfigurasi.service.ts b/src/konfigurasi/konfigurasi.service.ts
index 23ee9f8761d27dc8f257181c6f7d2abbf35de001..a8e7edaccbcafeeab8aa59f9039ce69ae88a21c2 100644
--- a/src/konfigurasi/konfigurasi.service.ts
+++ b/src/konfigurasi/konfigurasi.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
 import { InjectRepository } from "@nestjs/typeorm";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
 import { Repository } from "typeorm";
@@ -29,4 +29,16 @@ export class KonfigurasiService {
 
     return data?.value;
   }
+
+  async getPeriodeOrFail() {
+    const currPeriod = await this.getKonfigurasiByKey(
+      process.env.KONF_PERIODE_KEY,
+    );
+
+    if (!currPeriod) {
+      throw new BadRequestException("Periode belum dikonfigurasi");
+    }
+
+    return currPeriod;
+  }
 }
diff --git a/src/main.ts b/src/main.ts
index 11cb941afa48d31dc673e827398bac25ee2df6c0..fdce89e3372e4a1ea2d1a7bf5006ee2054c3f487 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -22,13 +22,15 @@ async function bootstrap() {
     .setDescription("GraduIT API Documentation for S2 services")
     .setVersion("1.0")
     .addTag("Alokasi Topik")
-    .addTag("Approval")
     .addTag("Bimbingan")
     .addTag("Dashboard")
     .addTag("Dosen Bimbingan")
     .addTag("Kelas")
     .addTag("Konfigurasi")
+    .addTag("Nilai")
     .addTag("Registrasi Tesis")
+    .addTag("Submisi Tugas")
+    .addTag("Tugas")
     .addCookieAuth(process.env.COOKIE_NAME)
     .addBearerAuth()
     .build();
diff --git a/src/middlewares/forbidden-exception.filter.ts b/src/middlewares/forbidden-exception.filter.ts
index ec5ba9ddf450d25a3698c6fbc8ac20f32f1d468b..9d6de0d98638596e89d2c171d2c28dbbc145c205 100644
--- a/src/middlewares/forbidden-exception.filter.ts
+++ b/src/middlewares/forbidden-exception.filter.ts
@@ -13,9 +13,9 @@ export class ForbiddenExceptionFilter implements ExceptionFilter {
     const response = ctx.getResponse<Response>();
     const status = exception.getStatus();
 
-    response.clearCookie(process.env.COOKIE_NAME).status(status).json({
-      message: "Forbidden",
-      status,
-    });
+    response
+      .clearCookie(process.env.COOKIE_NAME)
+      .status(status)
+      .json(exception.getResponse());
   }
 }
diff --git a/src/nilai/nilai.controller.ts b/src/nilai/nilai.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6bb1cb659a4ad0347e37c69df0ad85750da2d1ad
--- /dev/null
+++ b/src/nilai/nilai.controller.ts
@@ -0,0 +1,45 @@
+import { Body, Controller, Get, Patch, Query, UseGuards } from "@nestjs/common";
+import { NilaiService } from "./nilai.service";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiOkResponse,
+  ApiTags,
+} from "@nestjs/swagger";
+import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
+import { RolesGuard } from "src/middlewares/roles.guard";
+import { Roles } from "src/middlewares/roles.decorator";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import {
+  GetNilaiByMatkulQueryDto,
+  GetNilaiByMatkulRespDto,
+  UpdateNilaiDto,
+  UpdateNilaiRespDto,
+} from "./nilai.dto";
+
+@ApiBearerAuth()
+@ApiCookieAuth()
+@ApiTags("Nilai")
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+@Controller("nilai")
+export class NilaiController {
+  constructor(private nilaiServ: NilaiService) {}
+
+  @ApiOkResponse({ type: [GetNilaiByMatkulRespDto] })
+  @Get()
+  async getNilaiByMatkul(@Query() query: GetNilaiByMatkulQueryDto) {
+    return this.nilaiServ.getNilaiByMatkul(
+      query.kode,
+      query.page || 1,
+      query.limit || 10,
+      query.search || "",
+    );
+  }
+
+  @ApiOkResponse({ type: UpdateNilaiRespDto })
+  @Patch()
+  async updateNilai(@Body() body: UpdateNilaiDto) {
+    return this.nilaiServ.updateNilai(body.mahasiswaKelasIds, body.nilaiAkhir);
+  }
+}
diff --git a/src/nilai/nilai.dto.ts b/src/nilai/nilai.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1b933d07468212dbfacd8fc41d441227b5084418
--- /dev/null
+++ b/src/nilai/nilai.dto.ts
@@ -0,0 +1,78 @@
+import { IsNumberString } from "@nestjs/class-validator";
+import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import {
+  IsNumber,
+  IsOptional,
+  IsPositive,
+  IsString,
+  IsUUID,
+  MaxLength,
+  MinLength,
+} from "class-validator";
+
+export class GetNilaiByMatkulQueryDto {
+  @ApiPropertyOptional({ example: "IF4031" })
+  @IsOptional()
+  @IsString()
+  @MinLength(6)
+  @MaxLength(6)
+  kode?: string;
+
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsString()
+  search?: string;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 1" })
+  page?: number;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 10" })
+  limit?: number;
+}
+
+export class GetNilaiByMatkulRespDto {
+  @ApiProperty({ example: "IF4031" })
+  mata_kuliah_kode: string;
+
+  @ApiProperty({ example: "Pemrograman Berbasis Kerangka Kerja" })
+  mata_kuliah_nama: string;
+
+  @ApiProperty()
+  kelas_nomor: number;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  mahasiswa_kelas_id: string;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  mahasiswa_id: string;
+
+  @ApiProperty()
+  mahasiswa_nama: string;
+
+  @ApiProperty({ example: "13517000" })
+  mahasiswa_nim: string;
+
+  @ApiProperty()
+  nilai_akhir: string | null;
+}
+
+export class UpdateNilaiRespDto {
+  @ApiProperty({
+    type: [String],
+    example: ["550e8400-e29b-41d4-a716-446655440000"],
+  })
+  @IsUUID("all", { each: true })
+  mahasiswaKelasIds: string[];
+}
+
+export class UpdateNilaiDto extends UpdateNilaiRespDto {
+  @ApiPropertyOptional({ description: "undefined: assign nilai to null" })
+  @IsOptional()
+  @IsNumber()
+  @IsPositive()
+  nilaiAkhir?: number;
+}
diff --git a/src/nilai/nilai.module.ts b/src/nilai/nilai.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c0a7c9c19ed393373632c2a8f3f8f5c6cc5610a8
--- /dev/null
+++ b/src/nilai/nilai.module.ts
@@ -0,0 +1,35 @@
+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";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { PengajarKelas } from "src/entities/pengajarKelas.entity";
+
+@Module({
+  imports: [
+    TypeOrmModule.forFeature([
+      MahasiswaKelas,
+      Kelas,
+      MataKuliah,
+      Konfigurasi,
+      Pengguna,
+      PengajarKelas,
+    ]),
+    AuthModule,
+    KonfigurasiModule,
+    KelasModule,
+  ],
+  controllers: [NilaiController],
+  providers: [NilaiService, CustomStrategy, KelasService, KonfigurasiService],
+})
+export class NilaiModule {}
diff --git a/src/nilai/nilai.service.ts b/src/nilai/nilai.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..81ebba4d7151fc7b42f495d542af94057fd4f66e
--- /dev/null
+++ b/src/nilai/nilai.service.ts
@@ -0,0 +1,98 @@
+import { Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Kelas } from "src/entities/kelas.entity";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { Brackets, In, Repository } from "typeorm";
+import { GetNilaiByMatkulRespDto, UpdateNilaiRespDto } from "./nilai.dto";
+
+@Injectable()
+export class NilaiService {
+  constructor(
+    @InjectRepository(MahasiswaKelas)
+    private mhsKelasRepo: Repository<MahasiswaKelas>,
+    @InjectRepository(Kelas)
+    private kelasRepo: Repository<Kelas>,
+    @InjectRepository(MataKuliah)
+    private mataKuliahRepo: Repository<MataKuliah>,
+    private konfServ: KonfigurasiService,
+  ) {}
+
+  private async isMhsKelasOrFail(mhsKelasIds: string[]) {
+    const periode = await this.konfServ.getPeriodeOrFail();
+
+    const mhsKelas = await this.mhsKelasRepo.find({
+      select: { id: true },
+      where: { id: In(mhsKelasIds), kelas: { periode } },
+    });
+
+    for (const mhsKelasId of mhsKelasIds) {
+      if (!mhsKelas.find((mk) => mk.id === mhsKelasId)) {
+        throw new Error(`Mahasiswa kelas ${mhsKelasId} tidak ditemukan`);
+      }
+    }
+  }
+
+  async getNilaiByMatkul(
+    mataKuliahKode: string,
+    page: number,
+    limit: number,
+    search: string,
+  ) {
+    const currPeriode = await this.konfServ.getPeriodeOrFail();
+    console.log(limit);
+
+    const baseQuery = this.mataKuliahRepo
+      .createQueryBuilder("matkul")
+      .select([
+        "matkul.kode AS mata_kuliah_kode",
+        "matkul.nama AS mata_kuliah_nama",
+        "kelas.nomor AS kelas_nomor",
+        "mhsKelas.id AS mahasiswa_kelas_id",
+        "mahasiswa.id AS mahasiswa_id",
+        "mahasiswa.nama AS mahasiswa_nama",
+        "mahasiswa.nim AS mahasiswa_nim",
+        "mhsKelas.nilaiAkhir AS nilai_akhir",
+      ])
+      .innerJoin("matkul.kelas", "kelas")
+      .leftJoin("kelas.mahasiswa", "mhsKelas")
+      .innerJoin("mhsKelas.mahasiswa", "mahasiswa")
+      .where("kelas.periode = :periode", { periode: currPeriode })
+      .andWhere(
+        new Brackets((qb) => {
+          qb.where("mahasiswa.nama ILIKE :search", {
+            search: `%${search}%`,
+          }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` });
+        }),
+      );
+
+    if (mataKuliahKode) {
+      baseQuery.andWhere("matkul.kode = :kode", { kode: mataKuliahKode });
+    }
+
+    const mhsKelas: GetNilaiByMatkulRespDto[] = await baseQuery
+      .orderBy("matkul.kode")
+      .addOrderBy("kelas.nomor")
+      .addOrderBy("mahasiswa.nim")
+      .skip((page - 1) * limit)
+      .limit(limit)
+      .getRawMany();
+
+    return mhsKelas;
+  }
+
+  async updateNilai(
+    mhsKelasIds: string[],
+    nilaiAkhir?: number,
+  ): Promise<UpdateNilaiRespDto> {
+    await this.isMhsKelasOrFail(mhsKelasIds);
+
+    await this.mhsKelasRepo.update(
+      { id: In(mhsKelasIds) },
+      { nilaiAkhir: nilaiAkhir ?? null },
+    );
+
+    return { mahasiswaKelasIds: mhsKelasIds };
+  }
+}
diff --git a/src/registrasi-tesis/registrasi-tesis.controller.ts b/src/registrasi-tesis/registrasi-tesis.controller.ts
index 4b0904fe472ce0dba0b8363e62f53291e5988af4..e41920cdafcd7e91a4378bd0224bfdbcb9605ec7 100644
--- a/src/registrasi-tesis/registrasi-tesis.controller.ts
+++ b/src/registrasi-tesis/registrasi-tesis.controller.ts
@@ -4,7 +4,6 @@ import {
   Controller,
   ForbiddenException,
   Get,
-  NotFoundException,
   Param,
   Patch,
   Post,
@@ -16,6 +15,7 @@ import {
   ApiBearerAuth,
   ApiCookieAuth,
   ApiOkResponse,
+  ApiOperation,
   ApiTags,
 } from "@nestjs/swagger";
 import { Request } from "express";
@@ -27,9 +27,10 @@ import { Roles } from "src/middlewares/roles.decorator";
 import { RolesGuard } from "src/middlewares/roles.guard";
 import {
   FindAllNewestRegRespDto,
+  GetByIdRespDto,
+  IdDto,
   RegByMhsParamDto,
   RegDto,
-  RegParamDto,
   RegQueryDto,
   RegStatisticsRespDto,
   UpdateByMhsParamsDto,
@@ -50,13 +51,10 @@ export class RegistrasiTesisController {
     private readonly konfService: KonfigurasiService,
   ) {}
 
-  @UseGuards(CustomAuthGuard, RolesGuard)
-  @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
-  @Get("/mahasiswa/:mahasiswaId")
-  findByUserId(@Param() params: RegByMhsParamDto) {
-    return this.registrasiTesisService.findByUserId(params.mahasiswaId);
-  }
-
+  @ApiOperation({
+    summary:
+      "Create new registration. Roles: S2_MAHASISWA, ADMIN, S2_TIM_TESIS",
+  })
   @UseGuards(CustomAuthGuard, RolesGuard)
   @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
   @Post()
@@ -72,7 +70,88 @@ export class RegistrasiTesisController {
     );
   }
 
-  // Right now only admin & timtesis view is handled (apakah dosen perlu summary juga?)
+  @ApiOperation({
+    summary:
+      "Find registrations (historical) by Mahasiswa ID. Roles: S2_MAHASISWA, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: [GetByIdRespDto] })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.S2_MAHASISWA, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+  @Get("/mahasiswa/:mahasiswaId")
+  async findByUserId(@Param() params: RegByMhsParamDto, @Req() req: Request) {
+    const { id, roles } = req.user as AuthDto;
+
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_MAHASISWA
+      if (id !== params.mahasiswaId) {
+        throw new ForbiddenException();
+      }
+    }
+
+    const periode = await this.konfService.getKonfigurasiByKey(
+      process.env.KONF_PERIODE_KEY,
+    );
+
+    if (!periode) {
+      throw new BadRequestException("Periode belum dikonfigurasi.");
+    }
+
+    return this.registrasiTesisService.findByUserId(
+      params.mahasiswaId,
+      periode,
+      false,
+      undefined,
+    );
+  }
+
+  @ApiOperation({
+    summary:
+      "Find newest registration by Mahasiswa ID. Roles: ADMIN, S2_TIM_TESIS, S2_PEMBIMBING",
+  })
+  @ApiOkResponse({ type: GetByIdRespDto })
+  @UseGuards(CustomAuthGuard, RolesGuard)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING)
+  @Get("/mahasiswa/:mahasiswaId/newest")
+  async findNewestByUserId(
+    @Param() params: RegByMhsParamDto,
+    @Req() req: Request,
+  ) {
+    const { id, roles } = req.user as AuthDto;
+
+    let idPenerima = undefined;
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_PEMBIMBING
+      idPenerima = id;
+    }
+
+    const periode = await this.konfService.getKonfigurasiByKey(
+      process.env.KONF_PERIODE_KEY,
+    );
+
+    if (!periode) {
+      throw new BadRequestException("Periode belum dikonfigurasi.");
+    }
+
+    const res = await this.registrasiTesisService.findByUserId(
+      params.mahasiswaId,
+      periode,
+      true,
+      idPenerima,
+    );
+
+    return res[0];
+  }
+
+  @ApiOperation({
+    summary:
+      "Get statistics of registrations. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
   @ApiOkResponse({ type: RegStatisticsRespDto })
   @UseGuards(CustomAuthGuard, RolesGuard)
   @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
@@ -96,6 +175,10 @@ export class RegistrasiTesisController {
 
   // Admin & TimTesis view will show newst reg records per Mahasiswa
   // Pembimbing view will show all regs towards them
+  @ApiOperation({
+    summary:
+      "Find all newest registration for each Mahasiswa. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
   @ApiOkResponse({ type: FindAllNewestRegRespDto, isArray: true })
   @UseGuards(CustomAuthGuard, RolesGuard)
   @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
@@ -128,42 +211,18 @@ export class RegistrasiTesisController {
     });
   }
 
+  @ApiOperation({
+    summary:
+      "Update interview date of newest in process registration by Mahasiswa ID. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: IdDto })
   @UseGuards(CustomAuthGuard, RolesGuard)
-  @Roles(RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
-  @Get("/:id")
-  async findById(
-    @Req() req: Request,
-    @Param() params: RegParamDto,
-    @Query()
-    query: ViewQueryDto,
-  ) {
-    const { id: idPenerima, roles } = req.user as AuthDto;
-
-    if (!roles.includes(query.view)) {
-      throw new ForbiddenException();
-    }
-
-    const res = await this.registrasiTesisService.findRegById(params.id);
-    if (!res) {
-      throw new NotFoundException();
-    }
-
-    if (
-      query.view === RoleEnum.S2_PEMBIMBING &&
-      res.penerima.id !== idPenerima
-    ) {
-      throw new ForbiddenException();
-    }
-
-    return res;
-  }
-
-  @UseGuards(CustomAuthGuard, RolesGuard)
-  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING)
   @Patch("/:mhsId/interview")
   async updateInterviewDateByMhsId(
     @Param() params: UpdateByMhsParamsDto,
     @Body() body: UpdateInterviewBodyDto,
+    @Req() req: Request,
   ) {
     const periode = await this.konfService.getKonfigurasiByKey(
       process.env.KONF_PERIODE_KEY,
@@ -173,19 +232,37 @@ export class RegistrasiTesisController {
       throw new BadRequestException("Periode belum dikonfigurasi.");
     }
 
+    const { id, roles } = req.user as AuthDto;
+    let idPenerima = undefined;
+
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_PEMBIMBING
+      idPenerima = id;
+    }
+
     return await this.registrasiTesisService.updateInterviewDate(
       params.mhsId,
       periode,
       body,
+      idPenerima,
     );
   }
 
+  @ApiOperation({
+    summary:
+      "Update status of newest registration by Mahasiswa ID. Roles: S2_PEMBIMBING, ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: IdDto })
   @UseGuards(CustomAuthGuard, RolesGuard)
-  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+  @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS, RoleEnum.S2_PEMBIMBING)
   @Patch("/:mhsId/status")
   async updateStatusByMhsId(
     @Param() params: UpdateByMhsParamsDto,
     @Body() body: UpdateStatusBodyDto,
+    @Req() req: Request,
   ) {
     const periode = await this.konfService.getKonfigurasiByKey(
       process.env.KONF_PERIODE_KEY,
@@ -195,17 +272,34 @@ export class RegistrasiTesisController {
       throw new BadRequestException("Periode belum dikonfigurasi.");
     }
 
+    const { id, roles } = req.user as AuthDto;
+    let idPenerima = undefined;
+
+    if (
+      !roles.includes(RoleEnum.ADMIN) &&
+      !roles.includes(RoleEnum.S2_TIM_TESIS)
+    ) {
+      // roles only include RoleEnum.S2_PEMBIMBING
+      idPenerima = id;
+    }
+
     return await this.registrasiTesisService.updateStatus(
       params.mhsId,
       periode,
       body,
+      idPenerima,
     );
   }
 
+  @ApiOperation({
+    summary:
+      "Update pembimbing list of approved registration by Mahasiswa ID. Roles: ADMIN, S2_TIM_TESIS",
+  })
+  @ApiOkResponse({ type: IdDto })
   @UseGuards(CustomAuthGuard, RolesGuard)
   @Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
   @Patch("/:mhsId/pembimbing")
-  async udpatePembimbingListByMhsId(
+  async updatePembimbingListByMhsId(
     @Param() params: UpdateByMhsParamsDto,
     @Body() body: UpdatePembimbingBodyDto,
   ) {
diff --git a/src/registrasi-tesis/registrasi-tesis.dto.ts b/src/registrasi-tesis/registrasi-tesis.dto.ts
index f18cca53a0f91f6503197d1318f7ca283cb85015..330f31d2bf8ea6118fa74403d210c1cf87e13921 100644
--- a/src/registrasi-tesis/registrasi-tesis.dto.ts
+++ b/src/registrasi-tesis/registrasi-tesis.dto.ts
@@ -6,10 +6,14 @@ import {
   IsString,
   IsUUID,
 } from "@nestjs/class-validator";
-import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger";
 import { ArrayMinSize, ArrayUnique, IsArray } from "class-validator";
-import { JalurEnum, RegStatus } from "src/entities/pendaftaranTesis.entity";
-import { RoleEnum } from "src/entities/pengguna.entity";
+import {
+  JalurEnum,
+  PendaftaranTesis,
+  RegStatus,
+} from "src/entities/pendaftaranTesis.entity";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
 
 export class RegDto {
   @IsUUID()
@@ -39,13 +43,21 @@ export class RegByMhsParamDto {
   mahasiswaId: string;
 }
 
-export class RegParamDto {
+export class IdDto {
   @IsUUID()
   @ApiProperty()
   id: string;
 }
 
-export class RegQueryDto {
+export class ViewQueryDto {
+  @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS])
+  @ApiProperty({
+    enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS],
+  })
+  view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS;
+}
+
+export class RegQueryDto extends ViewQueryDto {
   @IsOptional()
   @IsNumberString()
   @ApiPropertyOptional()
@@ -75,35 +87,29 @@ export class RegQueryDto {
   @IsEnum(["ASC", "DESC"])
   @ApiPropertyOptional({ enum: ["ASC", "DESC"] })
   sort?: "ASC" | "DESC";
-
-  @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS])
-  @ApiProperty({
-    enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS],
-  })
-  view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS;
-}
-
-export class ViewQueryDto {
-  @IsEnum([RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS])
-  @ApiProperty({
-    enum: [RoleEnum.S2_PEMBIMBING, RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS],
-  })
-  view: RoleEnum.S2_PEMBIMBING | RoleEnum.ADMIN | RoleEnum.S2_TIM_TESIS;
 }
 
 export class FindAllNewestRegRespDataDto {
   @ApiProperty()
   pendaftaran_id: string;
+
   @ApiProperty()
   nim: string;
+
   @ApiProperty()
   mahasiswa_nama: string;
+
   @ApiProperty()
   mahasiswa_id: string;
+
   @ApiProperty()
   pembimbing_nama: string;
+
   @ApiProperty()
   status: string;
+
+  @ApiProperty()
+  jadwal_interview: Date;
 }
 
 export class FindAllNewestRegRespDto {
@@ -158,3 +164,26 @@ export class UpdatePembimbingBodyDto {
   @ArrayUnique()
   pembimbing_ids: string[];
 }
+
+class DosenPembimbingDto extends PickType(Pengguna, [
+  "id",
+  "nama",
+  "kontak",
+] as const) {}
+
+export class GetByIdRespDto extends PickType(PendaftaranTesis, [
+  "id",
+  "jadwalInterview",
+  "status",
+  "jalurPilihan",
+  "waktuPengiriman",
+] as const) {
+  @ApiProperty()
+  judulTopik: string;
+
+  @ApiProperty()
+  deskripsiTopik: string;
+
+  @ApiProperty({ type: [DosenPembimbingDto] })
+  dosenPembimbing: DosenPembimbingDto[];
+}
diff --git a/src/registrasi-tesis/registrasi-tesis.service.ts b/src/registrasi-tesis/registrasi-tesis.service.ts
index 1513caa4c7d7cae5ada90e6c5857f93349c6b533..74afb3e99311335adae3e191090ce7b4902964cb 100644
--- a/src/registrasi-tesis/registrasi-tesis.service.ts
+++ b/src/registrasi-tesis/registrasi-tesis.service.ts
@@ -1,5 +1,6 @@
 import {
   BadRequestException,
+  ForbiddenException,
   Injectable,
   InternalServerErrorException,
   NotFoundException,
@@ -14,15 +15,17 @@ import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
 import { Topik } from "src/entities/topik.entity";
 import { generateQueryBuilderOrderByObj } from "src/helper/sorting";
 import { validateId } from "src/helper/validation";
-import { ArrayContains, DataSource, In, Repository } from "typeorm";
+import { ArrayContains, Brackets, DataSource, In, Repository } from "typeorm";
 import {
   FindAllNewestRegRespDto,
+  IdDto,
   RegDto,
   RegStatisticsRespDto,
   UpdateInterviewBodyDto,
   UpdatePembimbingBodyDto,
   UpdateStatusBodyDto,
 } from "./registrasi-tesis.dto";
+import * as dayjs from "dayjs";
 
 @Injectable()
 export class RegistrasiTesisService {
@@ -84,21 +87,73 @@ export class RegistrasiTesisService {
     return createdRegistration;
   }
 
-  async findByUserId(mahasiswaId: string) {
-    const res = await this.pendaftaranTesisRepository.find({
-      relations: ["topik", "penerima"],
-      where: { mahasiswa: { id: mahasiswaId } },
-    });
+  async findByUserId(
+    mahasiswaId: string,
+    periode: string,
+    isNewestOnly: boolean,
+    idPenerima?: string,
+  ) {
+    const baseQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pt")
+      .select("pt.id")
+      .addSelect("pt.jadwalInterview")
+      .addSelect("pt.status")
+      .addSelect("pt.jalurPilihan")
+      .addSelect("pt.waktuPengiriman")
+      .addSelect("topik.judul")
+      .addSelect("penerima.id")
+      .addSelect("penerima.nama")
+      .addSelect("dosenBimbingan")
+      .addSelect("dosen.id")
+      .addSelect("dosen.nama")
+      .addSelect("dosen.kontak")
+      .addSelect("topik.judul")
+      .addSelect("topik.deskripsi")
+      .leftJoin("pt.topik", "topik")
+      .leftJoin("pt.penerima", "penerima")
+      .leftJoin("pt.dosenBimbingan", "dosenBimbingan")
+      .leftJoin("dosenBimbingan.dosen", "dosen")
+      .where("pt.mahasiswaId = :mahasiswaId", { mahasiswaId })
+      .andWhere("topik.periode = :periode", { periode })
+      .orderBy("pt.waktuPengiriman", "DESC");
+
+    const res = await baseQuery.getMany();
+
+    if (res.length === 0) {
+      throw new NotFoundException("Tidak ada registrasi tesis yang ditemukan.");
+    }
 
-    return res.map((r) => ({
-      ...r,
-      penerima: {
-        ...r.penerima,
-        password: undefined,
-        roles: undefined,
-        nim: undefined,
-      },
+    if (idPenerima) {
+      // requester only has S2_PEMBIMBING access
+      const reg = res[0];
+
+      if (reg.penerima.id !== idPenerima) {
+        throw new ForbiddenException();
+      }
+    }
+
+    const mappedRes = res.map((r) => ({
+      id: r.id,
+      jadwalInterview: r.jadwalInterview,
+      jalurPilihan: r.jalurPilihan,
+      status: r.status,
+      waktuPengiriman: r.waktuPengiriman,
+      judulTopik: r.topik.judul,
+      deskripsiTopik: r.topik.deskripsi,
+      dosenPembimbing:
+        r.status === RegStatus.APPROVED
+          ? r.dosenBimbingan.map((db) => db.dosen)
+          : [r.penerima],
     }));
+
+    if (isNewestOnly) {
+      // only get last registration
+      // slow performance because get all records first then only returns the first one
+      // need to change to use subquery
+      mappedRes.splice(1);
+    }
+
+    return mappedRes;
   }
 
   async getRegsStatistics(options: {
@@ -109,69 +164,70 @@ export class RegistrasiTesisService {
       where: { roles: ArrayContains([RoleEnum.S2_MAHASISWA]) },
     });
 
-    // Show newest regs per Mhs if POV TimTesis or Admin
-    if (!options.idPenerima) {
-      const baseQuery = this.pendaftaranTesisRepository
-        .createQueryBuilder("pt")
-        .innerJoinAndSelect(
-          (qb) =>
-            qb
-              .select([
-                "pt.mahasiswaId AS latest_mahasiswaId",
-                "MAX(pt.waktuPengiriman) AS latestPengiriman",
-              ])
-              .from(PendaftaranTesis, "pt")
-              .groupBy("pt.mahasiswaId"),
-          "latest",
-          "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman",
-        )
-        .innerJoinAndSelect("pt.topik", "topik")
-        .where("topik.periode = :periode", { periode: options.periode });
-
-      const totalDiterima = baseQuery
-        .clone()
-        .andWhere("pt.status = :status", { status: RegStatus.APPROVED })
-        .getCount();
-
-      const totalProses = baseQuery
-        .clone()
-        .where("pt.status IN (:...status)", {
-          status: [RegStatus.NOT_ASSIGNED, RegStatus.INTERVIEW],
-        })
-        .getCount();
-
-      const totalDitolak = baseQuery
-        .clone()
-        .where("pt.status = :status", { status: RegStatus.REJECTED })
-        .getCount();
-
-      const [total, diterima, proses, ditolak] = await Promise.all([
-        totalMahasiswa,
-        totalDiterima,
-        totalProses,
-        totalDitolak,
-      ]);
-
-      return {
-        diterima: {
-          amount: diterima,
-          percentage: Math.round((diterima / total) * 100),
-        },
-        sedang_proses: {
-          amount: proses,
-          percentage: Math.round((proses / total) * 100),
-        },
-        ditolak: {
-          amount: ditolak,
-          percentage: Math.round((ditolak / total) * 100),
-        },
-      };
-    } else {
-      throw new InternalServerErrorException("Not implemented");
+    // Show newest regs per Mhs
+    const baseQuery = this.pendaftaranTesisRepository
+      .createQueryBuilder("pt")
+      .innerJoinAndSelect(
+        (qb) =>
+          qb
+            .select([
+              "pt.mahasiswaId AS latest_mahasiswaId",
+              "MAX(pt.waktuPengiriman) AS latestPengiriman",
+            ])
+            .from(PendaftaranTesis, "pt")
+            .groupBy("pt.mahasiswaId"),
+        "latest",
+        "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman",
+      )
+      .innerJoinAndSelect("pt.topik", "topik")
+      .where("topik.periode = :periode", { periode: options.periode });
+
+    if (options.idPenerima) {
+      baseQuery.andWhere("pt.penerimaId = :idPenerima", {
+        idPenerima: options.idPenerima,
+      });
     }
+
+    const totalDiterima = baseQuery
+      .clone()
+      .andWhere("pt.status = :status", { status: RegStatus.APPROVED })
+      .getCount();
+
+    const totalProses = baseQuery
+      .clone()
+      .andWhere("pt.status IN (:...status)", {
+        status: [RegStatus.NOT_ASSIGNED, RegStatus.INTERVIEW],
+      })
+      .getCount();
+
+    const totalDitolak = baseQuery
+      .clone()
+      .andWhere("pt.status = :status", { status: RegStatus.REJECTED })
+      .getCount();
+
+    const [total, diterima, proses, ditolak] = await Promise.all([
+      totalMahasiswa,
+      totalDiterima,
+      totalProses,
+      totalDitolak,
+    ]);
+
+    return {
+      diterima: {
+        amount: diterima,
+        percentage: Math.round((diterima / total) * 100),
+      },
+      sedang_proses: {
+        amount: proses,
+        percentage: Math.round((proses / total) * 100),
+      },
+      ditolak: {
+        amount: ditolak,
+        percentage: Math.round((ditolak / total) * 100),
+      },
+    };
   }
 
-  // TODO sort
   async findAllRegs(options: {
     status?: RegStatus;
     page: number;
@@ -186,22 +242,20 @@ export class RegistrasiTesisService {
       .createQueryBuilder("pt")
       .select("pt");
 
-    // Show newest regs per Mhs if POV TimTesis or Admin
+    // Show newest regs per Mhs
     // May need to make materialized view to improve performance
-    if (!options.idPenerima) {
-      baseQuery.innerJoinAndSelect(
-        (qb) =>
-          qb
-            .select([
-              "pt.mahasiswaId AS latest_mahasiswaId",
-              "MAX(pt.waktuPengiriman) AS latestPengiriman",
-            ])
-            .from(PendaftaranTesis, "pt")
-            .groupBy("pt.mahasiswaId"),
-        "latest",
-        "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman",
-      );
-    }
+    baseQuery.innerJoinAndSelect(
+      (qb) =>
+        qb
+          .select([
+            "pt.mahasiswaId AS latest_mahasiswaId",
+            "MAX(pt.waktuPengiriman) AS latestPengiriman",
+          ])
+          .from(PendaftaranTesis, "pt")
+          .groupBy("pt.mahasiswaId"),
+      "latest",
+      "latest.latest_mahasiswaId = pt.mahasiswaId AND pt.waktuPengiriman = latest.latestPengiriman",
+    );
 
     baseQuery
       .innerJoinAndSelect("pt.topik", "topik")
@@ -209,19 +263,25 @@ export class RegistrasiTesisService {
       .innerJoinAndSelect("pt.mahasiswa", "mahasiswa")
       .where("topik.periode = :periode", { periode: options.periode });
 
+    if (options.idPenerima) {
+      baseQuery.andWhere("pt.penerimaId = :idPenerima", {
+        idPenerima: options.idPenerima,
+      });
+    }
+
     if (options.search)
       baseQuery.andWhere(
-        "mahasiswa.nama LIKE '%' || :search || '%' OR mahasiswa.nim LIKE '%' || :search || '%'",
-        {
-          search: options.search,
-        },
+        new Brackets((qb) =>
+          qb
+            .where("mahasiswa.nama ILIKE :search", {
+              search: `%${options.search}%`,
+            })
+            .orWhere("mahasiswa.nim ILIKE :search", {
+              search: `%${options.search}%`,
+            }),
+        ),
       );
 
-    if (options.idPenerima)
-      baseQuery.andWhere("penerima.id = :idPenerima", {
-        idPenerima: options.idPenerima,
-      });
-
     if (options.status)
       baseQuery.andWhere("pt.status = :status", {
         status: options.status,
@@ -256,6 +316,7 @@ export class RegistrasiTesisService {
         mahasiswa_nama: reg.mahasiswa.nama,
         pembimbing_nama: reg.penerima.nama,
         status: reg.status,
+        jadwal_interview: reg.jadwalInterview,
       })),
       count,
     };
@@ -263,40 +324,7 @@ export class RegistrasiTesisService {
     return resData;
   }
 
-  async findRegById(id: string) {
-    // not periode-protected
-    return await this.pendaftaranTesisRepository.findOne({
-      select: {
-        id: true,
-        waktuPengiriman: true,
-        jadwalInterview: true,
-        waktuKeputusan: true,
-        status: true,
-        jalurPilihan: true,
-        penerima: {
-          id: true,
-          nama: true,
-          email: true,
-        },
-        mahasiswa: {
-          id: true,
-          nama: true,
-          email: true,
-          nim: true,
-        },
-      },
-      where: {
-        id,
-      },
-      relations: {
-        penerima: true,
-        topik: true,
-        mahasiswa: true,
-      },
-    });
-  }
-
-  async getNewestRegByMhs(mahasiswaId: string, periode: string) {
+  private async getNewestRegByMhs(mahasiswaId: string, periode: string) {
     const mahasiswa = await this.penggunaRepository.findOne({
       select: {
         id: true,
@@ -313,14 +341,23 @@ export class RegistrasiTesisService {
     const newestReg = await this.pendaftaranTesisRepository.findOne({
       select: {
         id: true,
+        jadwalInterview: true,
         status: true,
         waktuPengiriman: true,
+        jalurPilihan: true,
         topik: {
+          judul: true,
+          deskripsi: true,
           periode: true,
         },
+        penerima: {
+          id: true,
+        },
       },
       relations: {
         topik: true,
+        penerima: true,
+        mahasiswa: true,
       },
       where: {
         mahasiswa: mahasiswa,
@@ -345,9 +382,23 @@ export class RegistrasiTesisService {
     mahasiswaId: string,
     periode: string,
     dto: UpdateInterviewBodyDto,
+    idPenerima?: string,
   ) {
+    const minDate = new Date();
+    minDate.setDate(minDate.getDate() + 2);
+
+    if (dayjs(dto.date).isBefore(dayjs(minDate).endOf("d"))) {
+      throw new BadRequestException(
+        "Interview date must be at least 2 days from now",
+      );
+    }
+
     const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode);
 
+    if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) {
+      throw new ForbiddenException();
+    }
+
     const restrictedStatus: RegStatus[] = [
       RegStatus.APPROVED,
       RegStatus.REJECTED,
@@ -367,22 +418,54 @@ export class RegistrasiTesisService {
       { jadwalInterview: newDate, status: RegStatus.INTERVIEW },
     );
 
-    return { status: "ok" };
+    return { id: newestReg.id } as IdDto;
   }
 
   async updateStatus(
     mahasiswaId: string,
     periode: string,
     dto: UpdateStatusBodyDto,
+    idPenerima?: string,
   ) {
     const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode);
 
-    await this.pendaftaranTesisRepository.update(
-      { id: newestReg.id },
-      { status: dto.status },
-    );
+    if (newestReg && idPenerima && newestReg.penerima.id !== idPenerima) {
+      throw new ForbiddenException();
+    }
+
+    const queryRunner = this.dataSource.createQueryRunner();
+    await queryRunner.connect();
+    await queryRunner.startTransaction();
+
+    try {
+      await queryRunner.manager.update(
+        PendaftaranTesis,
+        { id: newestReg.id },
+        { status: dto.status, waktuKeputusan: new Date() },
+      );
+
+      if (dto.status === RegStatus.APPROVED) {
+        await queryRunner.manager.insert(DosenBimbingan, {
+          idPendaftaran: newestReg.id,
+          idDosen: newestReg.penerima.id,
+        });
+      } else {
+        // dto.status === RegStatus.REJECTED
+        await queryRunner.manager.delete(DosenBimbingan, {
+          idPendaftaran: newestReg.id,
+        });
+      }
 
-    return { status: "ok" };
+      await queryRunner.commitTransaction();
+    } catch (err) {
+      await queryRunner.rollbackTransaction();
+
+      throw new InternalServerErrorException();
+    } finally {
+      await queryRunner.release();
+    }
+
+    return { id: newestReg.id } as IdDto;
   }
 
   async updatePembimbingList(
@@ -392,7 +475,6 @@ export class RegistrasiTesisService {
   ) {
     const newestReg = await this.getNewestRegByMhs(mahasiswaId, periode);
 
-    // TODO decide to allow unapproved Registrations to have their Penerima changed or not
     if (newestReg.status !== RegStatus.APPROVED)
       throw new BadRequestException(
         "Cannot update pembimbing on non-approved registration",
@@ -427,7 +509,7 @@ export class RegistrasiTesisService {
       (newId) => !newPembimbingIds.includes(newId),
     );
 
-    const queryRunner = await this.dataSource.createQueryRunner();
+    const queryRunner = this.dataSource.createQueryRunner();
     await queryRunner.connect();
     await queryRunner.startTransaction();
 
@@ -443,10 +525,12 @@ export class RegistrasiTesisService {
       await queryRunner.commitTransaction();
     } catch (err) {
       await queryRunner.rollbackTransaction();
+
+      throw new InternalServerErrorException();
     } finally {
       await queryRunner.release();
     }
 
-    return { status: "ok" };
+    return { id: newestReg.id } as IdDto;
   }
 }
diff --git a/src/submisi-tugas/submisi-tugas.controller.ts b/src/submisi-tugas/submisi-tugas.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cb4f45a1b1cdb0d5535344ac9d8ccee62e4db8b9
--- /dev/null
+++ b/src/submisi-tugas/submisi-tugas.controller.ts
@@ -0,0 +1,104 @@
+import {
+  Body,
+  Controller,
+  Get,
+  Param,
+  Post,
+  Query,
+  Req,
+  UseGuards,
+} from "@nestjs/common";
+import { SubmisiTugasService } from "./submisi-tugas.service";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiOkResponse,
+  ApiOperation,
+  ApiTags,
+} from "@nestjs/swagger";
+import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
+import { RolesGuard } from "src/middlewares/roles.guard";
+import { Roles } from "src/middlewares/roles.decorator";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import {
+  CreateSubmisiTugasDto,
+  GetSubmisiTugasByIdRespDto,
+  GetSubmisiTugasByTugasIdQueryDto,
+  GetSubmisiTugasByTugasIdRespDto,
+  SubmisiTugasIdDto,
+} from "./submisi-tugas.dto";
+import { AuthDto } from "src/auth/auth.dto";
+import { Request } from "express";
+
+@ApiTags("Submisi Tugas")
+@ApiBearerAuth()
+@ApiCookieAuth()
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Controller("submisi-tugas")
+export class SubmisiTugasController {
+  constructor(private submisiTugasServ: SubmisiTugasService) {}
+
+  @Roles(RoleEnum.S2_MAHASISWA)
+  @Post()
+  async createSubmisiTugas(
+    @Body() createDto: CreateSubmisiTugasDto,
+    @Req() req: Request,
+  ) {
+    // TODO: More validation
+    const { id } = req.user as AuthDto;
+
+    return await this.submisiTugasServ.createSubmisiTugas(createDto, id);
+  }
+
+  @ApiOperation({
+    summary:
+      "Get submisi tugas by sumbisi tugas id. Roles: S2_KULIAH, S2_MAHASISWA",
+  })
+  @ApiOkResponse({ type: GetSubmisiTugasByIdRespDto })
+  @Roles(RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA)
+  @Get("/:id")
+  async getSubmisiTugasById(
+    @Req() req: Request,
+    @Param() param: SubmisiTugasIdDto,
+  ) {
+    const { id, roles } = req.user as AuthDto;
+
+    let idMahasiswa = undefined;
+    let idPengajar = undefined;
+
+    if (!roles.includes(RoleEnum.S2_KULIAH)) {
+      idMahasiswa = id;
+    } else {
+      idPengajar = id;
+    }
+
+    return await this.submisiTugasServ.getSubmisiTugasById(
+      param.id,
+      idMahasiswa,
+      idPengajar,
+    );
+  }
+
+  @ApiOperation({
+    summary: "Get list of submisi tugas summary by tugas id. Roles: S2_KULIAH",
+  })
+  @ApiOkResponse({ type: [GetSubmisiTugasByTugasIdRespDto] })
+  @Roles(RoleEnum.S2_KULIAH)
+  @Get()
+  async getSubmisiTugasByTugasId(
+    @Req() req: Request,
+    @Query() query: GetSubmisiTugasByTugasIdQueryDto,
+  ) {
+    const { id } = req.user as AuthDto;
+
+    return await this.submisiTugasServ.getSubmisiTugasByTugasId(
+      query.tugasId,
+      id,
+      query.search || "",
+      query.page || 1,
+      query.limit || 10,
+      query.order || "ASC",
+      query.isSubmitted,
+    );
+  }
+}
diff --git a/src/submisi-tugas/submisi-tugas.dto.ts b/src/submisi-tugas/submisi-tugas.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..daa8486087d0e417d1ef0c72f9b74e8b8cf15900
--- /dev/null
+++ b/src/submisi-tugas/submisi-tugas.dto.ts
@@ -0,0 +1,126 @@
+import { IsString } from "@nestjs/class-validator";
+import {
+  ApiProperty,
+  ApiPropertyOptional,
+  OmitType,
+  PickType,
+} from "@nestjs/swagger";
+import { Transform, Type } from "class-transformer";
+import {
+  IsBoolean,
+  IsEnum,
+  IsNumberString,
+  IsOptional,
+  ValidateNested,
+} from "class-validator";
+import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity";
+import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { SubmisiTugas } from "src/entities/submisiTugas.entity";
+import { GetTugasByIdRespDto } from "src/tugas/tugas.dto";
+
+class BerkasSubmisiTugasWithoutId extends OmitType(BerkasSubmisiTugas, [
+  "id",
+] as const) {}
+
+export class CreateSubmisiTugasDto extends PickType(SubmisiTugas, [
+  "jawaban",
+  "isSubmitted",
+  "tugasId",
+]) {
+  @ApiProperty({ type: [BerkasSubmisiTugasWithoutId] })
+  @ValidateNested({ each: true })
+  @Type(() => BerkasSubmisiTugasWithoutId)
+  berkasSubmisiTugas: BerkasSubmisiTugasWithoutId[];
+}
+
+export class SubmisiTugasIdDto extends PickType(SubmisiTugas, [
+  "id",
+] as const) {}
+
+export class GetSubmisiTugasByTugasIdQueryDto extends PickType(SubmisiTugas, [
+  "tugasId",
+] as const) {
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsString()
+  search?: string;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 1" })
+  page?: number;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 10" })
+  limit?: number;
+
+  @IsOptional()
+  @IsBoolean()
+  @Transform(({ value }) => value === "true")
+  @ApiPropertyOptional({
+    description: "if not specified, will return all submisi tugas",
+  })
+  isSubmitted?: boolean;
+
+  @ApiPropertyOptional({
+    enum: ["ASC", "DESC"],
+    description: "order by nim. default: ASC",
+  })
+  @IsOptional()
+  @IsEnum(["ASC", "DESC"])
+  order?: "ASC" | "DESC";
+}
+
+class PickedSubmisiTugas extends PickType(SubmisiTugas, [
+  "id",
+  "isSubmitted",
+  "berkasSubmisiTugas",
+] as const) {}
+
+class PickedSubmisiTugasExtended extends PickType(SubmisiTugas, [
+  "id",
+  "isSubmitted",
+  "jawaban",
+  "submittedAt",
+  "berkasSubmisiTugas",
+] as const) {}
+
+class PickedPendaftaranTesis extends PickType(PendaftaranTesis, [
+  "id",
+  "jalurPilihan",
+  "waktuPengiriman",
+  "jadwalInterview",
+  "status",
+  "topik",
+] as const) {}
+
+class PickedPendaftaran extends PickType(Pengguna, [
+  "id",
+  "nama",
+  "email",
+] as const) {
+  @ApiProperty({ type: PickedPendaftaranTesis })
+  pendaftaranTesis: PickedPendaftaranTesis;
+}
+
+export class GetSubmisiTugasByTugasIdRespDto extends PickType(Pengguna, [
+  "id",
+  "nim",
+  "nama",
+] as const) {
+  @ApiPropertyOptional({ type: PickedSubmisiTugas })
+  submisiTugas?: PickedSubmisiTugas;
+}
+
+export class GetSubmisiTugasByIdRespDto {
+  @ApiProperty({ type: GetTugasByIdRespDto })
+  tugas: GetTugasByIdRespDto;
+
+  @ApiPropertyOptional({ type: PickedPendaftaran })
+  pendaftaran?: PickedPendaftaran;
+
+  @ApiProperty({ type: PickedSubmisiTugasExtended })
+  submisiTugas: PickedSubmisiTugasExtended;
+}
diff --git a/src/submisi-tugas/submisi-tugas.module.ts b/src/submisi-tugas/submisi-tugas.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1fd15428a499fe276ed0aeaef1b8168bd532b105
--- /dev/null
+++ b/src/submisi-tugas/submisi-tugas.module.ts
@@ -0,0 +1,31 @@
+import { Module } from "@nestjs/common";
+import { SubmisiTugasController } from "./submisi-tugas.controller";
+import { SubmisiTugasService } from "./submisi-tugas.service";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { AuthModule } from "src/auth/auth.module";
+import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module";
+import { SubmisiTugas } from "src/entities/submisiTugas.entity";
+import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity";
+import { TugasModule } from "src/tugas/tugas.module";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { Tugas } from "src/entities/tugas.entity";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+
+@Module({
+  imports: [
+    TypeOrmModule.forFeature([
+      SubmisiTugas,
+      BerkasSubmisiTugas,
+      Pengguna,
+      Tugas,
+      MahasiswaKelas,
+    ]),
+    AuthModule,
+    KonfigurasiModule,
+    TugasModule,
+  ],
+  controllers: [SubmisiTugasController],
+  providers: [SubmisiTugasService, CustomStrategy],
+})
+export class SubmisiTugasModule {}
diff --git a/src/submisi-tugas/submisi-tugas.service.ts b/src/submisi-tugas/submisi-tugas.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d3c1c11a98b638aecbf96c5b67c1b8326073ef93
--- /dev/null
+++ b/src/submisi-tugas/submisi-tugas.service.ts
@@ -0,0 +1,257 @@
+import {
+  ForbiddenException,
+  Injectable,
+  NotFoundException,
+} from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity";
+import { SubmisiTugas } from "src/entities/submisiTugas.entity";
+import { TugasService } from "src/tugas/tugas.service";
+import { Brackets, Repository } from "typeorm";
+import {
+  CreateSubmisiTugasDto,
+  GetSubmisiTugasByIdRespDto,
+  GetSubmisiTugasByTugasIdRespDto,
+} from "./submisi-tugas.dto";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { Tugas } from "src/entities/tugas.entity";
+import { RegStatus } from "src/entities/pendaftaranTesis.entity";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+
+@Injectable()
+export class SubmisiTugasService {
+  constructor(
+    @InjectRepository(SubmisiTugas)
+    private submisiTugasRepo: Repository<SubmisiTugas>,
+    @InjectRepository(BerkasSubmisiTugas)
+    private berkasSubmisiTugasRepo: Repository<BerkasSubmisiTugas>,
+    @InjectRepository(Pengguna)
+    private penggunaRepo: Repository<Pengguna>,
+    @InjectRepository(Tugas)
+    private tugasRepo: Repository<Tugas>,
+    @InjectRepository(MahasiswaKelas)
+    private mahasiswaKelasRepo: Repository<MahasiswaKelas>,
+    private tugasService: TugasService,
+    private konfService: KonfigurasiService,
+  ) {}
+  private async isMahasiswaSubmisiTugasOrFail(
+    submisiTugasId: string,
+    mahasiswaId: string,
+  ) {
+    const submisiTugas = await this.submisiTugasRepo.findOne({
+      where: { id: submisiTugasId },
+    });
+
+    if (!submisiTugas) {
+      throw new NotFoundException("Submisi tugas tidak ditemukan");
+    }
+
+    if (submisiTugas.mahasiswaId !== mahasiswaId) {
+      throw new ForbiddenException("Anda tidak memiliki akses");
+    }
+
+    // validate periode
+    await this.tugasService.isMahasiswaTugasOrFail(
+      mahasiswaId,
+      submisiTugas.tugasId,
+    );
+  }
+
+  private async isPengajarSubmisiTugasOrFail(
+    submisiTugasId: string,
+    pengajarId: string,
+  ) {
+    const submisiTugas = await this.submisiTugasRepo.findOne({
+      where: { id: submisiTugasId },
+      relations: ["tugas"],
+    });
+
+    if (!submisiTugas) {
+      throw new NotFoundException("Submisi tugas tidak ditemukan");
+    }
+
+    await this.tugasService.isPengajarTugasOrFail(
+      pengajarId,
+      submisiTugas.tugas.id,
+    );
+  }
+
+  async createSubmisiTugas(
+    createDto: CreateSubmisiTugasDto,
+    mahasiswaId: string,
+  ) {
+    await this.tugasService.isMahasiswaTugasOrFail(
+      mahasiswaId,
+      createDto.tugasId,
+    );
+
+    const tugas = await this.tugasRepo.findOneBy({ id: createDto.tugasId });
+    const mahasiswa = await this.penggunaRepo.findOneBy({ id: mahasiswaId });
+
+    const berkasSubmisiTugas = createDto.berkasSubmisiTugas.map(
+      (berkasSubmisiTugas) =>
+        this.berkasSubmisiTugasRepo.create(berkasSubmisiTugas),
+    );
+
+    const submisiTugas = this.submisiTugasRepo.create({
+      ...createDto,
+      mahasiswa,
+      submittedAt: createDto.isSubmitted ? new Date() : null,
+      tugas,
+      berkasSubmisiTugas,
+    });
+
+    await this.submisiTugasRepo.save(submisiTugas);
+
+    return submisiTugas;
+  }
+
+  private async getSubmisiTugas(id: string) {
+    const submisiTugas = await this.submisiTugasRepo.findOne({
+      where: { id },
+      relations: ["berkasSubmisiTugas"],
+    });
+
+    if (!submisiTugas) {
+      throw new NotFoundException("Submisi tugas tidak ditemukan");
+    }
+
+    return submisiTugas;
+  }
+
+  async getSubmisiTugasById(
+    id: string,
+    mahasiswaId?: string,
+    pengajarId?: string,
+  ) {
+    if (mahasiswaId) {
+      await this.isMahasiswaSubmisiTugasOrFail(id, mahasiswaId);
+    }
+
+    if (pengajarId) {
+      await this.isPengajarSubmisiTugasOrFail(id, pengajarId);
+    }
+
+    const currPeriod = await this.konfService.getPeriodeOrFail();
+    const submisiTugas = await this.getSubmisiTugas(id);
+
+    const pendaftaranQuery = this.penggunaRepo
+      .createQueryBuilder("pengguna")
+      .select([
+        "pengguna.id",
+        "pengguna.nama",
+        "pengguna.email",
+        "pendaftaranTesis.jalurPilihan",
+        "pendaftaranTesis.waktuPengiriman",
+        "topik.id",
+        "topik.judul",
+        "topik.deskripsi",
+      ])
+      .leftJoinAndSelect("pengguna.pendaftaranTesis", "pendaftaranTesis")
+      .leftJoinAndSelect("pendaftaranTesis.topik", "topik")
+      .where("pengguna.id = :id", { id: submisiTugas.mahasiswaId })
+      .andWhere("pendaftaranTesis.status = :status", {
+        status: RegStatus.APPROVED,
+      })
+      .andWhere("topik.periode = :periode", { periode: currPeriod })
+      .getOne();
+
+    const [tugas, pendaftaran] = await Promise.all([
+      this.tugasService.getTugasById(submisiTugas.tugasId),
+      pendaftaranQuery,
+    ]);
+
+    const result: GetSubmisiTugasByIdRespDto = {
+      tugas,
+      submisiTugas,
+      pendaftaran: {
+        ...pendaftaran,
+        pendaftaranTesis:
+          pendaftaran.pendaftaranTesis.length > 0
+            ? pendaftaran.pendaftaranTesis[0]
+            : undefined,
+      },
+    };
+
+    return result;
+  }
+
+  async getSubmisiTugasByTugasId(
+    tugasId: string,
+    idPenerima: string,
+    search: string,
+    page: number,
+    limit: number,
+    order: "ASC" | "DESC",
+    isSubmitted?: boolean,
+  ) {
+    await this.tugasService.isPengajarTugasOrFail(idPenerima, tugasId);
+
+    const baseQuery = await this.mahasiswaKelasRepo
+      .createQueryBuilder("mk")
+      .innerJoin("mk.kelas", "kelas", "kelas.id = mk.kelasId")
+      .innerJoinAndSelect("mk.mahasiswa", "mahasiswa")
+      .leftJoinAndSelect(
+        "mahasiswa.submisiTugas",
+        "submisiTugas",
+        "submisiTugas.tugasId = :tugasId",
+        { tugasId },
+      )
+      .leftJoinAndSelect(
+        "submisiTugas.berkasSubmisiTugas",
+        "berkasSubmisiTugas",
+      )
+      .select([
+        "mk.id",
+        "mahasiswa.id",
+        "mahasiswa.nim",
+        "mahasiswa.nama",
+        "submisiTugas.id",
+        "submisiTugas.isSubmitted",
+        "berkasSubmisiTugas",
+      ])
+      .distinctOn(["mahasiswa.nim"])
+      .where(
+        new Brackets((qb) => {
+          qb.where("mahasiswa.nama ILIKE :search", {
+            search: `%${search}%`,
+          }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` });
+        }),
+      )
+      .orderBy("mahasiswa.nim", order);
+
+    if (isSubmitted !== undefined) {
+      if (isSubmitted) {
+        baseQuery.andWhere("submisiTugas.isSubmitted = true");
+      } else {
+        baseQuery.andWhere(
+          new Brackets((qb) =>
+            qb
+              .where("submisiTugas.isSubmitted <> true")
+              .orWhere("submisiTugas.isSubmitted IS NULL"),
+          ),
+        );
+      }
+    }
+
+    const submisiTugas = await baseQuery
+      .limit(limit)
+      .skip((page - 1) * limit)
+      .getMany();
+
+    const mappedResult: GetSubmisiTugasByTugasIdRespDto[] = submisiTugas.map(
+      (submisi) => ({
+        id: submisi.mahasiswa.id,
+        nim: submisi.mahasiswa.nim,
+        nama: submisi.mahasiswa.nama,
+        submisiTugas:
+          submisi.mahasiswa.submisiTugas.length > 0
+            ? submisi.mahasiswa.submisiTugas[0]
+            : undefined,
+      }),
+    );
+
+    return mappedResult;
+  }
+}
diff --git a/src/tugas/tugas.controller.ts b/src/tugas/tugas.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b66f4efe167c7a14fea84ac9c710f4f559155186
--- /dev/null
+++ b/src/tugas/tugas.controller.ts
@@ -0,0 +1,105 @@
+import {
+  Body,
+  Controller,
+  Get,
+  Param,
+  Post,
+  Put,
+  Query,
+  Req,
+  UseGuards,
+} from "@nestjs/common";
+import { TugasService } from "./tugas.service";
+import { Roles } from "src/middlewares/roles.decorator";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiCreatedResponse,
+  ApiOkResponse,
+  ApiOperation,
+  ApiTags,
+} from "@nestjs/swagger";
+import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
+import { RolesGuard } from "src/middlewares/roles.guard";
+import {
+  TugasIdDto,
+  CreateTugasDto,
+  UpdateTugasDto,
+  GetTugasByIdRespDto,
+  GetTugasByKelasIdQueryDto,
+  GetTugasByKelasIdRespDto,
+} from "./tugas.dto";
+import { Request } from "express";
+import { AuthDto } from "src/auth/auth.dto";
+
+@ApiCookieAuth()
+@ApiBearerAuth()
+@ApiTags("Tugas")
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Controller("tugas")
+export class TugasController {
+  constructor(private readonly tugasService: TugasService) {}
+
+  @ApiOperation({ summary: "Create Tugas. Roles: S2_KULIAH" })
+  @ApiCreatedResponse({ type: TugasIdDto })
+  @Roles(RoleEnum.S2_KULIAH)
+  @Post()
+  async createTugas(@Body() createDto: CreateTugasDto, @Req() req: Request) {
+    const { id } = req.user as AuthDto;
+
+    return await this.tugasService.createTugas(createDto, id);
+  }
+
+  @ApiOperation({ summary: "Update Tugas. Roles: S2_KULIAH" })
+  @ApiOkResponse({ type: TugasIdDto })
+  @Roles(RoleEnum.S2_KULIAH)
+  @Put()
+  async updateTugas(@Body() updateDto: UpdateTugasDto, @Req() req: Request) {
+    const { id } = req.user as AuthDto;
+
+    return await this.tugasService.updateTugasById(updateDto, id);
+  }
+
+  @ApiOperation({
+    summary: "Get Tugas by id. Roles: S2_KULIAH, S2_MAHASISWA",
+  })
+  @ApiOkResponse({ type: GetTugasByIdRespDto })
+  @Roles(RoleEnum.S2_KULIAH, RoleEnum.S2_MAHASISWA)
+  @Get("/:id")
+  async getTugasById(@Param() param: TugasIdDto, @Req() req: Request) {
+    let idPengajar = undefined;
+    let idMahasiswa = undefined;
+
+    const { id, roles } = req.user as AuthDto;
+
+    if (!roles.includes(RoleEnum.S2_KULIAH)) {
+      idMahasiswa = id;
+    } else {
+      idPengajar = id;
+    }
+
+    return this.tugasService.getTugasById(param.id, idMahasiswa, idPengajar);
+  }
+
+  @ApiOperation({
+    summary: "Get Tugas list by kelas id. Roles: S2_KULIAH",
+  })
+  @ApiOkResponse({ type: GetTugasByKelasIdRespDto })
+  @Roles(RoleEnum.S2_KULIAH)
+  @Get()
+  async getTugasByKelasId(
+    @Req() req: Request,
+    @Query() query: GetTugasByKelasIdQueryDto,
+  ) {
+    const { id } = req.user as AuthDto;
+
+    return this.tugasService.getTugasByKelasId(
+      query.kelasId,
+      id,
+      query.search || "",
+      query.page || 1,
+      query.limit || 10,
+    );
+  }
+}
diff --git a/src/tugas/tugas.dto.ts b/src/tugas/tugas.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..afbb6eff67825b726e20169db6dee9e73d79f0ce
--- /dev/null
+++ b/src/tugas/tugas.dto.ts
@@ -0,0 +1,109 @@
+import {
+  ApiProperty,
+  ApiPropertyOptional,
+  OmitType,
+  PickType,
+} from "@nestjs/swagger";
+import { Type } from "class-transformer";
+import {
+  IsNumberString,
+  IsOptional,
+  IsString,
+  IsUUID,
+  ValidateNested,
+} from "class-validator";
+import { BerkasTugas } from "src/entities/berkasTugas.entity";
+import { Kelas } from "src/entities/kelas.entity";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { Tugas } from "src/entities/tugas.entity";
+import { GetKelasRespDto } from "src/kelas/kelas.dto";
+
+class BerkasTugasWithoutId extends OmitType(BerkasTugas, ["id"] as const) {}
+
+export class CreateTugasDto extends PickType(Tugas, [
+  "judul",
+  "waktuMulai",
+  "waktuSelesai",
+  "deskripsi",
+  "kelasId",
+]) {
+  @ApiProperty({ type: [BerkasTugasWithoutId] })
+  @ValidateNested({ each: true })
+  @Type(() => BerkasTugasWithoutId)
+  berkasTugas: BerkasTugasWithoutId[];
+}
+
+export class UpdateTugasDto extends OmitType(CreateTugasDto, [
+  "kelasId",
+] as const) {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  id: string;
+}
+
+export class TugasIdDto extends PickType(Tugas, ["id"] as const) {}
+
+class PickedPengajarKelas extends PickType(Pengguna, ["id", "nama"] as const) {}
+
+class PickedTugasKelas extends PickType(Kelas, [
+  "id",
+  "nomor",
+  "mataKuliah",
+] as const) {}
+
+export class GetTugasByIdRespDto extends PickType(Tugas, [
+  "id",
+  "judul",
+  "waktuMulai",
+  "waktuSelesai",
+  "deskripsi",
+  "createdAt",
+  "updatedAt",
+  "berkasTugas",
+] as const) {
+  @ApiProperty({ type: PickedPengajarKelas })
+  pembuat: PickedPengajarKelas;
+
+  @ApiProperty({ type: PickedPengajarKelas })
+  pengubah: PickedPengajarKelas;
+
+  @ApiProperty({ type: PickedTugasKelas })
+  kelas: PickedTugasKelas;
+}
+
+export class GetTugasByKelasIdQueryDto extends PickType(Tugas, [
+  "kelasId",
+] as const) {
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsString()
+  search?: string;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 1" })
+  page?: number;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 10" })
+  limit?: number;
+}
+
+export class GetTugasSummaryRespDto extends PickType(Tugas, [
+  "id",
+  "judul",
+  "waktuMulai",
+  "waktuSelesai",
+] as const) {
+  @ApiProperty()
+  totalSubmisi: number;
+}
+
+export class GetTugasByKelasIdRespDto {
+  @ApiProperty({ type: [GetTugasSummaryRespDto] })
+  tugas: GetTugasSummaryRespDto[];
+
+  @ApiProperty({ type: GetKelasRespDto })
+  kelas: GetKelasRespDto;
+}
diff --git a/src/tugas/tugas.module.ts b/src/tugas/tugas.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3a8d49258c546ffaa9d9d95014f4d10af9190442
--- /dev/null
+++ b/src/tugas/tugas.module.ts
@@ -0,0 +1,44 @@
+import { Module } from "@nestjs/common";
+import { TugasController } from "./tugas.controller";
+import { TugasService } from "./tugas.service";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { PengajarKelas } from "src/entities/pengajarKelas.entity";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+import { Tugas } from "src/entities/tugas.entity";
+import { SubmisiTugas } from "src/entities/submisiTugas.entity";
+import { AuthModule } from "src/auth/auth.module";
+import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+import { BerkasTugas } from "src/entities/berkasTugas.entity";
+import { BerkasSubmisiTugas } from "src/entities/berkasSubmisiTugas.entity";
+import { Kelas } from "src/entities/kelas.entity";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { KelasModule } from "src/kelas/kelas.module";
+import { KelasService } from "src/kelas/kelas.service";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { Konfigurasi } from "src/entities/konfigurasi.entity";
+
+@Module({
+  imports: [
+    TypeOrmModule.forFeature([
+      PengajarKelas,
+      MahasiswaKelas,
+      Tugas,
+      SubmisiTugas,
+      BerkasTugas,
+      BerkasSubmisiTugas,
+      Kelas,
+      Pengguna,
+      MataKuliah,
+      Konfigurasi,
+    ]),
+    AuthModule,
+    KonfigurasiModule,
+    KelasModule,
+  ],
+  controllers: [TugasController],
+  providers: [TugasService, CustomStrategy, KelasService, KonfigurasiService],
+  exports: [TugasService],
+})
+export class TugasModule {}
diff --git a/src/tugas/tugas.service.ts b/src/tugas/tugas.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f99625c045bdc011de895afd60bd01371658ecb
--- /dev/null
+++ b/src/tugas/tugas.service.ts
@@ -0,0 +1,277 @@
+import {
+  BadRequestException,
+  ForbiddenException,
+  Injectable,
+  NotFoundException,
+} from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Tugas } from "src/entities/tugas.entity";
+import { Repository } from "typeorm";
+import {
+  CreateTugasDto,
+  GetTugasByIdRespDto,
+  GetTugasByKelasIdRespDto,
+  GetTugasSummaryRespDto,
+  TugasIdDto,
+  UpdateTugasDto,
+} from "./tugas.dto";
+import { BerkasTugas } from "src/entities/berkasTugas.entity";
+import { SubmisiTugas } from "src/entities/submisiTugas.entity";
+import { PengajarKelas } from "src/entities/pengajarKelas.entity";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+import { Kelas } from "src/entities/kelas.entity";
+import { Pengguna } from "src/entities/pengguna.entity";
+import { KelasService } from "src/kelas/kelas.service";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import * as dayjs from "dayjs";
+
+@Injectable()
+export class TugasService {
+  constructor(
+    @InjectRepository(Tugas) private tugasRepo: Repository<Tugas>,
+    @InjectRepository(BerkasTugas)
+    private berkasTugasRepo: Repository<BerkasTugas>,
+    @InjectRepository(SubmisiTugas)
+    private submisiTugasRepo: Repository<SubmisiTugas>,
+    @InjectRepository(PengajarKelas)
+    private doskelRepo: Repository<PengajarKelas>,
+    @InjectRepository(MahasiswaKelas)
+    private mahasiswaKelasRepo: Repository<MahasiswaKelas>,
+    @InjectRepository(Kelas)
+    private kelasRepo: Repository<Kelas>,
+    @InjectRepository(Pengguna)
+    private penggunaRepo: Repository<Pengguna>,
+    private kelasService: KelasService,
+    private konfService: KonfigurasiService,
+  ) {}
+
+  private async isPengajarKelasOrFail(pengajarId: string, kelasId: string) {
+    const periode = await this.konfService.getPeriodeOrFail();
+
+    const doskel = await this.doskelRepo.findOne({
+      where: {
+        pengajarId,
+        kelasId,
+        kelas: {
+          periode,
+        },
+      },
+      relations: ["kelas"],
+    });
+
+    if (!doskel) {
+      throw new ForbiddenException("Anda tidak memiliki akses");
+    }
+  }
+
+  private async isMahasiswaKelasOrFail(mahasiswaId: string, kelasId: string) {
+    const periode = await this.konfService.getPeriodeOrFail();
+
+    const mahasiswaKelas = await this.mahasiswaKelasRepo.findOne({
+      where: {
+        mahasiswaId,
+        kelasId,
+        kelas: {
+          periode,
+        },
+      },
+      relations: ["kelas"],
+    });
+
+    if (!mahasiswaKelas) {
+      throw new ForbiddenException("Anda tidak memiliki akses");
+    }
+  }
+
+  async isPengajarTugasOrFail(pengajarId: string, tugasId: string) {
+    const tugas = await this.tugasRepo.findOne({
+      where: { id: tugasId },
+    });
+
+    if (!tugas) {
+      throw new NotFoundException("Tugas tidak ditemukan");
+    }
+
+    await this.isPengajarKelasOrFail(pengajarId, tugas.kelasId);
+  }
+
+  async isMahasiswaTugasOrFail(mahasiswaId: string, tugasId: string) {
+    const tugas = await this.tugasRepo.findOne({
+      where: { id: tugasId },
+    });
+
+    if (!tugas) {
+      throw new NotFoundException("Tugas tidak ditemukan");
+    }
+
+    return await this.isMahasiswaKelasOrFail(mahasiswaId, tugas.kelasId);
+  }
+
+  private async getTugas(tugasId: string): Promise<GetTugasByIdRespDto> {
+    const result: GetTugasByIdRespDto[] = await this.tugasRepo
+      .createQueryBuilder("tugas")
+      .leftJoinAndSelect("tugas.pembuat", "pembuat")
+      .leftJoinAndSelect("tugas.pengubah", "pengubah")
+      .leftJoinAndSelect("tugas.kelas", "kelas")
+      .leftJoinAndSelect("kelas.mataKuliah", "mataKuliah")
+      .leftJoinAndSelect("tugas.berkasTugas", "berkasTugas")
+      .select([
+        "tugas.id",
+        "pembuat.id",
+        "pembuat.nama",
+        "pengubah.id",
+        "pengubah.nama",
+        "tugas.judul",
+        "tugas.waktuMulai",
+        "tugas.waktuSelesai",
+        "tugas.deskripsi",
+        "tugas.createdAt",
+        "tugas.updatedAt",
+        "berkasTugas",
+        "kelas.id",
+        "kelas.nomor",
+        "mataKuliah.kode",
+        "mataKuliah.nama",
+      ])
+      .where("tugas.id = :tugasId", { tugasId })
+      .getMany();
+
+    return result[0];
+  }
+
+  async createTugas(
+    createDto: CreateTugasDto,
+    pembuatId: string,
+  ): Promise<TugasIdDto> {
+    await this.isPengajarKelasOrFail(pembuatId, createDto.kelasId);
+
+    if (dayjs(createDto.waktuMulai).isAfter(dayjs(createDto.waktuSelesai))) {
+      throw new BadRequestException(
+        "Waktu mulai tidak boleh setelah waktu selesai",
+      );
+    }
+
+    const kelas = await this.kelasRepo.findOne({
+      where: { id: createDto.kelasId },
+    });
+
+    const pembuat = await this.penggunaRepo.findOne({
+      where: { id: pembuatId },
+    });
+
+    const berkasTugas = createDto.berkasTugas.map((berkas) =>
+      this.berkasTugasRepo.create(berkas),
+    );
+
+    const tugas = this.tugasRepo.create({
+      ...createDto,
+      kelas,
+      pembuat,
+      pengubah: pembuat,
+      berkasTugas,
+    });
+
+    const result = await this.tugasRepo.save(tugas);
+
+    return { id: result.id };
+  }
+
+  async updateTugasById(
+    updateDto: UpdateTugasDto,
+    pengubahId: string,
+  ): Promise<TugasIdDto> {
+    await this.isPengajarTugasOrFail(pengubahId, updateDto.id);
+
+    if (dayjs(updateDto.waktuMulai).isAfter(dayjs(updateDto.waktuSelesai))) {
+      throw new BadRequestException(
+        "Waktu mulai tidak boleh setelah waktu selesai",
+      );
+    }
+
+    const berkasTugas = updateDto.berkasTugas.map((berkas) =>
+      this.berkasTugasRepo.create(berkas),
+    );
+
+    const pengubah = await this.penggunaRepo.findOne({
+      where: { id: pengubahId },
+    });
+
+    const prevTugas = await this.tugasRepo.findOne({
+      where: { id: updateDto.id },
+      relations: ["kelas", "pembuat"],
+    });
+
+    const data = {
+      ...updateDto,
+      updatedAt: new Date(),
+      pengubah,
+      kelas: prevTugas.kelas,
+      pembuat: prevTugas.pembuat,
+      berkasTugas,
+    };
+
+    await this.tugasRepo.save(data);
+
+    return { id: updateDto.id };
+  }
+
+  async getTugasById(id: string, idMahasiswa?: string, idPengajar?: string) {
+    if (idMahasiswa) {
+      await this.isMahasiswaTugasOrFail(idMahasiswa, id);
+    }
+
+    if (idPengajar) {
+      await this.isPengajarTugasOrFail(idPengajar, id);
+    }
+
+    const result = await this.getTugas(id);
+
+    return result;
+  }
+
+  async getTugasByKelasId(
+    kelasId: string,
+    idPengajar: string,
+    search: string,
+    page: number,
+    limit: number,
+  ): Promise<GetTugasByKelasIdRespDto> {
+    await this.isPengajarKelasOrFail(idPengajar, kelasId);
+
+    const kelasQuery = this.kelasService.getById(kelasId);
+    const tugasQuery = await this.tugasRepo
+      .createQueryBuilder("tugas")
+      .leftJoinAndSelect(
+        "tugas.submisiTugas",
+        "submisi_tugas",
+        "submisi_tugas.isSubmitted = true",
+      )
+      .select([
+        "tugas.id AS id",
+        "tugas.judul AS judul",
+        "tugas.waktuMulai AS waktu_mulai",
+        "tugas.waktuSelesai AS waktu_selesai",
+        "COUNT(submisi_tugas) AS total_submisi",
+      ])
+      .where("tugas.kelasId = :kelasId", {
+        kelasId,
+      })
+      .andWhere("tugas.judul ILIKE :search", { search: `%${search}%` })
+      .groupBy("tugas.id")
+      .orderBy("tugas.createdAt", "DESC")
+      .limit(limit)
+      .skip((page - 1) * limit)
+      .getRawMany();
+
+    const [kelas, tugas] = await Promise.all([kelasQuery, tugasQuery]);
+    const mappedTugas: GetTugasSummaryRespDto[] = tugas.map((tugas) => ({
+      id: tugas.id,
+      judul: tugas.judul,
+      waktuMulai: tugas.waktu_mulai,
+      waktuSelesai: tugas.waktu_selesai,
+      totalSubmisi: parseInt(tugas.total_submisi),
+    }));
+
+    return { kelas, tugas: mappedTugas };
+  }
+}