# Test Cases — Sistem Absensi Digital SMA

> **Versi:** 1.0.0 · **Tanggal:** April 2026
> **Referensi:** FR-NFR v2.0.0, List Page v1.0.0, Technical Specification v1.0.0

---

## Daftar Isi

1. [FR-01 — Autentikasi & Manajemen Sesi](#fr-01--autentikasi--manajemen-sesi)
2. [FR-02 — Manajemen Pengguna & Peran](#fr-02--manajemen-pengguna--peran)
3. [FR-03 — Manajemen Master Data](#fr-03--manajemen-master-data)
4. [FR-04 — Manajemen Tahun Ajaran & Semester](#fr-04--manajemen-tahun-ajaran--semester)
5. [FR-05 — Mekanisme Kenaikan Kelas](#fr-05--mekanisme-kenaikan-kelas)
6. [FR-06 — Manajemen Jadwal Pelajaran](#fr-06--manajemen-jadwal-pelajaran)
7. [FR-07 — Manajemen QR Code & Kiosk Token](#fr-07--manajemen-qr-code--kiosk-token)
8. [FR-08 — Halaman Kiosk / Scanner QR](#fr-08--halaman-kiosk--scanner-qr)
9. [FR-09 — Absensi Harian Guru](#fr-09--absensi-harian-guru)
10. [FR-10 — Absensi Harian Siswa](#fr-10--absensi-harian-siswa)
11. [FR-11 — Absensi Mengajar Guru (Per Sesi)](#fr-11--absensi-mengajar-guru-per-sesi)
12. [FR-12 — Absensi Mata Pelajaran Siswa (Per Sesi)](#fr-12--absensi-mata-pelajaran-siswa-per-sesi)
13. [FR-13 — Manajemen Guru Pengganti](#fr-13--manajemen-guru-pengganti)
14. [FR-14 — Kalender Hari Libur & Hari Khusus](#fr-14--kalender-hari-libur--hari-khusus)
15. [FR-15 — Pengajuan Izin Guru](#fr-15--pengajuan-izin-guru)
16. [FR-16 — Pengajuan Izin Siswa](#fr-16--pengajuan-izin-siswa)
17. [FR-17 — Sistem Alert Otomatis](#fr-17--sistem-alert-otomatis)
18. [FR-18 — Laporan & Ekspor Data](#fr-18--laporan--ekspor-data)
19. [FR-19 — Portal Mandiri Siswa](#fr-19--portal-mandiri-siswa)
20. [FR-20 — Manajemen Konfigurasi Sistem](#fr-20--manajemen-konfigurasi-sistem)
21. [Halaman & UI Tests](#halaman--ui-tests)

---

## Konvensi

| Kolom | Keterangan |
|-------|------------|
| **TC-ID** | Identifier unik test case: `TC-{FR}-{nomor_urut}` |
| **Ref** | ID requirement yang diuji (FR-xx.x) |
| **Tipe** | `Positive` (happy path), `Negative` (error/rejection), `Edge` (boundary/edge case) |
| **Prioritas** | `Critical`, `High`, `Medium`, `Low` |
| **Pre-condition** | Kondisi awal yang harus terpenuhi sebelum test dijalankan |
| **Steps** | Langkah-langkah eksekusi |
| **Expected Result** | Hasil yang diharapkan |

---

## FR-01 — Autentikasi & Manajemen Sesi

### TC-01-001 — Login berhasil dengan kredensial valid (super_admin)

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1, FR-01.2, FR-01.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Akun `super_admin` aktif tersedia di database |
| **Steps** | 1. Buka `GET /login` 2. Isi email dan password yang valid 3. Klik tombol "Masuk" |
| **Expected Result** | Login berhasil, sesi dibuat, redirect ke `/admin/dashboard` |

### TC-01-002 — Login berhasil dengan kredensial valid (operator)

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1, FR-01.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Akun `operator` aktif tersedia |
| **Steps** | 1. Buka `GET /login` 2. Isi email & password operator valid 3. Klik "Masuk" |
| **Expected Result** | Redirect ke dashboard operator |

### TC-01-003 — Login berhasil dengan kredensial valid (teacher)

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1, FR-01.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Akun `teacher` aktif tersedia |
| **Steps** | 1. Buka `GET /login` 2. Isi email & password teacher valid 3. Klik "Masuk" |
| **Expected Result** | Redirect ke `/teacher/dashboard` |

### TC-01-004 — Login berhasil dengan kredensial valid (student)

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1, FR-01.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Akun `student` aktif tersedia |
| **Steps** | 1. Buka `GET /login` 2. Isi email & password student valid 3. Klik "Masuk" |
| **Expected Result** | Redirect ke `/student/dashboard` |

### TC-01-005 — Login gagal: password salah

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Akun aktif tersedia |
| **Steps** | 1. Buka `GET /login` 2. Isi email valid tapi password salah 3. Klik "Masuk" |
| **Expected Result** | Login ditolak, pesan error validasi ditampilkan, tetap di halaman login |

### TC-01-006 — Login gagal: email tidak terdaftar

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | — |
| **Steps** | 1. Buka `GET /login` 2. Isi email yang tidak terdaftar 3. Klik "Masuk" |
| **Expected Result** | Login ditolak, pesan error ditampilkan |

### TC-01-007 — Login gagal: akun nonaktif (is_active = false)

| Field | Value |
|-------|-------|
| **Ref** | FR-01.7 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Akun dengan `is_active = false` |
| **Steps** | 1. Buka `GET /login` 2. Isi email & password yang benar dari akun nonaktif 3. Klik "Masuk" |
| **Expected Result** | Login ditolak dengan pesan informatif bahwa akun tidak aktif |

### TC-01-008 — Akses panel tanpa login → redirect ke login

| Field | Value |
|-------|-------|
| **Ref** | FR-01.2 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Tidak ada sesi aktif |
| **Steps** | 1. Akses `GET /admin/dashboard` tanpa login |
| **Expected Result** | Redirect ke `/login` |

### TC-01-009 — Akses panel role lain → respons 403

| Field | Value |
|-------|-------|
| **Ref** | FR-01.4 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `student` |
| **Steps** | 1. Akses `GET /admin/dashboard` |
| **Expected Result** | Respons HTTP 403 Forbidden |

### TC-01-010 — Teacher akses route student → 403

| Field | Value |
|-------|-------|
| **Ref** | FR-01.4 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `teacher` |
| **Steps** | 1. Akses `GET /student/dashboard` |
| **Expected Result** | Respons HTTP 403 |

### TC-01-011 — Operator akses route khusus super_admin → 403

| Field | Value |
|-------|-------|
| **Ref** | FR-01.4 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator` |
| **Steps** | 1. Akses `GET /admin/settings` (khusus super_admin) |
| **Expected Result** | Respons HTTP 403 |

### TC-01-012 — Logout berhasil

| Field | Value |
|-------|-------|
| **Ref** | FR-01.5 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai user manapun |
| **Steps** | 1. Klik tombol logout |
| **Expected Result** | Sesi dihapus, redirect ke `/login`, akses panel membutuhkan login ulang |

### TC-01-013 — Update profil: ubah nama dan email

| Field | Value |
|-------|-------|
| **Ref** | FR-01.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai user manapun |
| **Steps** | 1. Buka halaman profil `GET /profile` 2. Ubah nama dan email 3. Simpan |
| **Expected Result** | Data profil berhasil diperbarui, pesan sukses |

### TC-01-014 — Update password berhasil

| Field | Value |
|-------|-------|
| **Ref** | FR-01.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai user manapun |
| **Steps** | 1. Buka halaman profil 2. Isi password lama, password baru, konfirmasi 3. Simpan |
| **Expected Result** | Password berhasil diperbarui |

### TC-01-015 — Update password gagal: password lama salah

| Field | Value |
|-------|-------|
| **Ref** | FR-01.6 |
| **Tipe** | Negative |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai user manapun |
| **Steps** | 1. Buka halaman profil 2. Isi password lama yang salah 3. Simpan |
| **Expected Result** | Validasi gagal, pesan error password lama tidak sesuai |

### TC-01-016 — Login gagal: form kosong

| Field | Value |
|-------|-------|
| **Ref** | FR-01.1 |
| **Tipe** | Negative |
| **Prioritas** | Medium |
| **Pre-condition** | — |
| **Steps** | 1. Buka `GET /login` 2. Klik "Masuk" tanpa mengisi field |
| **Expected Result** | Validasi gagal, pesan error required field |

---

## FR-02 — Manajemen Pengguna & Peran

### TC-02-001 — Buat akun operator oleh super_admin

| Field | Value |
|-------|-------|
| **Ref** | FR-02.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka halaman Manajemen User 2. Klik "Tambah Pengguna" 3. Isi data dengan role `operator` 4. Simpan |
| **Expected Result** | Akun operator berhasil dibuat dan muncul di tabel |

### TC-02-002 — Buat akun teacher → profil guru otomatis terbuat

| Field | Value |
|-------|-------|
| **Ref** | FR-02.1, FR-02.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buat akun user dengan role `teacher` beserta data profil guru (NIP, nama, gender) 2. Simpan |
| **Expected Result** | Akun user dibuat, record `TEACHERS` otomatis terhubung ke user |

### TC-02-003 — Buat akun student → profil siswa otomatis terbuat

| Field | Value |
|-------|-------|
| **Ref** | FR-02.1, FR-02.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buat akun user dengan role `student` beserta data profil siswa (NIS, nama, kelas) 2. Simpan |
| **Expected Result** | Akun user dibuat, record `STUDENTS` otomatis terhubung ke user |

### TC-02-004 — Nonaktifkan akun pengguna

| Field | Value |
|-------|-------|
| **Ref** | FR-02.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, akun target aktif |
| **Steps** | 1. Buka Manajemen User 2. Klik toggle aktif/nonaktif pada akun target |
| **Expected Result** | `is_active` berubah menjadi `false`, akun tidak bisa login (TC-01-007) |

### TC-02-005 — Aktifkan kembali akun yang dinonaktifkan

| Field | Value |
|-------|-------|
| **Ref** | FR-02.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, akun target nonaktif |
| **Steps** | 1. Toggle aktifkan akun kembali |
| **Expected Result** | `is_active` berubah menjadi `true`, akun bisa login kembali |

### TC-02-006 — Ubah data akun pengguna

| Field | Value |
|-------|-------|
| **Ref** | FR-02.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Klik edit pada akun pengguna 2. Ubah nama/email 3. Simpan |
| **Expected Result** | Data akun berhasil diperbarui |

### TC-02-007 — Reset password pengguna

| Field | Value |
|-------|-------|
| **Ref** | FR-02.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Pilih akun pengguna 2. Klik "Reset Password" 3. Masukkan password baru |
| **Expected Result** | Password berhasil di-reset, pengguna bisa login dengan password baru |

### TC-02-008 — Buat akun dengan email duplikat → gagal

| Field | Value |
|-------|-------|
| **Ref** | FR-02.1 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, email sudah terdaftar |
| **Steps** | 1. Buat akun baru dengan email yang sudah ada |
| **Expected Result** | Validasi gagal, pesan error email sudah digunakan |

### TC-02-009 — Operator tidak bisa membuat akun

| Field | Value |
|-------|-------|
| **Ref** | FR-02.1 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator` |
| **Steps** | 1. Akses halaman Manajemen User atau endpoint create user |
| **Expected Result** | Akses ditolak (403) |

---

## FR-03 — Manajemen Master Data

### TC-03-001 — CRUD Guru: Buat data guru baru

| Field | Value |
|-------|-------|
| **Ref** | FR-03.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka Data Guru 2. Klik tambah 3. Isi NIP, nama, gender 4. Simpan |
| **Expected Result** | Data guru tersimpan dan tampil di tabel |

### TC-03-002 — CRUD Guru: Edit data guru

| Field | Value |
|-------|-------|
| **Ref** | FR-03.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, data guru ada |
| **Steps** | 1. Klik edit pada data guru 2. Ubah nama/NIP 3. Simpan |
| **Expected Result** | Data guru berhasil diperbarui |

### TC-03-003 — CRUD Guru: Nonaktifkan guru

| Field | Value |
|-------|-------|
| **Ref** | FR-03.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, guru aktif |
| **Steps** | 1. Toggle nonaktifkan guru |
| **Expected Result** | `is_active = false`, guru tidak muncul di daftar aktif |

### TC-03-004 — CRUD Siswa: Buat data siswa baru dengan kelas

| Field | Value |
|-------|-------|
| **Ref** | FR-03.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin`, kelas tersedia |
| **Steps** | 1. Buka Data Siswa 2. Klik tambah 3. Isi NIS, nama, gender, pilih kelas 4. Simpan |
| **Expected Result** | Data siswa tersimpan dengan `classroom_id` yang benar |

### TC-03-005 — CRUD Siswa: Edit penugasan kelas siswa

| Field | Value |
|-------|-------|
| **Ref** | FR-03.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, siswa dan kelas tersedia |
| **Steps** | 1. Edit data siswa 2. Ubah kelas 3. Simpan |
| **Expected Result** | `classroom_id` siswa berhasil diperbarui |

### TC-03-006 — CRUD Kelas: Buat kelas baru dengan tingkat dan wali kelas

| Field | Value |
|-------|-------|
| **Ref** | FR-03.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin`, guru tersedia |
| **Steps** | 1. Buka Data Kelas 2. Tambah kelas baru 3. Isi nama, tingkat, pilih wali kelas 4. Simpan |
| **Expected Result** | Kelas tersimpan dengan tingkat dan wali kelas |

### TC-03-007 — CRUD Mata Pelajaran: Buat mapel baru

| Field | Value |
|-------|-------|
| **Ref** | FR-03.4 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka Data Mapel 2. Tambah mapel 3. Isi kode, nama 4. Simpan |
| **Expected Result** | Mata pelajaran tersimpan |

### TC-03-008 — Upload foto profil guru

| Field | Value |
|-------|-------|
| **Ref** | FR-03.5 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Edit data guru 2. Upload file foto (JPG/PNG) 3. Simpan |
| **Expected Result** | Foto tersimpan, preview terlihat di tabel dan profil |

### TC-03-009 — Upload foto profil siswa

| Field | Value |
|-------|-------|
| **Ref** | FR-03.5 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Edit data siswa 2. Upload file foto 3. Simpan |
| **Expected Result** | Foto tersimpan dan ditampilkan |

### TC-03-010 — Buat guru dengan NIP duplikat → gagal

| Field | Value |
|-------|-------|
| **Ref** | FR-03.1 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, NIP sudah terdaftar |
| **Steps** | 1. Buat guru baru dengan NIP yang sudah ada |
| **Expected Result** | Validasi gagal, pesan error NIP duplikat |

### TC-03-011 — Buat siswa dengan NIS duplikat → gagal

| Field | Value |
|-------|-------|
| **Ref** | FR-03.2 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, NIS sudah terdaftar |
| **Steps** | 1. Buat siswa baru dengan NIS yang sudah ada |
| **Expected Result** | Validasi gagal, pesan error NIS duplikat |

### TC-03-012 — Filter data guru berdasarkan search dan status

| Field | Value |
|-------|-------|
| **Ref** | FR-03.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin`, data guru tersedia |
| **Steps** | 1. Buka Data Guru 2. Gunakan search nama/NIP 3. Filter status aktif/nonaktif |
| **Expected Result** | Tabel menampilkan hasil sesuai filter |

### TC-03-013 — Filter data siswa berdasarkan kelas dan tingkat

| Field | Value |
|-------|-------|
| **Ref** | FR-03.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin`, data siswa tersedia |
| **Steps** | 1. Buka Data Siswa 2. Pilih tingkat kelas → filter cascading ke kelas |
| **Expected Result** | Tabel menampilkan siswa sesuai kelas dan tingkat yang dipilih |

---

## FR-04 — Manajemen Tahun Ajaran & Semester

### TC-04-001 — Buat tahun ajaran baru → otomatis 2 semester

| Field | Value |
|-------|-------|
| **Ref** | FR-04.1, FR-04.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka Tahun Ajaran 2. Klik "Tambah TA" 3. Isi nama dan tanggal 4. Simpan |
| **Expected Result** | Tahun ajaran dibuat dengan tepat 2 semester (Semester 1 dan Semester 2) |

### TC-04-002 — Aktivasi semester → semester lain dinonaktifkan

| Field | Value |
|-------|-------|
| **Ref** | FR-04.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin`, semester lain sedang aktif |
| **Steps** | 1. Aktivasi semester baru 2. Konfirmasi warning |
| **Expected Result** | Semester baru aktif, semester lama otomatis nonaktif. Hanya 1 semester aktif di seluruh sistem |

### TC-04-003 — Hanya 1 semester aktif di seluruh sistem

| Field | Value |
|-------|-------|
| **Ref** | FR-04.3 |
| **Tipe** | Edge |
| **Prioritas** | Critical |
| **Pre-condition** | Semester A aktif |
| **Steps** | 1. Aktivasi semester B dari TA berbeda |
| **Expected Result** | Semester A menjadi nonaktif, hanya semester B yang aktif |

### TC-04-004 — Semester aktif menjadi acuan absensi

| Field | Value |
|-------|-------|
| **Ref** | FR-04.4 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Semester aktif tersedia, jadwal terhubung ke semester aktif |
| **Steps** | 1. Cek jadwal yang muncul di panel guru |
| **Expected Result** | Hanya jadwal dari semester aktif yang ditampilkan |

### TC-04-005 — Tutup semester dan aktivasi semester berikutnya

| Field | Value |
|-------|-------|
| **Ref** | FR-04.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, semester 1 aktif |
| **Steps** | 1. Tutup semester 1 2. Aktivasi semester 2 |
| **Expected Result** | Semester 1 nonaktif, semester 2 aktif |

### TC-04-006 — Edit tahun ajaran

| Field | Value |
|-------|-------|
| **Ref** | FR-04.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin`, TA tersedia |
| **Steps** | 1. Edit nama/tahun dari TA yang ada 2. Simpan |
| **Expected Result** | Data TA berhasil diperbarui |

---

## FR-05 — Mekanisme Kenaikan Kelas

### TC-05-001 — Fitur kenaikan kelas tersedia setelah Semester 2 ditutup

| Field | Value |
|-------|-------|
| **Ref** | FR-05.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin`, Semester 2 sudah ditutup |
| **Steps** | 1. Buka halaman Kenaikan Kelas |
| **Expected Result** | Fitur aktif, daftar siswa ditampilkan |

### TC-05-002 — Fitur kenaikan kelas tidak tersedia jika Semester 2 masih aktif

| Field | Value |
|-------|-------|
| **Ref** | FR-05.1 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Semester 2 masih aktif |
| **Steps** | 1. Buka halaman Kenaikan Kelas |
| **Expected Result** | Fitur disabled/tidak bisa diproses, informasi bahwa Semester 2 harus ditutup dulu |

### TC-05-003 — Tampilkan daftar siswa aktif untuk kenaikan kelas

| Field | Value |
|-------|-------|
| **Ref** | FR-05.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Semester 2 ditutup, siswa aktif tersedia |
| **Steps** | 1. Buka halaman Kenaikan Kelas |
| **Expected Result** | Semua siswa aktif tampil dengan kelas dan tingkat saat ini |

### TC-05-004 — Siswa naik kelas → pindah ke kelas baru

| Field | Value |
|-------|-------|
| **Ref** | FR-05.3, FR-05.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Siswa kelas X, kelas XI tersedia |
| **Steps** | 1. Pilih keputusan "naik kelas" untuk siswa 2. Pilih kelas tujuan (tingkat berikutnya) 3. Proses |
| **Expected Result** | `classroom_id` siswa berubah ke kelas baru di tingkat berikutnya |

### TC-05-005 — Siswa tinggal kelas → tetap di tingkat sama

| Field | Value |
|-------|-------|
| **Ref** | FR-05.3, FR-05.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Siswa kelas X |
| **Steps** | 1. Pilih "tinggal kelas" 2. Pilih kelas (bisa sama atau beda di tingkat yang sama) 3. Proses |
| **Expected Result** | Siswa tetap di tingkat yang sama, kelas bisa berubah |

### TC-05-006 — Siswa lulus (kelas XII) → dinonaktifkan

| Field | Value |
|-------|-------|
| **Ref** | FR-05.3, FR-05.6 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Siswa kelas XII |
| **Steps** | 1. Pilih "lulus" untuk siswa kelas XII 2. Proses |
| **Expected Result** | `STUDENTS.is_active = false`, `USERS.is_active = false`, siswa tidak bisa login |

### TC-05-007 — Proses kenaikan kelas bersifat irreversible

| Field | Value |
|-------|-------|
| **Ref** | FR-05.7 |
| **Tipe** | Edge |
| **Prioritas** | Critical |
| **Pre-condition** | Kenaikan kelas sudah diproses untuk TA ini |
| **Steps** | 1. Buka halaman Kenaikan Kelas untuk TA yang sama |
| **Expected Result** | Fitur tidak bisa dijalankan ulang, pesan bahwa proses sudah dilakukan |

### TC-05-008 — Riwayat absensi tetap tersimpan setelah kenaikan kelas

| Field | Value |
|-------|-------|
| **Ref** | FR-05.8 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Siswa punya riwayat absensi, kenaikan kelas diproses |
| **Steps** | 1. Cek data absensi siswa dari tahun ajaran sebelumnya |
| **Expected Result** | Semua riwayat absensi tetap ada dan bisa diakses |

### TC-05-009 — Konfirmasi ganda sebelum proses kenaikan kelas

| Field | Value |
|-------|-------|
| **Ref** | FR-05.7 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Data keputusan sudah diisi |
| **Steps** | 1. Klik "Proses" 2. Dialog konfirmasi muncul, minta ketik nama TA |
| **Expected Result** | Proses hanya berjalan setelah konfirmasi valid diberikan |

### TC-05-010 — Kenaikan kelas gagal di tengah proses → rollback semua

| Field | Value |
|-------|-------|
| **Ref** | FR-05.7 |
| **Tipe** | Edge |
| **Prioritas** | Critical |
| **Pre-condition** | Data keputusan diisi, satu data invalid |
| **Steps** | 1. Proses kenaikan kelas dengan satu entri yang menyebabkan error |
| **Expected Result** | Seluruh proses di-rollback, tidak ada perubahan parsial |

---

## FR-06 — Manajemen Jadwal Pelajaran

### TC-06-001 — Buat jadwal pelajaran baru

| Field | Value |
|-------|-------|
| **Ref** | FR-06.1, FR-06.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin`, semester/mapel/kelas/guru tersedia |
| **Steps** | 1. Buka Jadwal Pelajaran 2. Tambah jadwal: semester, hari, sesi, waktu mulai & selesai, mapel, kelas, guru 3. Simpan |
| **Expected Result** | Jadwal tersimpan dengan semua relasi yang benar |

### TC-06-002 — Jadwal konflik: guru sama, hari & sesi & semester sama → gagal

| Field | Value |
|-------|-------|
| **Ref** | FR-06.3 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Jadwal sudah ada untuk Guru A pada Senin sesi 1 semester aktif |
| **Steps** | 1. Buat jadwal baru untuk Guru A pada Senin sesi 1 semester yang sama |
| **Expected Result** | Validasi gagal, pesan error konflik jadwal guru |

### TC-06-003 — Jadwal konflik: kelas sama, hari & sesi & semester sama → gagal

| Field | Value |
|-------|-------|
| **Ref** | FR-06.3 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Jadwal sudah ada untuk Kelas X-1 pada Senin sesi 1 |
| **Steps** | 1. Buat jadwal baru untuk Kelas X-1 pada Senin sesi 1 semester yang sama |
| **Expected Result** | Validasi gagal, pesan error konflik jadwal kelas |

### TC-06-004 — Jadwal nonaktif tidak memunculkan sesi absensi

| Field | Value |
|-------|-------|
| **Ref** | FR-06.4 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Jadwal dengan `is_active = false` |
| **Steps** | 1. Login sebagai guru yang punya jadwal nonaktif 2. Cek daftar sesi hari ini |
| **Expected Result** | Jadwal nonaktif tidak muncul di daftar sesi guru |

### TC-06-005 — Edit jadwal yang ada

| Field | Value |
|-------|-------|
| **Ref** | FR-06.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin`, jadwal ada |
| **Steps** | 1. Edit jadwal 2. Ubah guru/mapel/waktu 3. Simpan |
| **Expected Result** | Jadwal berhasil diperbarui tanpa konflik |

### TC-06-006 — View jadwal per kelas dan per guru

| Field | Value |
|-------|-------|
| **Ref** | FR-06.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin`, jadwal tersedia |
| **Steps** | 1. Buka Jadwal Pelajaran 2. Toggle view "per kelas" vs "per guru" |
| **Expected Result** | Tampilan grid berubah sesuai mode yang dipilih |

---

## FR-07 — Manajemen QR Code & Kiosk Token

### TC-07-001 — QR token guru dibuat otomatis

| Field | Value |
|-------|-------|
| **Ref** | FR-07.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Guru baru dibuat |
| **Steps** | 1. Verifikasi record `TEACHER_QR_TOKENS` ada untuk guru tersebut |
| **Expected Result** | QR token unik aktif tersedia untuk guru |

### TC-07-002 — QR token siswa dibuat otomatis

| Field | Value |
|-------|-------|
| **Ref** | FR-07.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Siswa baru dibuat |
| **Steps** | 1. Verifikasi record `STUDENT_QR_TOKENS` ada untuk siswa tersebut |
| **Expected Result** | QR token unik aktif tersedia untuk siswa |

### TC-07-003 — Regenerate QR token guru oleh super_admin

| Field | Value |
|-------|-------|
| **Ref** | FR-07.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin`, guru memiliki token aktif |
| **Steps** | 1. Klik "Regenerate QR" pada guru 2. Konfirmasi |
| **Expected Result** | Token lama `is_active = false`, token baru `is_active = true`, dalam satu transaksi |

### TC-07-004 — Regenerate QR token oleh operator

| Field | Value |
|-------|-------|
| **Ref** | FR-07.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator` |
| **Steps** | 1. Regenerate QR token untuk guru atau siswa |
| **Expected Result** | Regenerasi berhasil, log regenerasi tersimpan |

### TC-07-005 — Token lama langsung tidak valid setelah regenerasi

| Field | Value |
|-------|-------|
| **Ref** | FR-07.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Token lama di-regenerate |
| **Steps** | 1. Scan QR dengan token lama di kiosk |
| **Expected Result** | Scan gagal, pesan "QR tidak dikenal" |

### TC-07-006 — Download QR Code guru dalam format gambar

| Field | Value |
|-------|-------|
| **Ref** | FR-07.4 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, guru punya token aktif |
| **Steps** | 1. Klik "Download QR" pada guru |
| **Expected Result** | File gambar QR Code berhasil diunduh |

### TC-07-007 — Log regenerasi QR tercatat (kartu hilang)

| Field | Value |
|-------|-------|
| **Ref** | FR-07.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`/`operator` |
| **Steps** | 1. Regenerate QR dengan alasan "kartu hilang" |
| **Expected Result** | Log mencatat: siapa yang melakukan, kapan, dan alasan regenerasi |

### TC-07-008 — Buat Kiosk Token baru

| Field | Value |
|-------|-------|
| **Ref** | FR-07.7 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka Kiosk Token 2. Klik "Buat Token" 3. Isi nama perangkat, masa berlaku 4. Simpan |
| **Expected Result** | Kiosk token dibuat, token ditampilkan sekali untuk di-copy |

### TC-07-009 — Nonaktifkan Kiosk Token

| Field | Value |
|-------|-------|
| **Ref** | FR-07.8 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Kiosk token aktif |
| **Steps** | 1. Toggle nonaktifkan kiosk token |
| **Expected Result** | Token nonaktif, halaman scanner dengan token tersebut tidak bisa diakses |

### TC-07-010 — Kiosk Token kadaluarsa → tidak bisa dipakai

| Field | Value |
|-------|-------|
| **Ref** | FR-07.8 |
| **Tipe** | Edge |
| **Prioritas** | High |
| **Pre-condition** | Kiosk token dengan `expires_at` yang sudah lewat |
| **Steps** | 1. Akses halaman scanner dengan token kadaluarsa |
| **Expected Result** | Akses ditolak, pesan token sudah kadaluarsa |

---

## FR-08 — Halaman Kiosk / Scanner QR

### TC-08-001 — Halaman scanner terbuka dengan Kiosk Token valid

| Field | Value |
|-------|-------|
| **Ref** | FR-08.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Kiosk token valid dan belum kadaluarsa |
| **Steps** | 1. Buka `GET /kiosk/{token}` |
| **Expected Result** | Halaman scanner terbuka, kamera aktif, header menampilkan nama sekolah dan kiosk |

### TC-08-002 — Halaman scanner gagal dengan token invalid

| Field | Value |
|-------|-------|
| **Ref** | FR-08.1, FR-08.10 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Token tidak valid |
| **Steps** | 1. Buka `GET /kiosk/{invalid_token}` |
| **Expected Result** | Akses ditolak, pesan error ditampilkan |

### TC-08-003 — Scan QR guru valid → check-in berhasil

| Field | Value |
|-------|-------|
| **Ref** | FR-08.3, FR-08.5, FR-08.7 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Kiosk aktif, guru aktif, bukan hari libur, belum check-in hari ini |
| **Steps** | 1. Scan QR Code guru |
| **Expected Result** | Record check-in dibuat, card data diri tampil: foto, nama, role, status "Check-in berhasil" (hijau) |

### TC-08-004 — Scan QR siswa valid → check-in berhasil

| Field | Value |
|-------|-------|
| **Ref** | FR-08.3, FR-08.5, FR-08.7 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Kiosk aktif, siswa aktif, bukan hari libur, belum check-in |
| **Steps** | 1. Scan QR Code siswa |
| **Expected Result** | Record check-in dibuat, card tampil: foto, nama, kelas, status "Check-in berhasil" |

### TC-08-005 — Scan kedua valid → check-out berhasil

| Field | Value |
|-------|-------|
| **Ref** | FR-08.5 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Sudah check-in, belum check-out, lebih dari 1 menit sejak check-in |
| **Steps** | 1. Scan QR yang sama lagi setelah > 1 menit |
| **Expected Result** | `check_out_time` dicatat, card "Check-out berhasil" (biru) |

### TC-08-006 — Double scan dalam 1 menit → diabaikan

| Field | Value |
|-------|-------|
| **Ref** | FR-08.4 |
| **Tipe** | Edge |
| **Prioritas** | Critical |
| **Pre-condition** | Scan check-in baru terjadi < 1 menit |
| **Steps** | 1. Scan QR yang sama dalam waktu < 1 menit |
| **Expected Result** | Scan diabaikan, pesan "Absensi sudah tercatat, silakan coba beberapa saat lagi" (kuning), tidak ada record baru |

### TC-08-007 — Scan setelah check-in dan check-out → diabaikan

| Field | Value |
|-------|-------|
| **Ref** | FR-08.6 |
| **Tipe** | Edge |
| **Prioritas** | High |
| **Pre-condition** | Check-in dan check-out sudah tercatat hari ini |
| **Steps** | 1. Scan QR yang sama untuk ketiga kalinya |
| **Expected Result** | Scan diabaikan, pesan "Check-in dan check-out hari ini sudah tercatat" (info) |

### TC-08-008 — Validation chain: QR token tidak dikenal

| Field | Value |
|-------|-------|
| **Ref** | FR-08.3 (step 2) |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Kiosk valid |
| **Steps** | 1. Scan QR Code yang tidak terdaftar di database |
| **Expected Result** | Card "QR tidak dikenal" (merah) |

### TC-08-009 — Validation chain: pengguna nonaktif

| Field | Value |
|-------|-------|
| **Ref** | FR-08.3 (step 3) |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | QR valid tapi pemilik `is_active = false` |
| **Steps** | 1. Scan QR dari pengguna nonaktif |
| **Expected Result** | Card "User nonaktif" (merah), tidak ada record dibuat |

### TC-08-010 — Validation chain: hari libur

| Field | Value |
|-------|-------|
| **Ref** | FR-08.3 (step 4) |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Hari ini adalah hari libur dengan `is_daily_attendance_active = false` |
| **Steps** | 1. Scan QR valid pada hari libur |
| **Expected Result** | Card "Hari libur" (abu-abu), tidak ada record |

### TC-08-011 — Feedback visual berbeda per kondisi

| Field | Value |
|-------|-------|
| **Ref** | FR-08.8 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Kiosk aktif |
| **Steps** | 1. Verifikasi warna dan ikon card untuk setiap kondisi: check-in (hijau), check-out (biru), double scan (kuning), QR asing (merah), user nonaktif (merah), hari libur (abu), sudah lengkap (info) |
| **Expected Result** | Setiap kondisi memiliki visual feedback yang berbeda dan jelas |

### TC-08-012 — Indikator status koneksi server

| Field | Value |
|-------|-------|
| **Ref** | FR-08.9 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Halaman kiosk terbuka |
| **Steps** | 1. Verifikasi indikator hijau saat online 2. Putuskan koneksi server 3. Verifikasi indikator merah dan fungsi scan nonaktif |
| **Expected Result** | Indikator berubah sesuai status koneksi, scan diblokir saat offline |

### TC-08-013 — Kiosk Token kadaluarsa saat sedang digunakan

| Field | Value |
|-------|-------|
| **Ref** | FR-08.10 |
| **Tipe** | Edge |
| **Prioritas** | High |
| **Pre-condition** | Halaman kiosk terbuka, token mendekati kadaluarsa |
| **Steps** | 1. Tunggu token kadaluarsa 2. Coba scan |
| **Expected Result** | Scan ditolak, pesan kiosk token kadaluarsa |

### TC-08-014 — Card data diri menampilkan info lengkap

| Field | Value |
|-------|-------|
| **Ref** | FR-08.7 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Scan berhasil |
| **Steps** | 1. Verifikasi card data diri setelah scan berhasil |
| **Expected Result** | Card menampilkan: foto, nama lengkap, role, kelas (siswa) / jabatan (guru), status kehadiran |

### TC-08-015 — Penentuan status present vs late saat check-in

| Field | Value |
|-------|-------|
| **Ref** | FR-08.5 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Setting `attendance.late_threshold_teacher` = "07:15" |
| **Steps** | 1. Scan guru check-in sebelum 07:15 → status `present` 2. Scan guru check-in setelah 07:15 → status `late` |
| **Expected Result** | Status otomatis ditentukan berdasarkan waktu check-in vs threshold |

---

## FR-09 — Absensi Harian Guru

### TC-09-001 — Record absensi guru dibuat dari scan kiosk

| Field | Value |
|-------|-------|
| **Ref** | FR-09.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Guru scan QR di kiosk |
| **Steps** | 1. Scan QR guru di kiosk 2. Verifikasi record `TEACHER_DAILY_ATTENDANCES` |
| **Expected Result** | Record dengan tanggal hari ini, check_in_time, dan status yang benar |

### TC-09-002 — Status present: check-in sebelum batas waktu

| Field | Value |
|-------|-------|
| **Ref** | FR-09.2, FR-09.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | `late_threshold_teacher = "07:15"`, guru check-in pukul 07:00 |
| **Steps** | 1. Guru scan check-in pukul 07:00 |
| **Expected Result** | Status = `present` |

### TC-09-003 — Status late: check-in setelah batas waktu

| Field | Value |
|-------|-------|
| **Ref** | FR-09.2, FR-09.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | `late_threshold_teacher = "07:15"`, guru check-in pukul 07:30 |
| **Steps** | 1. Guru scan check-in pukul 07:30 |
| **Expected Result** | Status = `late` |

### TC-09-004 — Auto-absent: guru tidak scan → status absent otomatis

| Field | Value |
|-------|-------|
| **Ref** | FR-09.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Guru aktif, tidak scan hari ini, bukan hari libur |
| **Steps** | 1. Jalankan scheduler `app:auto-absent-teachers` |
| **Expected Result** | Record `absent` dibuat dengan `is_auto = true` |

### TC-09-005 — Koreksi absensi harian guru oleh operator

| Field | Value |
|-------|-------|
| **Ref** | FR-09.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`, record absensi guru ada |
| **Steps** | 1. Buka Absensi Harian Guru 2. Klik koreksi 3. Ubah status dan tambah catatan 4. Simpan |
| **Expected Result** | Status dan catatan berhasil diperbarui |

### TC-09-006 — Koreksi absensi harian guru oleh super_admin

| Field | Value |
|-------|-------|
| **Ref** | FR-09.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Koreksi status absensi guru |
| **Expected Result** | Status berhasil dikoreksi |

### TC-09-007 — Record tidak dibuat pada hari libur

| Field | Value |
|-------|-------|
| **Ref** | FR-09.6 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Hari ini adalah hari libur `whole_school` |
| **Steps** | 1. Jalankan auto-absent |
| **Expected Result** | Tidak ada record absent dibuat untuk guru |

### TC-09-008 — Semua status yang didukung bisa digunakan

| Field | Value |
|-------|-------|
| **Ref** | FR-09.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Koreksi absensi guru ke setiap status: `present`, `late`, `absent`, `permission`, `sick`, `out_of_office` |
| **Expected Result** | Semua status valid dan tersimpan |

### TC-09-009 — Filter absensi harian guru: tanggal, status, search

| Field | Value |
|-------|-------|
| **Ref** | FR-09.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Data absensi tersedia |
| **Steps** | 1. Filter berdasarkan tanggal, status, atau nama guru |
| **Expected Result** | Tabel hanya menampilkan data sesuai filter |

---

## FR-10 — Absensi Harian Siswa

### TC-10-001 — Record absensi siswa dibuat dari scan kiosk

| Field | Value |
|-------|-------|
| **Ref** | FR-10.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Siswa aktif, bukan hari libur |
| **Steps** | 1. Scan QR siswa di kiosk |
| **Expected Result** | Record `STUDENT_DAILY_ATTENDANCES` dibuat dengan `check_in_time` |

### TC-10-002 — Status present vs late untuk siswa

| Field | Value |
|-------|-------|
| **Ref** | FR-10.2, FR-10.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | `late_threshold_student = "07:15"` |
| **Steps** | 1. Scan sebelum 07:15 → present 2. Scan setelah 07:15 → late |
| **Expected Result** | Status otomatis sesuai threshold |

### TC-10-003 — Auto-absent siswa

| Field | Value |
|-------|-------|
| **Ref** | FR-10.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Siswa aktif, tidak scan, bukan hari libur |
| **Steps** | 1. Jalankan scheduler `app:auto-absent-students` |
| **Expected Result** | Record `absent` dengan `is_auto = true` |

### TC-10-004 — Koreksi absensi siswa oleh operator/super_admin

| Field | Value |
|-------|-------|
| **Ref** | FR-10.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin` |
| **Steps** | 1. Buka Absensi Harian Siswa 2. Koreksi status siswa tertentu 3. Simpan |
| **Expected Result** | Status berhasil dikoreksi |

### TC-10-005 — Record tidak dibuat pada hari libur kelas tertentu

| Field | Value |
|-------|-------|
| **Ref** | FR-10.6 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Hari libur scope `per_class` berlaku untuk kelas siswa |
| **Steps** | 1. Jalankan auto-absent |
| **Expected Result** | Siswa di kelas yang libur tidak mendapat record absent otomatis |

### TC-10-006 — Siswa di kelas lain tetap di-absent pada hari libur per_class

| Field | Value |
|-------|-------|
| **Ref** | FR-10.6 |
| **Tipe** | Edge |
| **Prioritas** | High |
| **Pre-condition** | Hari libur `per_class` hanya untuk kelas X-1, siswa kelas X-2 tidak scan |
| **Steps** | 1. Jalankan auto-absent |
| **Expected Result** | Siswa kelas X-2 mendapat record `absent`, siswa kelas X-1 tidak |

### TC-10-007 — Semua status siswa yang didukung valid

| Field | Value |
|-------|-------|
| **Ref** | FR-10.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | — |
| **Steps** | 1. Koreksi ke setiap status: `present`, `late`, `absent`, `permission`, `sick` |
| **Expected Result** | Semua status valid |

---

## FR-11 — Absensi Mengajar Guru (Per Sesi)

### TC-11-001 — Guru melihat daftar sesi hari ini

| Field | Value |
|-------|-------|
| **Ref** | FR-11.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `teacher`, jadwal aktif tersedia untuk hari ini |
| **Steps** | 1. Buka `GET /teacher/schedules/today` |
| **Expected Result** | Daftar sesi hari ini tampil berdasarkan jadwal aktif guru |

### TC-11-002 — Guru klik "Mulai Sesi Mengajar" → status teaching

| Field | Value |
|-------|-------|
| **Ref** | FR-11.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Sesi jadwal hari ini belum dimulai, bukan hari libur |
| **Steps** | 1. Klik "Mulai Sesi Mengajar" pada jadwal 2. Konfirmasi |
| **Expected Result** | Record `TEACHING_ATTENDANCES` dibuat, status `teaching`, waktu mulai dicatat |

### TC-11-003 — Guru klik "Selesai Mengajar" → waktu selesai dicatat

| Field | Value |
|-------|-------|
| **Ref** | FR-11.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Sesi sudah dimulai (status `teaching`) |
| **Steps** | 1. Klik "Selesai Mengajar" |
| **Expected Result** | Waktu selesai dicatat di record yang sama |

### TC-11-004 — Auto not_teaching: guru tidak mulai sesi

| Field | Value |
|-------|-------|
| **Ref** | FR-11.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Sesi sudah lewat `end_time`, guru tidak klik mulai |
| **Steps** | 1. Jalankan scheduler `app:auto-not-teaching` |
| **Expected Result** | Record `not_teaching` + `is_auto = true` |

### TC-11-005 — Koreksi absensi mengajar oleh operator/super_admin

| Field | Value |
|-------|-------|
| **Ref** | FR-11.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin` |
| **Steps** | 1. Buka Absensi Mengajar 2. Koreksi status sesi guru 3. Simpan |
| **Expected Result** | Status berhasil dikoreksi |

### TC-11-006 — Guru tidak bisa mulai sesi pada jadwal orang lain

| Field | Value |
|-------|-------|
| **Ref** | FR-11.6 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai guru A, jadwal milik guru B |
| **Steps** | 1. Coba akses endpoint `POST /teacher/teaching/{schedule_guru_B}/start` |
| **Expected Result** | Akses ditolak (403 atau validasi gagal) |

### TC-11-007 — Guru tidak bisa mulai sesi pada hari yang salah

| Field | Value |
|-------|-------|
| **Ref** | FR-11.6 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Jadwal guru untuk hari Senin, hari ini Selasa |
| **Steps** | 1. Coba mulai sesi pada jadwal Senin |
| **Expected Result** | Validasi gagal, sesi tidak bisa dimulai |

### TC-11-008 — Record mengajar tidak dibuat pada hari libur

| Field | Value |
|-------|-------|
| **Ref** | FR-11.7 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Hari ini hari libur |
| **Steps** | 1. Auto-not-teaching dijalankan |
| **Expected Result** | Tidak ada record dibuat pada hari libur |

### TC-11-009 — Guru tidak bisa mulai sesi yang sudah dimulai

| Field | Value |
|-------|-------|
| **Ref** | FR-11.2 |
| **Tipe** | Edge |
| **Prioritas** | High |
| **Pre-condition** | Sesi sudah berstatus `teaching` |
| **Steps** | 1. Coba klik "Mulai Sesi" lagi |
| **Expected Result** | Tombol disabled atau validasi mencegah duplikat |

---

## FR-12 — Absensi Mata Pelajaran Siswa (Per Sesi)

### TC-12-001 — Form absensi muncul setelah mulai sesi

| Field | Value |
|-------|-------|
| **Ref** | FR-12.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Guru sudah klik "Mulai Sesi Mengajar" |
| **Steps** | 1. Klik "Input Absensi" atau navigasi ke `GET /teacher/teaching/{schedule}/attendance` |
| **Expected Result** | Form daftar siswa di kelas tersebut tampil |

### TC-12-002 — Guru input status kehadiran seluruh siswa

| Field | Value |
|-------|-------|
| **Ref** | FR-12.2, FR-12.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Form absensi mapel terbuka |
| **Steps** | 1. Set status setiap siswa (`present`, `absent`, `permission`, `sick`, `late`) 2. Klik "Simpan" |
| **Expected Result** | Record `STUDENT_SUBJECT_ATTENDANCES` terbuat untuk setiap siswa |

### TC-12-003 — Submitted_by dan submitted_at tercatat

| Field | Value |
|-------|-------|
| **Ref** | FR-12.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Guru submit absensi mapel |
| **Steps** | 1. Verifikasi field `submitted_by` dan `submitted_at` di database |
| **Expected Result** | `submitted_by` = user ID guru, `submitted_at` = waktu submit |

### TC-12-004 — Guru tidak bisa input absensi untuk jadwal/kelas bukan miliknya

| Field | Value |
|-------|-------|
| **Ref** | FR-12.4 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai guru A, jadwal milik guru B |
| **Steps** | 1. Akses `GET /teacher/teaching/{jadwal_guru_B}/attendance` |
| **Expected Result** | Akses ditolak |

### TC-12-005 — Koreksi absensi mapel oleh operator/super_admin

| Field | Value |
|-------|-------|
| **Ref** | FR-12.6 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin`, record absensi mapel ada |
| **Steps** | 1. Buka Absensi Mapel Siswa 2. Koreksi status 3. Simpan |
| **Expected Result** | Status berhasil dikoreksi |

### TC-12-006 — Form tidak tersedia jika sesi belum dimulai

| Field | Value |
|-------|-------|
| **Ref** | FR-12.1 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Sesi belum dimulai guru |
| **Steps** | 1. Coba akses form absensi mapel |
| **Expected Result** | Form tidak tersedia, pesan bahwa sesi harus dimulai dulu |

### TC-12-007 — Bulk set all siswa ke status tertentu

| Field | Value |
|-------|-------|
| **Ref** | FR-12.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Form absensi mapel terbuka |
| **Steps** | 1. Gunakan fitur "Set All" untuk menandai semua siswa sebagai `present` |
| **Expected Result** | Status semua siswa berubah ke `present` |

---

## FR-13 — Manajemen Guru Pengganti

### TC-13-001 — Tetapkan guru pengganti untuk jadwal tertentu

| Field | Value |
|-------|-------|
| **Ref** | FR-13.1, FR-13.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `operator`/`super_admin`, jadwal dan guru tersedia |
| **Steps** | 1. Buka Guru Pengganti 2. Pilih tanggal, jadwal, guru pengganti 3. Simpan |
| **Expected Result** | Record `SUBSTITUTE_TEACHERS` dibuat, mencatat jadwal, tanggal, guru asli, guru pengganti, dan pembuat |

### TC-13-002 — Sesi mengajar muncul di panel guru pengganti

| Field | Value |
|-------|-------|
| **Ref** | FR-13.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Guru pengganti ditunjuk untuk jadwal hari ini |
| **Steps** | 1. Login sebagai guru pengganti 2. Buka jadwal hari ini |
| **Expected Result** | Sesi yang digantikan muncul di daftar guru pengganti |

### TC-13-003 — Sesi tidak muncul di panel guru asli

| Field | Value |
|-------|-------|
| **Ref** | FR-13.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Guru pengganti ditunjuk |
| **Steps** | 1. Login sebagai guru asli 2. Cek jadwal hari ini |
| **Expected Result** | Sesi yang digantikan tidak muncul di panel guru asli |

### TC-13-004 — Absensi mengajar dicatat atas nama guru pengganti

| Field | Value |
|-------|-------|
| **Ref** | FR-13.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Guru pengganti mulai sesi |
| **Steps** | 1. Guru pengganti klik "Mulai Sesi" 2. Verifikasi `TEACHING_ATTENDANCES.teacher_id` |
| **Expected Result** | `teacher_id` = ID guru pengganti, bukan guru asli |

### TC-13-005 — Batalkan penunjukan guru pengganti (sebelum sesi dimulai)

| Field | Value |
|-------|-------|
| **Ref** | FR-13.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Guru pengganti ditunjuk, sesi belum dimulai |
| **Steps** | 1. Klik batalkan penunjukan |
| **Expected Result** | Record dihapus, sesi kembali muncul di panel guru asli |

### TC-13-006 — Batalkan penunjukan setelah sesi dimulai → gagal

| Field | Value |
|-------|-------|
| **Ref** | FR-13.1 |
| **Tipe** | Negative |
| **Prioritas** | Medium |
| **Pre-condition** | Sesi sudah dimulai guru pengganti |
| **Steps** | 1. Coba batalkan penunjukan |
| **Expected Result** | Pembatalan ditolak, sesi sudah berjalan |

---

## FR-14 — Kalender Hari Libur & Hari Khusus

### TC-14-001 — Buat hari libur whole_school

| Field | Value |
|-------|-------|
| **Ref** | FR-14.1, FR-14.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka Kalender Libur 2. Klik Tambah 3. Isi tanggal, deskripsi, tipe `holiday`, scope `whole_school` 4. Simpan |
| **Expected Result** | Hari libur tersimpan, berlaku untuk semua kelas |

### TC-14-002 — Buat hari libur per_class

| Field | Value |
|-------|-------|
| **Ref** | FR-14.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, kelas tersedia |
| **Steps** | 1. Buat hari libur scope `per_class` 2. Pilih kelas yang berlaku |
| **Expected Result** | Hari libur tersimpan, `CLASSROOM_HOLIDAYS` terisi untuk kelas terpilih |

### TC-14-003 — Tipe holiday vs school_event

| Field | Value |
|-------|-------|
| **Ref** | FR-14.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | — |
| **Steps** | 1. Buat hari libur tipe `holiday` 2. Buat hari khusus tipe `school_event` |
| **Expected Result** | Kedua tipe berhasil disimpan |

### TC-14-004 — Field is_daily_attendance_active mengontrol absensi harian

| Field | Value |
|-------|-------|
| **Ref** | FR-14.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Hari libur dengan `is_daily_attendance_active = false` |
| **Steps** | 1. Coba scan QR pada tanggal tersebut |
| **Expected Result** | Scan ditolak karena hari libur |

### TC-14-005 — Hari libur dengan is_daily_attendance_active = true → absensi tetap jalan

| Field | Value |
|-------|-------|
| **Ref** | FR-14.4 |
| **Tipe** | Edge |
| **Prioritas** | High |
| **Pre-condition** | Hari libur (school_event) dengan `is_daily_attendance_active = true` |
| **Steps** | 1. Scan QR pada tanggal tersebut |
| **Expected Result** | Scan berhasil, absensi tetap dicatat |

### TC-14-006 — Auto-absent dan alert cek tabel holidays

| Field | Value |
|-------|-------|
| **Ref** | FR-14.5 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Hari ini hari libur |
| **Steps** | 1. Jalankan auto-absent dan alert scheduler |
| **Expected Result** | Scheduler skip eksekusi karena hari libur |

### TC-14-007 — View kalender dan tabel list toggle

| Field | Value |
|-------|-------|
| **Ref** | FR-14.1 |
| **Tipe** | Positive |
| **Prioritas** | Low |
| **Pre-condition** | Data hari libur tersedia |
| **Steps** | 1. Toggle view "calendar" vs "table" |
| **Expected Result** | Tampilan berubah sesuai mode |

---

## FR-15 — Pengajuan Izin Guru

### TC-15-001 — Guru mengajukan izin/cuti

| Field | Value |
|-------|-------|
| **Ref** | FR-15.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `teacher` |
| **Steps** | 1. Buka Pengajuan Izin 2. Klik "Ajukan Izin" 3. Isi rentang tanggal, alasan, upload lampiran 4. Submit |
| **Expected Result** | Leave request dibuat dengan status `pending` |

### TC-15-002 — Operator approve izin guru

| Field | Value |
|-------|-------|
| **Ref** | FR-15.3 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `operator`, izin guru status `pending` |
| **Steps** | 1. Buka Izin Guru 2. Klik Approve pada izin pending 3. Tambah catatan reviewer |
| **Expected Result** | Status berubah ke `approved` |

### TC-15-003 — Approval otomatis update absensi harian

| Field | Value |
|-------|-------|
| **Ref** | FR-15.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Izin guru disetujui dengan rentang tanggal |
| **Steps** | 1. Approve izin 2. Cek `TEACHER_DAILY_ATTENDANCES` pada tanggal yang dicakup |
| **Expected Result** | Status absensi harian pada tanggal izin diperbarui ke `permission` atau `sick` |

### TC-15-004 — Reject izin guru

| Field | Value |
|-------|-------|
| **Ref** | FR-15.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin`, izin `pending` |
| **Steps** | 1. Reject izin dengan catatan alasan |
| **Expected Result** | Status berubah ke `rejected`, catatan reviewer tersimpan |

### TC-15-005 — Admin input izin langsung (is_admin_input = true)

| Field | Value |
|-------|-------|
| **Ref** | FR-15.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin` |
| **Steps** | 1. Klik "Input Manual" 2. Pilih guru, isi tanggal dan alasan 3. Simpan |
| **Expected Result** | Izin dibuat langsung dengan status `approved`, absensi harian otomatis diupdate |

### TC-15-006 — Notifikasi in-app saat status izin berubah

| Field | Value |
|-------|-------|
| **Ref** | FR-15.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Izin guru di-approve/reject |
| **Steps** | 1. Login sebagai guru 2. Cek notifikasi |
| **Expected Result** | Notifikasi perubahan status izin muncul |

### TC-15-007 — Guru membatalkan izin pending

| Field | Value |
|-------|-------|
| **Ref** | FR-15.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Izin berstatus `pending` |
| **Steps** | 1. Klik batalkan pada izin pending |
| **Expected Result** | Status berubah ke `canceled` |

### TC-15-008 — Guru tidak bisa membatalkan izin yang sudah approved

| Field | Value |
|-------|-------|
| **Ref** | FR-15.2 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Izin berstatus `approved` |
| **Steps** | 1. Coba batalkan izin |
| **Expected Result** | Pembatalan ditolak, hanya bisa dibatalkan saat `pending` |

### TC-15-009 — Upload lampiran izin (PDF/gambar)

| Field | Value |
|-------|-------|
| **Ref** | FR-15.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `teacher` |
| **Steps** | 1. Ajukan izin dengan lampiran PDF 2. Simpan |
| **Expected Result** | Lampiran tersimpan di `storage/app/leave-attachments/` |

### TC-15-010 — Lampiran izin tidak bisa diakses via URL publik

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.7 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Lampiran tersimpan |
| **Steps** | 1. Coba akses file langsung via URL |
| **Expected Result** | File tidak accessible langsung, hanya bisa diakses via route khusus dengan auth |

---

## FR-16 — Pengajuan Izin Siswa

### TC-16-001 — Siswa mengajukan izin

| Field | Value |
|-------|-------|
| **Ref** | FR-16.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai `student` |
| **Steps** | 1. Buka Pengajuan Izin 2. Ajukan izin dengan tanggal, alasan, lampiran |
| **Expected Result** | Leave request dibuat status `pending` |

### TC-16-002 — Approval izin siswa → update absensi harian dan mapel

| Field | Value |
|-------|-------|
| **Ref** | FR-16.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Izin siswa di-approve |
| **Steps** | 1. Approve izin siswa 2. Cek absensi harian dan mapel pada tanggal izin |
| **Expected Result** | Absensi harian dan mapel diupdate ke `permission`/`sick` |

### TC-16-003 — Admin input izin siswa langsung

| Field | Value |
|-------|-------|
| **Ref** | FR-16.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin` |
| **Steps** | 1. Input manual izin siswa |
| **Expected Result** | Izin langsung dengan status `approved` |

### TC-16-004 — Notifikasi siswa saat status izin berubah

| Field | Value |
|-------|-------|
| **Ref** | FR-16.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Izin siswa di-approve/reject |
| **Steps** | 1. Login sebagai siswa 2. Cek notifikasi |
| **Expected Result** | Notifikasi muncul |

### TC-16-005 — Siswa membatalkan izin pending

| Field | Value |
|-------|-------|
| **Ref** | FR-16.2 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Izin siswa status `pending` |
| **Steps** | 1. Klik batalkan |
| **Expected Result** | Status berubah ke `canceled` |

### TC-16-006 — Reject izin siswa

| Field | Value |
|-------|-------|
| **Ref** | FR-16.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator`/`super_admin` |
| **Steps** | 1. Reject izin siswa dengan catatan |
| **Expected Result** | Status `rejected`, catatan tersimpan |

---

## FR-17 — Sistem Alert Otomatis

### TC-17-001 — Alert guru belum check-in lewat batas waktu

| Field | Value |
|-------|-------|
| **Ref** | FR-17.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Guru aktif belum check-in, bukan hari libur |
| **Steps** | 1. Jalankan `app:alert-teacher-late-checkin` pada jam yang dikonfigurasi |
| **Expected Result** | Notifikasi dikirim ke `super_admin` dan `operator` |

### TC-17-002 — Alert persentase siswa absen melebihi threshold

| Field | Value |
|-------|-------|
| **Ref** | FR-17.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | % siswa absent dalam kelas > threshold (misal 30%) |
| **Steps** | 1. Jalankan `app:alert-student-absence-rate` |
| **Expected Result** | Alert dikirim ke super_admin dan operator |

### TC-17-003 — Alert guru tidak mulai sesi mengajar

| Field | Value |
|-------|-------|
| **Ref** | FR-17.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Jadwal sudah lewat waktu mulai, guru belum klik "Mulai Sesi" |
| **Steps** | 1. Jalankan `app:alert-teacher-not-teaching` |
| **Expected Result** | Alert dikirim |

### TC-17-004 — Alert threshold dikonfigurasi via SETTINGS

| Field | Value |
|-------|-------|
| **Ref** | FR-17.4 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Settings diubah |
| **Steps** | 1. Ubah `alert.student_absence_threshold` dari 30 ke 50 2. Verifikasi alert menggunakan threshold baru |
| **Expected Result** | Alert menggunakan threshold yang diperbarui |

### TC-17-005 — Alert tidak dikirim pada hari libur

| Field | Value |
|-------|-------|
| **Ref** | FR-17.5 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Hari ini hari libur |
| **Steps** | 1. Jalankan semua alert scheduler |
| **Expected Result** | Tidak ada alert yang dikirim |

### TC-17-006 — Alert dikirim via notifikasi in-app (database)

| Field | Value |
|-------|-------|
| **Ref** | FR-17.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Alert di-trigger |
| **Steps** | 1. Cek tabel `notifications` 2. Login sebagai super_admin/operator, cek halaman notifikasi |
| **Expected Result** | Notifikasi tersimpan di database dan tampil di halaman notifikasi |

---

## FR-18 — Laporan & Ekspor Data

### TC-18-001 — Laporan rekap absensi harian guru per periode

| Field | Value |
|-------|-------|
| **Ref** | FR-18.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`/`operator`, data absensi tersedia |
| **Steps** | 1. Buka Laporan Absensi Harian Guru 2. Filter date range |
| **Expected Result** | Tabel rekap menampilkan: guru, hadir, terlambat, absen, izin, sakit, persentase |

### TC-18-002 — Laporan rekap absensi harian siswa per kelas per periode

| Field | Value |
|-------|-------|
| **Ref** | FR-18.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Data absensi siswa tersedia |
| **Steps** | 1. Buka Laporan Absensi Harian Siswa 2. Filter kelas dan date range |
| **Expected Result** | Rekap per siswa di kelas yang dipilih |

### TC-18-003 — Laporan absensi mengajar guru per semester

| Field | Value |
|-------|-------|
| **Ref** | FR-18.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Data teaching attendance tersedia |
| **Steps** | 1. Buka Laporan Absensi Mengajar 2. Filter semester |
| **Expected Result** | Rekap: guru, total sesi, mengajar, tidak, persentase |

### TC-18-004 — Laporan absensi mapel siswa per semester per kelas

| Field | Value |
|-------|-------|
| **Ref** | FR-18.4 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Data student subject attendance tersedia |
| **Steps** | 1. Feed filter semester, kelas, mapel |
| **Expected Result** | Rekap: siswa, mapel, sesi, hadir, absen, persentase |

### TC-18-005 — Ekspor laporan ke PDF

| Field | Value |
|-------|-------|
| **Ref** | FR-18.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Laporan dengan data tersedia |
| **Steps** | 1. Klik "Ekspor PDF" |
| **Expected Result** | File PDF berhasil diunduh dengan konten laporan |

### TC-18-006 — Ekspor laporan ke Excel

| Field | Value |
|-------|-------|
| **Ref** | FR-18.6 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Laporan dengan data tersedia |
| **Steps** | 1. Klik "Ekspor Excel" |
| **Expected Result** | File Excel berhasil diunduh |

### TC-18-007 — Filter laporan: rentang tanggal, kelas, guru, semester

| Field | Value |
|-------|-------|
| **Ref** | FR-18.7 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Data laporan tersedia |
| **Steps** | 1. Terapkan berbagai kombinasi filter |
| **Expected Result** | Laporan hanya menampilkan data sesuai filter yang dipilih |

### TC-18-008 — Laporan kosong jika tidak ada data di rentang

| Field | Value |
|-------|-------|
| **Ref** | FR-18.1 |
| **Tipe** | Edge |
| **Prioritas** | Medium |
| **Pre-condition** | Tidak ada data absensi di rentang tanggal yang dipilih |
| **Steps** | 1. Pilih date range tanpa data |
| **Expected Result** | Tabel kosong dengan pesan informatif |

---

## FR-19 — Portal Mandiri Siswa

### TC-19-001 — Siswa melihat rekap absensi harian

| Field | Value |
|-------|-------|
| **Ref** | FR-19.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `student`, data absensi ada |
| **Steps** | 1. Buka Rekap Absensi Harian |
| **Expected Result** | Rekap absensi harian siswa sendiri tampil beserta status |

### TC-19-002 — Siswa melihat rekap absensi per mapel

| Field | Value |
|-------|-------|
| **Ref** | FR-19.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `student`, data absensi mapel ada |
| **Steps** | 1. Buka Rekap Absensi Mapel |
| **Expected Result** | Rekap per mata pelajaran tampil |

### TC-19-003 — Siswa melihat jadwal pelajaran semester aktif

| Field | Value |
|-------|-------|
| **Ref** | FR-19.3 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `student`, semester aktif, jadwal tersedia |
| **Steps** | 1. Buka Jadwal Pelajaran |
| **Expected Result** | Grid jadwal kelas siswa untuk semester aktif |

### TC-19-004 — Siswa melihat riwayat izin

| Field | Value |
|-------|-------|
| **Ref** | FR-19.4 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `student`, pernah mengajukan izin |
| **Steps** | 1. Buka Pengajuan Izin |
| **Expected Result** | Riwayat semua pengajuan izin tampil dengan status masing-masing |

### TC-19-005 — Siswa hanya melihat data miliknya sendiri

| Field | Value |
|-------|-------|
| **Ref** | FR-19.5 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | Login sebagai student A |
| **Steps** | 1. Cek semua halaman portal siswa |
| **Expected Result** | Hanya data milik student A yang tampil, tidak bisa melihat data siswa lain |

### TC-19-006 — Filter rekap absensi harian per bulan/tahun

| Field | Value |
|-------|-------|
| **Ref** | FR-19.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Data absensi multi-bulan |
| **Steps** | 1. Gunakan month picker untuk filter |
| **Expected Result** | Data ditampilkan sesuai bulan/tahun yang dipilih |

---

## FR-20 — Manajemen Konfigurasi Sistem

### TC-20-001 — Super_admin melihat konfigurasi sistem

| Field | Value |
|-------|-------|
| **Ref** | FR-20.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Buka Konfigurasi Sistem |
| **Expected Result** | Semua setting tampil dikelompokkan per group |

### TC-20-002 — Ubah konfigurasi waktu batas check-in

| Field | Value |
|-------|-------|
| **Ref** | FR-20.2 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin` |
| **Steps** | 1. Ubah `attendance.late_threshold_teacher` dari "07:15" ke "07:30" 2. Simpan |
| **Expected Result** | Konfigurasi tersimpan, threshold terlambat berubah |

### TC-20-003 — Tipe konfigurasi: string, integer, boolean, json

| Field | Value |
|-------|-------|
| **Ref** | FR-20.3 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Setting berbagai tipe tersedia |
| **Steps** | 1. Ubah setting tipe string 2. Ubah setting tipe integer 3. Ubah setting tipe boolean (toggle) 4. Ubah setting tipe json |
| **Expected Result** | Semua tipe tersimpan dan di-cast dengan benar |

### TC-20-004 — Perubahan konfigurasi berlaku langsung

| Field | Value |
|-------|-------|
| **Ref** | FR-20.4 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Konfigurasi diubah |
| **Steps** | 1. Ubah `attendance.late_threshold_teacher` 2. Tanpa restart, scan guru 3. Verifikasi threshold baru berlaku |
| **Expected Result** | Perubahan konfigurasi berlaku langsung (cache di-flush) |

### TC-20-005 — Operator tidak bisa akses konfigurasi

| Field | Value |
|-------|-------|
| **Ref** | FR-20.1 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `operator` |
| **Steps** | 1. Akses `GET /admin/settings` |
| **Expected Result** | Akses ditolak (403) |

---

## Halaman & UI Tests

### Dashboard Tests

### TC-UI-001 — Dashboard Super Admin menampilkan stat cards

| Field | Value |
|-------|-------|
| **Ref** | Page 2.1.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `super_admin`, data tersedia |
| **Steps** | 1. Buka Dashboard |
| **Expected Result** | Tampil stat cards: guru aktif, siswa aktif, total kelas, kehadiran hari ini (%), grafik 7 hari, alert terbaru |

### TC-UI-002 — Dashboard Teacher menampilkan data guru

| Field | Value |
|-------|-------|
| **Ref** | Page 4.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `teacher` |
| **Steps** | 1. Buka Dashboard Guru |
| **Expected Result** | Card profil, stat bulan ini, jadwal hari ini, notifikasi terbaru |

### TC-UI-003 — Dashboard Student menampilkan data siswa

| Field | Value |
|-------|-------|
| **Ref** | Page 5.1 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | Login sebagai `student` |
| **Steps** | 1. Buka Dashboard Siswa |
| **Expected Result** | Card profil, stat bulan ini, status hari ini, jadwal hari ini |

---

### Panel Guru — Teacher Panel Tests

### TC-UI-004 — Guru melihat jadwal mingguan

| Field | Value |
|-------|-------|
| **Ref** | Page 4.4 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `teacher`, jadwal tersedia |
| **Steps** | 1. Buka Jadwal Mingguan |
| **Expected Result** | Grid jadwal: kolom=hari, baris=sesi, cell=mapel+kelas |

### TC-UI-005 — Guru melihat rekap absensi harian sendiri

| Field | Value |
|-------|-------|
| **Ref** | Page 4.5 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `teacher`, data absensi ada |
| **Steps** | 1. Buka Rekap Absensi Harian |
| **Expected Result** | Ringkasan bulanan + tabel detail per tanggal |

### TC-UI-006 — Guru melihat rekap mengajar

| Field | Value |
|-------|-------|
| **Ref** | Page 4.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai `teacher`, data teaching attendance ada |
| **Steps** | 1. Buka Rekap Mengajar 2. Filter semester |
| **Expected Result** | Tabel rekap mengajar dengan persentase |

---

### Notifikasi Tests

### TC-UI-007 — Halaman notifikasi: tab belum dibaca / semua

| Field | Value |
|-------|-------|
| **Ref** | Page 2.6.6, 4.8, 5.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Login sebagai user manapun, notifikasi tersedia |
| **Steps** | 1. Buka Notifikasi 2. Toggle tab "Belum Dibaca" dan "Semua" |
| **Expected Result** | Tab filter berfungsi, badge count terlihat |

### TC-UI-008 — Tandai notifikasi sebagai dibaca

| Field | Value |
|-------|-------|
| **Ref** | Page 2.6.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Notifikasi unread tersedia |
| **Steps** | 1. Klik notifikasi atau "Tandai Semua Dibaca" |
| **Expected Result** | Notifikasi ditandai sebagai read, badge count berkurang |

---

### Kiosk Scanner UI Tests

### TC-UI-009 — Kiosk menampilkan header lengkap

| Field | Value |
|-------|-------|
| **Ref** | Page 6.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Halaman kiosk terbuka |
| **Steps** | 1. Verifikasi header |
| **Expected Result** | Header: nama sekolah, nama kiosk, jam realtime, tanggal |

### TC-UI-010 — Kiosk card auto-dismiss setelah 5 detik

| Field | Value |
|-------|-------|
| **Ref** | Page 6.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Scan berhasil, card feedback tampil |
| **Steps** | 1. Tunggu 5 detik setelah card muncul |
| **Expected Result** | Card otomatis hilang setelah 5 detik |

### TC-UI-011 — Kiosk health check polling setiap 10 detik

| Field | Value |
|-------|-------|
| **Ref** | Page 6.1 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Halaman kiosk terbuka |
| **Steps** | 1. Monitor network requests |
| **Expected Result** | Request ke `/kiosk/{token}/health` setiap 10 detik |

---

### Keamanan (Security) Tests

### TC-SEC-001 — Password disimpan dengan bcrypt

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.1 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | User dibuat |
| **Steps** | 1. Verifikasi kolom password di database |
| **Expected Result** | Password di-hash dengan bcrypt, bukan plaintext |

### TC-SEC-002 — CSRF protection pada form

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.8 |
| **Tipe** | Negative |
| **Prioritas** | Critical |
| **Pre-condition** | — |
| **Steps** | 1. Kirim POST request tanpa CSRF token |
| **Expected Result** | Request ditolak (419 Token Mismatch) |

### TC-SEC-003 — Validasi input server-side

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.9 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | — |
| **Steps** | 1. Submit form dengan data invalid (email format salah, field kosong, dll) |
| **Expected Result** | Server-side validation menolak input, pesan error ditampilkan |

### TC-SEC-004 — QR Token unik dan tidak bisa ditebak

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.5 |
| **Tipe** | Positive |
| **Prioritas** | High |
| **Pre-condition** | QR token dibuat |
| **Steps** | 1. Verifikasi panjang token (64 karakter) 2. Verifikasi token random, bukan berbasis ID |
| **Expected Result** | Token 64 karakter, cryptographically random |

### TC-SEC-005 — Hanya satu QR token aktif per individu

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.5 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | Token di-regenerate |
| **Steps** | 1. Count token aktif untuk satu individu |
| **Expected Result** | Tepat satu token `is_active = true` per individu |

### TC-SEC-006 — Regenerate QR token bersifat atomic

| Field | Value |
|-------|-------|
| **Ref** | NFR-04.2 |
| **Tipe** | Positive |
| **Prioritas** | Critical |
| **Pre-condition** | — |
| **Steps** | 1. Regenerate token 2. Verifikasi tidak ada jeda dua token aktif bersamaan |
| **Expected Result** | Token lama nonaktif dan token baru aktif dalam satu transaksi database |

### TC-SEC-007 — File upload validasi MIME type dan ukuran

| Field | Value |
|-------|-------|
| **Ref** | NFR-02.9 |
| **Tipe** | Negative |
| **Prioritas** | High |
| **Pre-condition** | — |
| **Steps** | 1. Upload file lampiran izin dengan tipe tidak didukung (misal .exe) 2. Upload file > 2MB |
| **Expected Result** | Upload ditolak dengan pesan error |

---

### Responsivitas & Usability Tests

### TC-UX-001 — Halaman responsif pada tablet (768px)

| Field | Value |
|-------|-------|
| **Ref** | NFR-06.4 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | — |
| **Steps** | 1. Akses semua halaman pada resolusi 768px |
| **Expected Result** | Layout responsif, tidak ada elemen yang terpotong |

### TC-UX-002 — Pesan error dalam Bahasa Indonesia

| Field | Value |
|-------|-------|
| **Ref** | NFR-06.5 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | — |
| **Steps** | 1. Trigger berbagai pesan error (validasi, akses ditolak) |
| **Expected Result** | Semua pesan dalam Bahasa Indonesia yang jelas dan tidak teknis |

### TC-UX-003 — Indikator status koneksi kiosk terlihat jelas

| Field | Value |
|-------|-------|
| **Ref** | NFR-06.6 |
| **Tipe** | Positive |
| **Prioritas** | Medium |
| **Pre-condition** | Halaman kiosk terbuka |
| **Steps** | 1. Verifikasi indikator koneksi terlihat jelas tanpa menghalangi area scan |
| **Expected Result** | Indikator visible tapi tidak mengganggu area scan |

---

## Ringkasan Jumlah Test Cases

| Modul (FR) | Jumlah TC | Critical | High | Medium | Low |
|------------|----------|----------|------|--------|-----|
| FR-01 Autentikasi | 16 | 8 | 3 | 5 | 0 |
| FR-02 Manajemen User | 9 | 3 | 5 | 1 | 0 |
| FR-03 Master Data | 13 | 3 | 5 | 5 | 0 |
| FR-04 Tahun Ajaran | 6 | 3 | 2 | 1 | 0 |
| FR-05 Kenaikan Kelas | 10 | 5 | 3 | 2 | 0 |
| FR-06 Jadwal | 6 | 3 | 1 | 2 | 0 |
| FR-07 QR & Kiosk Token | 10 | 4 | 5 | 1 | 0 |
| FR-08 Scanner Kiosk | 15 | 6 | 6 | 3 | 0 |
| FR-09 Absensi Harian Guru | 9 | 4 | 3 | 2 | 0 |
| FR-10 Absensi Harian Siswa | 7 | 3 | 3 | 1 | 0 |
| FR-11 Absensi Mengajar | 9 | 3 | 4 | 2 | 0 |
| FR-12 Absensi Mapel | 7 | 3 | 2 | 2 | 0 |
| FR-13 Guru Pengganti | 6 | 3 | 1 | 2 | 0 |
| FR-14 Kalender Libur | 7 | 3 | 2 | 1 | 1 |
| FR-15 Izin Guru | 10 | 2 | 4 | 4 | 0 |
| FR-16 Izin Siswa | 6 | 1 | 3 | 2 | 0 |
| FR-17 Alert Otomatis | 6 | 0 | 4 | 2 | 0 |
| FR-18 Laporan & Ekspor | 8 | 0 | 5 | 3 | 0 |
| FR-19 Portal Siswa | 6 | 1 | 3 | 2 | 0 |
| FR-20 Konfigurasi | 5 | 1 | 2 | 2 | 0 |
| UI & Halaman | 11 | 0 | 2 | 9 | 0 |
| Security | 7 | 4 | 2 | 1 | 0 |
| UX & Responsif | 3 | 0 | 0 | 3 | 0 |
| **TOTAL** | ****192**** | **63** | **70** | **50** | **1** |

---

> _Dokumen ini mencakup test case untuk seluruh 20 Functional Requirements berdasarkan FR-NFR v2.0.0, List Page v1.0.0, dan Technical Specification v1.0.0. Setiap test case ditulis dengan referensi langsung ke ID requirement yang diuji._
