diff --git a/src/app.module.ts b/src/app.module.ts
index b783ea53a60cf8640420a25453d4bf4ba6c64472..e2718653c5b8f7856d0baf4d258e4cacf378060b 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -8,7 +8,7 @@ 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 { Ruangan } from "./entities/ruangan.entity";
@@ -23,12 +23,15 @@ import { BimbinganModule } from "./bimbingan/bimbingan.module";
 import { Konfigurasi } from "./entities/konfigurasi.entity";
 import { KonfigurasiModule } from "./konfigurasi/konfigurasi.module";
 import { validate } from "./env.validation";
-import { BerkasBimbingan } from "./entities/berkasBimbingan";
-import { MataKuliah } from "./entities/mataKuliah";
-import { SubmisiTugas } from "./entities/submisiTugas";
+import { BerkasBimbingan } from "./entities/berkasBimbingan.entity";
+import { MataKuliah } from "./entities/mataKuliah.entity";
+import { SubmisiTugas } from "./entities/submisiTugas.entity";
+import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas.entity";
+import { BerkasTugas } from "./entities/berkasTugas.entity";
+import { TugasModule } from "./tugas/tugas.module";
 import { KelasModule } from "./kelas/kelas.module";
-import { BerkasSubmisiTugas } from "./entities/berkasSubmisiTugas";
-import { BerkasTugas } from "./entities/berkasTugas";
+import { 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";
 
@@ -73,6 +76,9 @@ import { DosenBimbinganModule } from "./dosen-bimbingan/dosen-bimbingan.module";
     BimbinganModule,
     KonfigurasiModule,
     KelasModule,
+    TugasModule,
+    SubmisiTugasModule,
+    NilaiModule,
     DosenBimbinganModule,
   ],
   controllers: [AppController],
diff --git a/src/bimbingan/bimbingan.dto.ts b/src/bimbingan/bimbingan.dto.ts
index 7409b67e8c4293b1d0565e02b61883b668acde42..f3afcdac8aeeb0380633e5609bc7b35d8c864013 100644
--- a/src/bimbingan/bimbingan.dto.ts
+++ b/src/bimbingan/bimbingan.dto.ts
@@ -14,7 +14,7 @@ import {
   PickType,
 } from "@nestjs/swagger";
 import { Type } from "class-transformer";
-import { BerkasBimbingan } from "src/entities/berkasBimbingan";
+import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
 import { Bimbingan, BimbinganStatus } from "src/entities/bimbingan.entity";
 import {
   JalurEnum,
diff --git a/src/bimbingan/bimbingan.module.ts b/src/bimbingan/bimbingan.module.ts
index 25e02da5c5205c69b800de0b59e223c65948519c..30174c69ff5a33b4a5caa742095ef9f830c5eb8d 100644
--- a/src/bimbingan/bimbingan.module.ts
+++ b/src/bimbingan/bimbingan.module.ts
@@ -6,7 +6,7 @@ import { Bimbingan } from "src/entities/bimbingan.entity";
 import { PendaftaranTesis } from "src/entities/pendaftaranTesis.entity";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
 import { DosenBimbingan } from "src/entities/dosenBimbingan.entity";
-import { BerkasBimbingan } from "src/entities/berkasBimbingan";
+import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
 
 @Module({
   imports: [
diff --git a/src/bimbingan/bimbingan.service.ts b/src/bimbingan/bimbingan.service.ts
index 1947b81ab45a475f276c6a8d58c4ee043d8aaec6..52c1ba28e98f9c71aad2d88e5ebeafb8d1cb1d40 100644
--- a/src/bimbingan/bimbingan.service.ts
+++ b/src/bimbingan/bimbingan.service.ts
@@ -24,7 +24,7 @@ import {
   UpdateStatusDto,
   UpdateStatusResDto,
 } from "./bimbingan.dto";
-import { BerkasBimbingan } from "src/entities/berkasBimbingan";
+import { BerkasBimbingan } from "src/entities/berkasBimbingan.entity";
 
 @Injectable()
 export class BimbinganService {
diff --git a/src/entities/berkasBimbingan.ts b/src/entities/berkasBimbingan.entity.ts
similarity index 77%
rename from src/entities/berkasBimbingan.ts
rename to src/entities/berkasBimbingan.entity.ts
index 6acceea0c51e5cfaa149724cb5f7824fa331544c..35b44ace3f7a70db3a1bcbd210c3a95be701c99a 100644
--- a/src/entities/berkasBimbingan.ts
+++ b/src/entities/berkasBimbingan.entity.ts
@@ -9,7 +9,9 @@ export class BerkasBimbingan {
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
-  @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id)
+  @ManyToOne(() => Bimbingan, (bimbingan) => bimbingan.id, {
+    orphanedRowAction: "delete",
+  })
   bimbingan: Bimbingan;
 
   @Column({ type: "text" })
@@ -19,6 +21,6 @@ export class BerkasBimbingan {
 
   @Column({ type: "text" })
   @IsUrl()
-  @ApiProperty()
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
   url: string;
 }
diff --git a/src/entities/berkasSubmisiTugas.entity.ts b/src/entities/berkasSubmisiTugas.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e48d5e8d88f1449b767d3e88f1f9862e8e0f6560
--- /dev/null
+++ b/src/entities/berkasSubmisiTugas.entity.ts
@@ -0,0 +1,28 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { SubmisiTugas } from "./submisiTugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsString, IsUUID } from "@nestjs/class-validator";
+import { IsUrl } from "class-validator";
+
+@Entity()
+export class BerkasSubmisiTugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(() => SubmisiTugas, (submisi) => submisi.id, {
+    orphanedRowAction: "delete",
+  })
+  submisiTugas: SubmisiTugas;
+
+  @ApiProperty()
+  @IsString()
+  @Column({ type: "text" })
+  nama: string;
+
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  @IsUrl()
+  @Column({ type: "text" })
+  url: string;
+}
diff --git a/src/entities/berkasSubmisiTugas.ts b/src/entities/berkasSubmisiTugas.ts
deleted file mode 100644
index 9694eb7069548036483845915c5a61b8fc7e2a75..0000000000000000000000000000000000000000
--- a/src/entities/berkasSubmisiTugas.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { SubmisiTugas } from "./submisiTugas";
-
-@Entity()
-export class BerkasSubmisiTugas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => SubmisiTugas, (submisi) => submisi.id)
-  submisiTugas: SubmisiTugas;
-
-  @Column({ type: "text" })
-  nama: string;
-
-  @Column({ type: "text" })
-  url: string;
-}
diff --git a/src/entities/berkasTugas.entity.ts b/src/entities/berkasTugas.entity.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c18bccfa9c839754ca8ed7bb0f5789f8841e3ff1
--- /dev/null
+++ b/src/entities/berkasTugas.entity.ts
@@ -0,0 +1,26 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
+import { Tugas } from "./tugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsUrl, IsUUID } from "class-validator";
+import { IsString } from "@nestjs/class-validator";
+
+@Entity()
+export class BerkasTugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @PrimaryGeneratedColumn("uuid")
+  id: string;
+
+  @ManyToOne(() => Tugas, (tugas) => tugas.id, { orphanedRowAction: "delete" })
+  tugas: Tugas;
+
+  @ApiProperty()
+  @IsString()
+  @Column({ type: "text" })
+  nama: string;
+
+  @ApiProperty({ example: "https://example.com/berkas.pdf" })
+  @IsUrl()
+  @Column({ type: "text" })
+  url: string;
+}
diff --git a/src/entities/berkasTugas.ts b/src/entities/berkasTugas.ts
deleted file mode 100644
index 91cf3ec86a5b6d8f3b243e6d076e41bfa39adcbd..0000000000000000000000000000000000000000
--- a/src/entities/berkasTugas.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
-import { Tugas } from "./tugas.entity";
-
-@Entity()
-export class BerkasTugas {
-  @PrimaryGeneratedColumn("uuid")
-  id: string;
-
-  @ManyToOne(() => Tugas, (tugas) => tugas.id)
-  tugas: Tugas;
-
-  @Column({ type: "text" })
-  nama: string;
-
-  @Column({ type: "text" })
-  url: string;
-}
diff --git a/src/entities/bimbingan.entity.ts b/src/entities/bimbingan.entity.ts
index 33df7a17badd1b84d408bb313c47c46fad2410b7..a846e824d3d98389e6a2402efad351f49188f9fb 100644
--- a/src/entities/bimbingan.entity.ts
+++ b/src/entities/bimbingan.entity.ts
@@ -7,7 +7,7 @@ import {
 } from "typeorm";
 import { PendaftaranTesis } from "./pendaftaranTesis.entity";
 import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
-import { BerkasBimbingan } from "./berkasBimbingan";
+import { BerkasBimbingan } from "./berkasBimbingan.entity";
 
 export enum BimbinganStatus {
   LANCAR = "LANCAR",
diff --git a/src/entities/kelas.entity.ts b/src/entities/kelas.entity.ts
index cc9df1e26aee879759d3260c02fa58378c7a5238..deec327ed9a9028f7eb8ddd886826f35a3294c80 100644
--- a/src/entities/kelas.entity.ts
+++ b/src/entities/kelas.entity.ts
@@ -6,10 +6,10 @@ import {
   OneToMany,
   PrimaryGeneratedColumn,
 } from "typeorm";
-import { MataKuliah } from "./mataKuliah";
+import { MataKuliah } from "./mataKuliah.entity";
 import { ApiProperty } from "@nestjs/swagger";
 import { PengajarKelas } from "./pengajarKelas.entity";
-import { MahasiswaKelas } from "./mahasiswaKelas";
+import { MahasiswaKelas } from "./mahasiswaKelas.entity";
 import {
   IsPositive,
   IsString,
@@ -17,6 +17,7 @@ import {
   Length,
   MaxLength,
 } from "@nestjs/class-validator";
+import { Tugas } from "./tugas.entity";
 
 @Entity()
 export class Kelas {
@@ -35,6 +36,7 @@ export class Kelas {
   @Column({ type: "text" })
   periode: string;
 
+  @ApiProperty({ type: MataKuliah })
   @ManyToOne(() => MataKuliah, (mataKuliah) => mataKuliah.kode)
   @JoinColumn({ name: "mataKuliahKode" })
   mataKuliah: MataKuliah;
@@ -56,4 +58,7 @@ export class Kelas {
 
   @OneToMany(() => MahasiswaKelas, (mahasiswa) => mahasiswa.kelas)
   mahasiswa: MahasiswaKelas[];
+
+  @OneToMany(() => Tugas, (tugas) => tugas.kelas)
+  tugas: Tugas[];
 }
diff --git a/src/entities/mahasiswaKelas.ts b/src/entities/mahasiswaKelas.entity.ts
similarity index 96%
rename from src/entities/mahasiswaKelas.ts
rename to src/entities/mahasiswaKelas.entity.ts
index 7d47e0abfa6461f68fc0fa11ef736601cd397c93..0753aa6f75f865913b7e42209248310948dc67c2 100644
--- a/src/entities/mahasiswaKelas.ts
+++ b/src/entities/mahasiswaKelas.entity.ts
@@ -28,5 +28,5 @@ export class MahasiswaKelas {
   mahasiswaId: string;
 
   @Column({ type: "real", nullable: true })
-  nilaiAkhir: number;
+  nilaiAkhir?: number;
 }
diff --git a/src/entities/mataKuliah.ts b/src/entities/mataKuliah.entity.ts
similarity index 70%
rename from src/entities/mataKuliah.ts
rename to src/entities/mataKuliah.entity.ts
index 6f78c692074b3a7c80123deac69c84643c1c7a11..9e1fed5d807a176b89288144623ec72575ef5e33 100644
--- a/src/entities/mataKuliah.ts
+++ b/src/entities/mataKuliah.entity.ts
@@ -1,6 +1,7 @@
 import { ApiProperty } from "@nestjs/swagger";
 import { IsString, Length, MaxLength } from "class-validator";
-import { Column, Entity, PrimaryColumn } from "typeorm";
+import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
+import { Kelas } from "./kelas.entity";
 
 @Entity()
 export class MataKuliah {
@@ -15,4 +16,7 @@ export class MataKuliah {
   @MaxLength(256)
   @Column({ type: "varchar", length: 256 })
   nama: string;
+
+  @OneToMany(() => Kelas, (kelas) => kelas.mataKuliah)
+  kelas: Kelas[];
 }
diff --git a/src/entities/pendaftaranTesis.entity.ts b/src/entities/pendaftaranTesis.entity.ts
index e7414aabc036cdc7d2a2a1344bcbabb1fd7ace88..637a05e71e7be84615568bcaef45f0adc2d14e2c 100644
--- a/src/entities/pendaftaranTesis.entity.ts
+++ b/src/entities/pendaftaranTesis.entity.ts
@@ -55,7 +55,7 @@ export class PendaftaranTesis {
   @Column({ type: "enum", enum: RegStatus, default: RegStatus.NOT_ASSIGNED })
   status: RegStatus;
 
-  @ApiProperty()
+  @ApiProperty({ type: Topik })
   @ManyToOne(() => Topik, (topik) => topik.id)
   topik: Topik;
 
diff --git a/src/entities/pengguna.entity.ts b/src/entities/pengguna.entity.ts
index 9039cf4295dcf27135972b7ba4fc82ce6206e7b6..c28a606ea4031436e36fc7958c0756a78f898cbe 100644
--- a/src/entities/pengguna.entity.ts
+++ b/src/entities/pengguna.entity.ts
@@ -4,7 +4,9 @@ import {
   ApiProperty,
   ApiPropertyOptional,
 } from "@nestjs/swagger";
-import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
+import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
+import { PendaftaranTesis } from "./pendaftaranTesis.entity";
+import { SubmisiTugas } from "./submisiTugas.entity";
 
 export enum RoleEnum {
   ADMIN = "ADMIN",
@@ -56,4 +58,10 @@ export class Pengguna {
   @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/submisiTugas.ts b/src/entities/submisiTugas.entity.ts
similarity index 50%
rename from src/entities/submisiTugas.ts
rename to src/entities/submisiTugas.entity.ts
index ef0c8da1a854dc6b985b23b313a65eaba2aa7d22..be380d37d6c0b4dadf641f794735a343990e13b5 100644
--- a/src/entities/submisiTugas.ts
+++ b/src/entities/submisiTugas.entity.ts
@@ -1,34 +1,53 @@
 import {
   Column,
   Entity,
+  JoinColumn,
   ManyToOne,
   OneToMany,
   PrimaryGeneratedColumn,
 } from "typeorm";
 import { Pengguna } from "./pengguna.entity";
 import { Tugas } from "./tugas.entity";
-import { BerkasSubmisiTugas } from "./berkasSubmisiTugas";
+import { BerkasSubmisiTugas } from "./berkasSubmisiTugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsString, IsUUID } from "class-validator";
+import { IsBoolean } from "@nestjs/class-validator";
 
 @Entity()
 export class SubmisiTugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "mahasiswaId" })
   mahasiswa: Pengguna;
 
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  mahasiswaId: string;
+
+  @ApiProperty()
+  @IsString()
   @Column({ type: "text" })
   jawaban: string;
 
+  @ApiProperty({ description: "true means submitted, false means draft" })
+  @IsBoolean()
   @Column({ type: "boolean" })
   isSubmitted: boolean; // false means draft (saved), true means submitted
 
+  @ApiProperty({ type: [BerkasSubmisiTugas] })
   @OneToMany(
     () => BerkasSubmisiTugas,
     (berkasSubmisiTugas) => berkasSubmisiTugas.submisiTugas,
+    { cascade: true },
   )
   berkasSubmisiTugas: BerkasSubmisiTugas[];
 
+  @ApiProperty()
   @Column({
     type: "timestamptz",
     default: () => "CURRENT_TIMESTAMP",
@@ -37,5 +56,11 @@ export class SubmisiTugas {
   submittedAt: Date;
 
   @ManyToOne(() => Tugas, (tugas) => tugas.id)
+  @JoinColumn({ name: "tugasId" })
   tugas: Tugas;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  tugasId: string;
 }
diff --git a/src/entities/tugas.entity.ts b/src/entities/tugas.entity.ts
index 563bcb32a0d12b55e6e3a30232bea75a162a233d..2424b87973fd01fe854e40ca50079dbc40e0f4ee 100644
--- a/src/entities/tugas.entity.ts
+++ b/src/entities/tugas.entity.ts
@@ -1,46 +1,88 @@
 import {
   Column,
   Entity,
+  JoinColumn,
   ManyToOne,
   OneToMany,
   PrimaryGeneratedColumn,
 } from "typeorm";
 import { Kelas } from "./kelas.entity";
 import { Pengguna } from "./pengguna.entity";
-import { BerkasTugas } from "./berkasTugas";
+import { BerkasTugas } from "./berkasTugas.entity";
+import { ApiProperty } from "@nestjs/swagger";
+import { IsDateString, IsUUID, MaxLength } from "class-validator";
+import { IsString } from "@nestjs/class-validator";
+import { SubmisiTugas } from "./submisiTugas.entity";
 
 @Entity()
 export class Tugas {
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
   @PrimaryGeneratedColumn("uuid")
   id: string;
 
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "pembuatId" })
   pembuat: Pengguna;
 
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  pembuatId: string;
+
   @ManyToOne(() => Pengguna, (pengguna) => pengguna.id)
+  @JoinColumn({ name: "pengubahId" })
   pengubah: Pengguna;
 
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  pengubahId: string;
+
+  @ApiProperty()
+  @IsString()
+  @MaxLength(256)
   @Column({ type: "varchar", length: 256 })
   judul: string;
 
+  @ApiProperty()
+  @IsDateString()
   @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
   waktuMulai: Date;
 
+  @ApiProperty()
+  @IsDateString()
   @Column({ type: "timestamptz" })
   waktuSelesai: Date;
 
+  @ApiProperty()
+  @IsString()
   @Column({ type: "text" })
   deskripsi: string;
 
-  @OneToMany(() => BerkasTugas, (berkasTugas) => berkasTugas.tugas)
+  @ApiProperty({ type: [BerkasTugas] })
+  @OneToMany(() => BerkasTugas, (berkasTugas) => berkasTugas.tugas, {
+    cascade: true,
+  })
   berkasTugas: BerkasTugas[];
 
+  @ApiProperty()
   @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
   createdAt: Date;
 
+  @ApiProperty()
   @Column({ type: "timestamptz", default: () => "CURRENT_TIMESTAMP" })
   updatedAt: Date;
 
   @ManyToOne(() => Kelas, (kelas) => kelas.id)
+  @JoinColumn({ name: "kelasId" })
   kelas: Kelas;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  @IsUUID()
+  @Column()
+  kelasId: string;
+
+  @OneToMany(() => SubmisiTugas, (submisiTugas) => submisiTugas.tugas)
+  submisiTugas: SubmisiTugas[];
 }
diff --git a/src/kelas/kelas.controller.ts b/src/kelas/kelas.controller.ts
index 88460f5dd5acbe94f91341ea795f387f2d02fe17..ae942b029bd3415ece79a2d5df0b3b4ad5befd3a 100644
--- a/src/kelas/kelas.controller.ts
+++ b/src/kelas/kelas.controller.ts
@@ -12,10 +12,12 @@ import {
   UseGuards,
 } from "@nestjs/common";
 import {
+  ByIdKelasDto,
   CreateKelasDto,
   DeleteKelasDto,
+  GetKelasDetailRespDto,
   GetKelasQueryDto,
-  GetListKelasRespDto,
+  GetKelasRespDto,
   GetNextNomorResDto,
   IdKelasResDto,
   KodeRespDto,
@@ -32,13 +34,14 @@ import {
   ApiInternalServerErrorResponse,
   ApiNotFoundResponse,
   ApiOkResponse,
+  ApiOperation,
   ApiTags,
 } from "@nestjs/swagger";
 import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
 import { RolesGuard } from "src/middlewares/roles.guard";
 import { KelasService } from "./kelas.service";
 import { Roles } from "src/middlewares/roles.decorator";
-import { MataKuliah } from "src/entities/mataKuliah";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
 import { Kelas } from "src/entities/kelas.entity";
 
 @ApiTags("Kelas")
@@ -49,7 +52,11 @@ import { Kelas } from "src/entities/kelas.entity";
 export class KelasController {
   constructor(private readonly kelasServ: KelasService) {}
 
-  @ApiOkResponse({ type: GetListKelasRespDto, isArray: true })
+  @ApiOperation({
+    summary:
+      "Get list of kelas. Roles: S2_KULIAH, S2_MAHASISWA, S2_TIM_TESIS, ADMIN",
+  })
+  @ApiOkResponse({ type: GetKelasRespDto, isArray: true })
   @Roles(
     RoleEnum.S2_KULIAH,
     RoleEnum.S2_MAHASISWA,
@@ -148,4 +155,77 @@ export class KelasController {
   async delete(@Body() body: DeleteKelasDto): Promise<Kelas> {
     return await this.kelasServ.delete(body);
   }
+
+  @Roles(
+    RoleEnum.S2_TIM_TESIS,
+    RoleEnum.ADMIN,
+    RoleEnum.S2_KULIAH,
+    RoleEnum.S2_MAHASISWA,
+  )
+  @ApiOkResponse({ type: GetKelasRespDto })
+  @ApiOperation({
+    summary:
+      "Get kelas general information by kelas id. Roles: S2_KULIAH, S2_MAHASISWA, S2_TIM_TESIS, ADMIN",
+  })
+  @Get("/:id")
+  async getById(
+    @Param() param: ByIdKelasDto,
+    @Req() req: Request,
+  ): Promise<GetKelasRespDto> {
+    let idMahasiswa = undefined;
+    let idPengajar = undefined;
+
+    const { id, roles } = req.user as AuthDto;
+
+    if (
+      !roles.includes(RoleEnum.S2_TIM_TESIS) &&
+      !roles.includes(RoleEnum.ADMIN)
+    ) {
+      if (roles.includes(RoleEnum.S2_KULIAH)) {
+        idPengajar = id;
+      } else {
+        // requester only has S2_MAHASISWA access
+        idMahasiswa = id;
+      }
+    }
+
+    return await this.kelasServ.getById(param.id, idMahasiswa, idPengajar);
+  }
+
+  @Roles(
+    RoleEnum.S2_TIM_TESIS,
+    RoleEnum.ADMIN,
+    RoleEnum.S2_KULIAH,
+    RoleEnum.S2_MAHASISWA,
+  )
+  @ApiOperation({
+    summary:
+      "Get kelas mahasiswa and pengajar list by kelas id. Roles: S2_KULIAH, S2_MAHASISWA, S2_TIM_TESIS, ADMIN",
+  })
+  @ApiOkResponse({ type: GetKelasDetailRespDto })
+  @Get("/:id/detail")
+  async getKelasDetail(@Param() param: ByIdKelasDto, @Req() req: Request) {
+    let idMahasiswa = undefined;
+    let idPengajar = undefined;
+
+    const { id, roles } = req.user as AuthDto;
+
+    if (
+      !roles.includes(RoleEnum.S2_TIM_TESIS) &&
+      !roles.includes(RoleEnum.ADMIN)
+    ) {
+      if (roles.includes(RoleEnum.S2_KULIAH)) {
+        idPengajar = id;
+      } else {
+        // requester only has S2_MAHASISWA access
+        idMahasiswa = id;
+      }
+    }
+
+    return await this.kelasServ.getKelasDetail(
+      param.id,
+      idMahasiswa,
+      idPengajar,
+    );
+  }
 }
diff --git a/src/kelas/kelas.dto.ts b/src/kelas/kelas.dto.ts
index e9d6b5f5b2b7b2b265d59cf87a9ec73ba3f05033..352ae36ea1a1d0c0e45ee7e9eb6eb778cd460439 100644
--- a/src/kelas/kelas.dto.ts
+++ b/src/kelas/kelas.dto.ts
@@ -6,8 +6,8 @@ import {
   ApiPropertyOptional,
 } from "@nestjs/swagger";
 import { Kelas } from "src/entities/kelas.entity";
-import { MataKuliah } from "src/entities/mataKuliah";
-import { RoleEnum } from "src/entities/pengguna.entity";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
+import { Pengguna, RoleEnum } from "src/entities/pengguna.entity";
 
 export class CreateKelasDto extends PickType(Kelas, [
   "mataKuliahKode",
@@ -43,18 +43,26 @@ export class GetKelasQueryDto {
   search: string;
 }
 
-export class GetListKelasRespDto {
+export class ByIdKelasDto extends PickType(Kelas, ["id"] as const) {}
+
+export class GetKelasRespDto {
   @ApiProperty()
   id: string;
 
   @ApiProperty({ example: "K02" })
   nomor: string;
 
-  @ApiProperty({ example: "IF4031 Pengembangan Aplikasi Terdistribusi" })
-  mata_kuliah: string;
+  @ApiProperty({ example: "IF3270" })
+  kode_mata_kuliah: string;
+
+  @ApiProperty({ example: "Pengembangan Aplikasi Terdistribusi" })
+  nama_mata_kuliah: string;
 
   @ApiProperty()
   jumlah_mahasiswa: number;
+
+  @ApiProperty({ example: "bg-blue-600/20" })
+  warna: string;
 }
 
 export class KodeRespDto extends PickType(MataKuliah, ["kode"] as const) {}
@@ -63,3 +71,22 @@ export class GetNextNomorResDto {
   @ApiProperty({ example: 2 })
   nomor: number;
 }
+
+class PickedPengajarKelasDto extends PickType(Pengguna, [
+  "id",
+  "nama",
+] as const) {}
+
+class PickedMahasiswaKelasDto extends PickType(Pengguna, [
+  "id",
+  "nama",
+  "nim",
+] as const) {}
+
+export class GetKelasDetailRespDto extends PickType(Kelas, ["id"] as const) {
+  @ApiProperty({ type: [PickedPengajarKelasDto] })
+  pengajar: PickedPengajarKelasDto[];
+
+  @ApiProperty({ type: [PickedMahasiswaKelasDto] })
+  mahasiswa: PickedMahasiswaKelasDto[];
+}
diff --git a/src/kelas/kelas.module.ts b/src/kelas/kelas.module.ts
index 80ddae351a722bc5c4aecc32508e90fcd108c9ce..38dfa27f8690f2608b1450c134536f7d3d920651 100644
--- a/src/kelas/kelas.module.ts
+++ b/src/kelas/kelas.module.ts
@@ -6,7 +6,7 @@ import { AuthModule } from "src/auth/auth.module";
 import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module";
 import { KelasController } from "./kelas.controller";
 import { CustomStrategy } from "src/middlewares/custom.strategy";
-import { MataKuliah } from "src/entities/mataKuliah";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
 
 @Module({
   imports: [
diff --git a/src/kelas/kelas.service.ts b/src/kelas/kelas.service.ts
index c771ac6324a2ac862cbc114275577a633d3a334b..fa62a4707fe36efeca92ef76b8074dd392e7a92b 100644
--- a/src/kelas/kelas.service.ts
+++ b/src/kelas/kelas.service.ts
@@ -10,12 +10,13 @@ import { Brackets, Repository } from "typeorm";
 import {
   CreateKelasDto,
   DeleteKelasDto,
-  GetListKelasRespDto,
+  GetKelasDetailRespDto,
+  GetKelasRespDto,
   IdKelasResDto,
   UpdateKelasDto,
 } from "./kelas.dto";
 import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
-import { MataKuliah } from "src/entities/mataKuliah";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
 import { CARD_COLORS } from "./kelas.constant";
 
 @Injectable()
@@ -27,20 +28,13 @@ export class KelasService {
     private mataKuliahRepo: Repository<MataKuliah>,
     private konfService: KonfigurasiService,
   ) {}
-
   async getListKelas(
     idMahasiswa?: string,
     idPengajar?: string,
     kodeMatkul?: string,
     search?: string,
   ) {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
-
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
-    }
+    const currPeriod = await this.konfService.getPeriodeOrFail();
 
     let baseQuery = this.kelasRepo
       .createQueryBuilder("kelas")
@@ -49,6 +43,7 @@ export class KelasService {
       .select([
         "kelas.id AS id",
         "kelas.nomor AS nomor",
+        "kelas.warna AS warna",
         "mataKuliah.kode AS kode_mata_kuliah",
         "mataKuliah.nama AS nama_mata_kuliah",
         "COUNT(mahasiswa) AS jumlah_mahasiswa",
@@ -95,25 +90,143 @@ export class KelasService {
       .groupBy("kelas.id, mataKuliah.kode")
       .getRawMany();
 
-    const mapped: GetListKelasRespDto[] = result.map((r) => ({
+    const mapped: GetKelasRespDto[] = result.map((r) => ({
       id: r.id,
       nomor: "K" + `${r.nomor}`.padStart(2, "0"),
-      mata_kuliah: `${r.kode_mata_kuliah} ${r.nama_mata_kuliah}`,
+      kode_mata_kuliah: r.kode_mata_kuliah,
+      nama_mata_kuliah: r.nama_mata_kuliah,
       jumlah_mahasiswa: parseInt(r.jumlah_mahasiswa),
+      warna: r.warna,
     }));
 
     return mapped;
   }
 
-  async create(createDto: CreateKelasDto): Promise<IdKelasResDto> {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
+  async getById(id: string, idMahasiswa?: string, idPengajar?: string) {
+    const currPeriod = await this.konfService.getPeriodeOrFail();
+
+    let baseQuery = this.kelasRepo
+      .createQueryBuilder("kelas")
+      .leftJoinAndSelect("kelas.mahasiswa", "mahasiswa")
+      .leftJoinAndSelect("kelas.mataKuliah", "mataKuliah")
+      .select([
+        "kelas.id AS id",
+        "kelas.nomor AS nomor",
+        "kelas.warna AS warna",
+        "mataKuliah.kode AS kode_mata_kuliah",
+        "mataKuliah.nama AS nama_mata_kuliah",
+        "COUNT(mahasiswa) AS jumlah_mahasiswa",
+      ])
+      .where("kelas.id = :id", { id })
+      .andWhere("kelas.periode = :periode", { periode: currPeriod });
+
+    if (idMahasiswa) {
+      baseQuery = baseQuery
+        .innerJoin("kelas.mahasiswa", "mahasiswa_filter")
+        .andWhere("mahasiswa_filter.mahasiswaId = :idMahasiswa", {
+          idMahasiswa,
+        });
+    }
+
+    if (idPengajar) {
+      baseQuery = baseQuery
+        .innerJoin("kelas.pengajar", "pengajar")
+        .andWhere("pengajar.pengajarId = :idPengajar", {
+          idPengajar,
+        });
+    }
+
+    const result = await baseQuery
+      .groupBy("kelas.id, mataKuliah.kode")
+      .getRawOne();
+
+    if (!result) {
+      throw new NotFoundException("Kelas tidak ditemukan");
+    }
+
+    const mapped: GetKelasRespDto = {
+      id: result.id,
+      nomor: "K" + `${result.nomor}`.padStart(2, "0"),
+      kode_mata_kuliah: result.kode_mata_kuliah,
+      nama_mata_kuliah: result.nama_mata_kuliah,
+      jumlah_mahasiswa: parseInt(result.jumlah_mahasiswa),
+      warna: result.warna,
+    };
 
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
+    return mapped;
+  }
+
+  async getKelasDetail(
+    idKelas: string,
+    idMahasiswa?: string,
+    idPengajar?: string,
+  ) {
+    const currPeriod = await this.konfService.getPeriodeOrFail();
+
+    let baseQuery = this.kelasRepo
+      .createQueryBuilder("kelas")
+      .leftJoinAndSelect("kelas.mahasiswa", "mahasiswaKelas")
+      .leftJoinAndSelect("mahasiswaKelas.mahasiswa", "mahasiswa")
+      .leftJoinAndSelect("kelas.pengajar", "pengajarKelas")
+      .leftJoinAndSelect("pengajarKelas.pengajar", "pengajar")
+      .select([
+        "kelas.id",
+        "mahasiswaKelas.id",
+        "mahasiswa.id",
+        "mahasiswa.nim",
+        "mahasiswa.nama",
+        "pengajarKelas.id",
+        "pengajar.id",
+        "pengajar.nama",
+      ])
+      .orderBy("pengajar.nama", "ASC")
+      .addOrderBy("mahasiswa.nim", "ASC")
+      .where("kelas.id = :idKelas", { idKelas })
+      .andWhere("kelas.periode = :periode", { periode: currPeriod });
+
+    if (idMahasiswa) {
+      baseQuery = baseQuery
+        .innerJoin("kelas.mahasiswa", "mahasiswaFilter")
+        .andWhere("mahasiswaFilter.mahasiswaId = :idMahasiswa", {
+          idMahasiswa,
+        });
+    }
+
+    if (idPengajar) {
+      baseQuery = baseQuery
+        .innerJoin("kelas.pengajar", "pengajarFilter")
+        .andWhere("pengajarFilter.pengajarId = :idPengajar", {
+          idPengajar,
+        });
+    }
+
+    const result = await baseQuery.getOne();
+
+    if (!result) {
+      throw new NotFoundException(
+        "Kelas tidak ditemukan di antara kelas yang dapat Anda akses",
+      );
     }
 
+    const mapped: GetKelasDetailRespDto = {
+      id: result.id,
+      pengajar: result.pengajar.map((p) => ({
+        id: p.pengajar.id,
+        nama: p.pengajar.nama,
+      })),
+      mahasiswa: result.mahasiswa.map((m) => ({
+        id: m.mahasiswa.id,
+        nim: m.mahasiswa.nim,
+        nama: m.mahasiswa.nama,
+      })),
+    };
+
+    return mapped;
+  }
+
+  async create(createDto: CreateKelasDto): Promise<IdKelasResDto> {
+    const currPeriod = await this.konfService.getPeriodeOrFail();
+
     let nomor = createDto.nomor;
     if (nomor) {
       const checkClassQueary = this.kelasRepo
@@ -159,13 +272,7 @@ export class KelasService {
   }
 
   async updateOrCreate(dto: UpdateKelasDto): Promise<IdKelasResDto> {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
-
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
-    }
+    const currPeriod = await this.konfService.getPeriodeOrFail();
 
     if (!dto.id) {
       // Create kelas
@@ -207,13 +314,7 @@ export class KelasService {
   }
 
   async delete(dto: DeleteKelasDto): Promise<Kelas> {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
-
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
-    }
+    const currPeriod = await this.konfService.getPeriodeOrFail();
 
     const kelasQuery = this.kelasRepo
       .createQueryBuilder("kelas")
@@ -243,13 +344,7 @@ export class KelasService {
   }
 
   async getNextNomorKelas(kodeMatkul: string): Promise<number> {
-    const currPeriod = await this.konfService.getKonfigurasiByKey(
-      process.env.KONF_PERIODE_KEY,
-    );
-
-    if (!currPeriod) {
-      throw new BadRequestException("Periode belum dikonfigurasi");
-    }
+    const currPeriod = await this.konfService.getPeriodeOrFail();
 
     const maxClass = await this.kelasRepo.findOne({
       where: {
diff --git a/src/konfigurasi/konfigurasi.service.ts b/src/konfigurasi/konfigurasi.service.ts
index 23ee9f8761d27dc8f257181c6f7d2abbf35de001..a8e7edaccbcafeeab8aa59f9039ce69ae88a21c2 100644
--- a/src/konfigurasi/konfigurasi.service.ts
+++ b/src/konfigurasi/konfigurasi.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from "@nestjs/common";
+import { BadRequestException, Injectable } from "@nestjs/common";
 import { InjectRepository } from "@nestjs/typeorm";
 import { Konfigurasi } from "src/entities/konfigurasi.entity";
 import { Repository } from "typeorm";
@@ -29,4 +29,16 @@ export class KonfigurasiService {
 
     return data?.value;
   }
+
+  async getPeriodeOrFail() {
+    const currPeriod = await this.getKonfigurasiByKey(
+      process.env.KONF_PERIODE_KEY,
+    );
+
+    if (!currPeriod) {
+      throw new BadRequestException("Periode belum dikonfigurasi");
+    }
+
+    return currPeriod;
+  }
 }
diff --git a/src/main.ts b/src/main.ts
index bb2da38e1a57d5422546e9ae52909898e1d51176..fdce89e3372e4a1ea2d1a7bf5006ee2054c3f487 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -27,7 +27,10 @@ async function bootstrap() {
     .addTag("Dosen Bimbingan")
     .addTag("Kelas")
     .addTag("Konfigurasi")
+    .addTag("Nilai")
     .addTag("Registrasi Tesis")
+    .addTag("Submisi Tugas")
+    .addTag("Tugas")
     .addCookieAuth(process.env.COOKIE_NAME)
     .addBearerAuth()
     .build();
diff --git a/src/middlewares/forbidden-exception.filter.ts b/src/middlewares/forbidden-exception.filter.ts
index ec5ba9ddf450d25a3698c6fbc8ac20f32f1d468b..9d6de0d98638596e89d2c171d2c28dbbc145c205 100644
--- a/src/middlewares/forbidden-exception.filter.ts
+++ b/src/middlewares/forbidden-exception.filter.ts
@@ -13,9 +13,9 @@ export class ForbiddenExceptionFilter implements ExceptionFilter {
     const response = ctx.getResponse<Response>();
     const status = exception.getStatus();
 
-    response.clearCookie(process.env.COOKIE_NAME).status(status).json({
-      message: "Forbidden",
-      status,
-    });
+    response
+      .clearCookie(process.env.COOKIE_NAME)
+      .status(status)
+      .json(exception.getResponse());
   }
 }
diff --git a/src/nilai/nilai.controller.ts b/src/nilai/nilai.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6bb1cb659a4ad0347e37c69df0ad85750da2d1ad
--- /dev/null
+++ b/src/nilai/nilai.controller.ts
@@ -0,0 +1,45 @@
+import { Body, Controller, Get, Patch, Query, UseGuards } from "@nestjs/common";
+import { NilaiService } from "./nilai.service";
+import {
+  ApiBearerAuth,
+  ApiCookieAuth,
+  ApiOkResponse,
+  ApiTags,
+} from "@nestjs/swagger";
+import { CustomAuthGuard } from "src/middlewares/custom-auth.guard";
+import { RolesGuard } from "src/middlewares/roles.guard";
+import { Roles } from "src/middlewares/roles.decorator";
+import { RoleEnum } from "src/entities/pengguna.entity";
+import {
+  GetNilaiByMatkulQueryDto,
+  GetNilaiByMatkulRespDto,
+  UpdateNilaiDto,
+  UpdateNilaiRespDto,
+} from "./nilai.dto";
+
+@ApiBearerAuth()
+@ApiCookieAuth()
+@ApiTags("Nilai")
+@UseGuards(CustomAuthGuard, RolesGuard)
+@Roles(RoleEnum.ADMIN, RoleEnum.S2_TIM_TESIS)
+@Controller("nilai")
+export class NilaiController {
+  constructor(private nilaiServ: NilaiService) {}
+
+  @ApiOkResponse({ type: [GetNilaiByMatkulRespDto] })
+  @Get()
+  async getNilaiByMatkul(@Query() query: GetNilaiByMatkulQueryDto) {
+    return this.nilaiServ.getNilaiByMatkul(
+      query.kode,
+      query.page || 1,
+      query.limit || 10,
+      query.search || "",
+    );
+  }
+
+  @ApiOkResponse({ type: UpdateNilaiRespDto })
+  @Patch()
+  async updateNilai(@Body() body: UpdateNilaiDto) {
+    return this.nilaiServ.updateNilai(body.mahasiswaKelasIds, body.nilaiAkhir);
+  }
+}
diff --git a/src/nilai/nilai.dto.ts b/src/nilai/nilai.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1b933d07468212dbfacd8fc41d441227b5084418
--- /dev/null
+++ b/src/nilai/nilai.dto.ts
@@ -0,0 +1,78 @@
+import { IsNumberString } from "@nestjs/class-validator";
+import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import {
+  IsNumber,
+  IsOptional,
+  IsPositive,
+  IsString,
+  IsUUID,
+  MaxLength,
+  MinLength,
+} from "class-validator";
+
+export class GetNilaiByMatkulQueryDto {
+  @ApiPropertyOptional({ example: "IF4031" })
+  @IsOptional()
+  @IsString()
+  @MinLength(6)
+  @MaxLength(6)
+  kode?: string;
+
+  @ApiPropertyOptional()
+  @IsOptional()
+  @IsString()
+  search?: string;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 1" })
+  page?: number;
+
+  @IsOptional()
+  @IsNumberString()
+  @ApiPropertyOptional({ description: "default: 10" })
+  limit?: number;
+}
+
+export class GetNilaiByMatkulRespDto {
+  @ApiProperty({ example: "IF4031" })
+  mata_kuliah_kode: string;
+
+  @ApiProperty({ example: "Pemrograman Berbasis Kerangka Kerja" })
+  mata_kuliah_nama: string;
+
+  @ApiProperty()
+  kelas_nomor: number;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  mahasiswa_kelas_id: string;
+
+  @ApiProperty({ example: "550e8400-e29b-41d4-a716-446655440000" })
+  mahasiswa_id: string;
+
+  @ApiProperty()
+  mahasiswa_nama: string;
+
+  @ApiProperty({ example: "13517000" })
+  mahasiswa_nim: string;
+
+  @ApiProperty()
+  nilai_akhir: string | null;
+}
+
+export class UpdateNilaiRespDto {
+  @ApiProperty({
+    type: [String],
+    example: ["550e8400-e29b-41d4-a716-446655440000"],
+  })
+  @IsUUID("all", { each: true })
+  mahasiswaKelasIds: string[];
+}
+
+export class UpdateNilaiDto extends UpdateNilaiRespDto {
+  @ApiPropertyOptional({ description: "undefined: assign nilai to null" })
+  @IsOptional()
+  @IsNumber()
+  @IsPositive()
+  nilaiAkhir?: number;
+}
diff --git a/src/nilai/nilai.module.ts b/src/nilai/nilai.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f9aca923b411670865c235c946b28d698a099a52
--- /dev/null
+++ b/src/nilai/nilai.module.ts
@@ -0,0 +1,26 @@
+import { Module } from "@nestjs/common";
+import { TypeOrmModule } from "@nestjs/typeorm";
+import { AuthModule } from "src/auth/auth.module";
+import { Kelas } from "src/entities/kelas.entity";
+import { Konfigurasi } from "src/entities/konfigurasi.entity";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
+import { KelasModule } from "src/kelas/kelas.module";
+import { KonfigurasiModule } from "src/konfigurasi/konfigurasi.module";
+import { NilaiController } from "./nilai.controller";
+import { NilaiService } from "./nilai.service";
+import { CustomStrategy } from "src/middlewares/custom.strategy";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { KelasService } from "src/kelas/kelas.service";
+
+@Module({
+  imports: [
+    TypeOrmModule.forFeature([MahasiswaKelas, Kelas, MataKuliah, Konfigurasi]),
+    AuthModule,
+    KonfigurasiModule,
+    KelasModule,
+  ],
+  controllers: [NilaiController],
+  providers: [NilaiService, CustomStrategy, KelasService, KonfigurasiService],
+})
+export class NilaiModule {}
diff --git a/src/nilai/nilai.service.ts b/src/nilai/nilai.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..81ebba4d7151fc7b42f495d542af94057fd4f66e
--- /dev/null
+++ b/src/nilai/nilai.service.ts
@@ -0,0 +1,98 @@
+import { Injectable } from "@nestjs/common";
+import { InjectRepository } from "@nestjs/typeorm";
+import { Kelas } from "src/entities/kelas.entity";
+import { MahasiswaKelas } from "src/entities/mahasiswaKelas.entity";
+import { MataKuliah } from "src/entities/mataKuliah.entity";
+import { KonfigurasiService } from "src/konfigurasi/konfigurasi.service";
+import { Brackets, In, Repository } from "typeorm";
+import { GetNilaiByMatkulRespDto, UpdateNilaiRespDto } from "./nilai.dto";
+
+@Injectable()
+export class NilaiService {
+  constructor(
+    @InjectRepository(MahasiswaKelas)
+    private mhsKelasRepo: Repository<MahasiswaKelas>,
+    @InjectRepository(Kelas)
+    private kelasRepo: Repository<Kelas>,
+    @InjectRepository(MataKuliah)
+    private mataKuliahRepo: Repository<MataKuliah>,
+    private konfServ: KonfigurasiService,
+  ) {}
+
+  private async isMhsKelasOrFail(mhsKelasIds: string[]) {
+    const periode = await this.konfServ.getPeriodeOrFail();
+
+    const mhsKelas = await this.mhsKelasRepo.find({
+      select: { id: true },
+      where: { id: In(mhsKelasIds), kelas: { periode } },
+    });
+
+    for (const mhsKelasId of mhsKelasIds) {
+      if (!mhsKelas.find((mk) => mk.id === mhsKelasId)) {
+        throw new Error(`Mahasiswa kelas ${mhsKelasId} tidak ditemukan`);
+      }
+    }
+  }
+
+  async getNilaiByMatkul(
+    mataKuliahKode: string,
+    page: number,
+    limit: number,
+    search: string,
+  ) {
+    const currPeriode = await this.konfServ.getPeriodeOrFail();
+    console.log(limit);
+
+    const baseQuery = this.mataKuliahRepo
+      .createQueryBuilder("matkul")
+      .select([
+        "matkul.kode AS mata_kuliah_kode",
+        "matkul.nama AS mata_kuliah_nama",
+        "kelas.nomor AS kelas_nomor",
+        "mhsKelas.id AS mahasiswa_kelas_id",
+        "mahasiswa.id AS mahasiswa_id",
+        "mahasiswa.nama AS mahasiswa_nama",
+        "mahasiswa.nim AS mahasiswa_nim",
+        "mhsKelas.nilaiAkhir AS nilai_akhir",
+      ])
+      .innerJoin("matkul.kelas", "kelas")
+      .leftJoin("kelas.mahasiswa", "mhsKelas")
+      .innerJoin("mhsKelas.mahasiswa", "mahasiswa")
+      .where("kelas.periode = :periode", { periode: currPeriode })
+      .andWhere(
+        new Brackets((qb) => {
+          qb.where("mahasiswa.nama ILIKE :search", {
+            search: `%${search}%`,
+          }).orWhere("mahasiswa.nim ILIKE :search", { search: `%${search}%` });
+        }),
+      );
+
+    if (mataKuliahKode) {
+      baseQuery.andWhere("matkul.kode = :kode", { kode: mataKuliahKode });
+    }
+
+    const mhsKelas: GetNilaiByMatkulRespDto[] = await baseQuery
+      .orderBy("matkul.kode")
+      .addOrderBy("kelas.nomor")
+      .addOrderBy("mahasiswa.nim")
+      .skip((page - 1) * limit)
+      .limit(limit)
+      .getRawMany();
+
+    return mhsKelas;
+  }
+
+  async updateNilai(
+    mhsKelasIds: string[],
+    nilaiAkhir?: number,
+  ): Promise<UpdateNilaiRespDto> {
+    await this.isMhsKelasOrFail(mhsKelasIds);
+
+    await this.mhsKelasRepo.update(
+      { id: In(mhsKelasIds) },
+      { nilaiAkhir: nilaiAkhir ?? null },
+    );
+
+    return { mahasiswaKelasIds: mhsKelasIds };
+  }
+}
diff --git a/src/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 };
+  }
+}