# PLAN — Arsip Periode (Tahun Pelajaran & Semester) + Enrollment Siswa

**Tujuan utama**: Setiap data pembelajaran (materi, tugas, submission/nilai, absensi, jurnal) harus
**tetap tersimpan dan bisa diakses kembali per Tahun Pelajaran & Semester**, meskipun siswa
naik kelas atau lulus. Guru bisa membuka "arsip" tahun/semester lampau dan melihat hasil siswa.

Status: **SEBAGIAN BESAR DIIMPLEMENTASI** (15 Juli 2026). Dokumen ini adalah acuan eksekusi.

---

## 0. Status Implementasi

| Langkah | Status |
|---------|--------|
| Migrasi `student_enrollments` + kolom periode `assignments` + backfill | ✅ |
| Model `StudentEnrollment` + relasi `SyncedStudent`/`SyncedClass`/`Assignment`/`AcademicYear` | ✅ |
| Fix `$fillable` `SyncedStudent` (`is_active`) | ✅ |
| `ApiSyncService::syncStudents()` — catat enrollment aktif | ✅ |
| `ApiSyncService::syncStudentHistory()` — tarik `/students/:id/history` | ✅ |
| `PeriodContext` helper + `HandleInertiaRequests` share `academicYears` | ✅ |
| Refactor `AssignmentController` — period filter + enrollment queries | ✅ |
| Refactor `MaterialController` — enrollment queries | ✅ |
| Refactor `AttendanceController` — period filter + enrollment queries | ✅ |
| Refactor `DashboardController` — enrollment queries | ✅ |
| Refactor `StudentController` — period filter | ✅ |
| Refactor `QuizController` — enrollment verification | ✅ |
| Refactor `ArenaPlayController` — enrollment verification | ✅ |
| Refactor `GameController` — enrollment queries | ✅ |
| Amankan `ResetSyncData` command + `SyncController::resetData()` | ✅ |
| Route `sync/history` | ✅ |
| **Frontend UI**: dropdown filter arsip di halaman Nilai/Absensi/Materi/Student | ⏳ Belum |
| **Frontend UI**: tombol "Sync Riwayat" di halaman Sinkronisasi | ⏳ Belum |
| **Frontend UI**: tampilkan arsip siswa di portal siswa (riwayal kelas) | ⏳ Belum (Fase 2) |
| `teaching_assignments` terikat periode | ⏳ Belum (opsional) |

---

## 1. Ringkasan Masalah (hasil audit)

Diverifikasi terhadap kode UtiVerse dan API pusat (`landing_mts`, `api.mtssapo.sch.id`).

### Cara kerja API pusat
- Tabel `classes` memakai **id yang sama tiap tahun** (mis. "VII A" id=4 selamanya). Hanya isinya (siswa) yang berganti.
- `POST /students/promote` mengubah `students.classId` ke kelas tujuan, dan **menyimpan kelas lama ke `student_class_history` per `academicYear`**.
- `GET /students/:id/history` mengembalikan riwayat kelas siswa per tahun ajaran. **UtiVerse belum memakainya.**
- `GET /students` (yang dipakai UtiVerse) hanya mengembalikan `classId` **saat ini**.

### Konsekuensi di UtiVerse (bug)
| # | Masalah | Lokasi | Dampak |
|---|---------|--------|--------|
| 1 | `assignments` tidak punya `semester_id`/`academic_year_id` (hanya lewat `material`) | migrasi `2026_06_22...` | Tugas sulit diarsip per periode |
| 2 | Sync **overwrite** `synced_students.class_id`; tak ada riwayat enrollment | `ApiSyncService::syncStudents` :140 | Naik kelas → siswa tak lihat tugas lama; arsip rancu |
| 3 | `synced_classes` tak punya penanda tahun; `api_class_id` dipakai ulang | skema + sync | Angkatan lama & baru tercampur di kelas sama |
| 4 | `utiverse:reset-sync` melakukan `truncate` → cascade **menghapus SEMUA arsip** | `ResetSyncData` :50-57 | Penghancur data, bukan pengarsip |
| 5 | `is_active` tidak ada di `$fillable` `SyncedStudent` | `SyncedStudent` :10 | Nilai `is_active` dari sync tidak tersimpan |
| 6 | Semua query siswa memakai `class_id` **sekarang** | banyak controller | Buka arsip = daftar siswa salah |
| 7 | Tidak ada filter periode (arsip) di UI | semua controller | Tak bisa lihat hasil tahun lampau |
| 8 | Data awal: TP aktif `2025/2026` tapi semester aktif = `genap` (harusnya `ganjil`) | data seed | Semester aktif salah |
| 9 | `teaching_assignments` tak terikat periode | migrasi | Penugasan mengajar tak bisa diarsip per tahun |

---

## 2. Prinsip Desain

1. **Identitas vs Penempatan**: `synced_students` = identitas siswa (1 baris/siswa, seumur hidup).
   Penempatan kelas per periode disimpan di tabel baru **`student_enrollments`**.
2. **Kelas tetap 1 baris** (`synced_classes`). Pembeda antar tahun ada di enrollment & di kolom
   `academic_year_id`/`semester_id` pada setiap data transaksional.
3. **Stamping periode**: setiap data pembelajaran menyimpan `academic_year_id` + `semester_id`
   pada saat dibuat. Ini "membekukan" data ke periode tersebut (immutable snapshot).
4. **Query berbasis konteks periode**: default = periode aktif (`is_current`), tapi bisa dipilih
   periode lampau (mode arsip). Daftar siswa diambil dari enrollment periode terpilih, bukan
   `class_id` sekarang.
5. **Tidak ada penghapusan data untuk pergantian tahun.** Naik kelas = tambah enrollment baru,
   bukan overwrite/truncate.

---

## 3. Perubahan Skema Database

### 3.1 Tabel baru: `student_enrollments`
```
id
student_id      -> synced_students.id  (cascadeOnDelete)
class_id        -> synced_classes.id   (cascadeOnDelete)
academic_year_id-> academic_years.id   (cascadeOnDelete)
semester_id     -> semesters.id (nullable; enrollment biasanya per-tahun, semester opsional)
is_active       boolean default true   (enrollment periode berjalan)
source          enum('sync','history','manual') default 'sync'
created_at / updated_at
UNIQUE(student_id, academic_year_id)   -- 1 penempatan per siswa per tahun
INDEX(class_id, academic_year_id)
```
Catatan: enrollment di-key per **tahun ajaran** (bukan semester) karena kenaikan kelas terjadi per tahun.
Semester dipakai untuk stamping data, bukan untuk penempatan kelas.

### 3.2 `assignments`: tambah kolom periode
```
academic_year_id nullable -> academic_years.id (nullOnDelete)
semester_id      nullable -> semesters.id (nullOnDelete)
```
Diisi dari `material` saat assignment dibuat (material sudah punya `semester_id`).
Backfill data lama: ambil dari `material.semester_id` + `semester.academic_year_id`.

### 3.3 `synced_students`: model `$fillable` + `casts`
Tambah `is_active` ke `$fillable` dan cast boolean. (Kolom sudah ada di DB.)

### 3.4 `teaching_assignments`: tambah `academic_year_id` (Fase lanjutan)
```
academic_year_id nullable -> academic_years.id
UNIQUE(user_id, class_id, subject_id, academic_year_id)  -- ganti unique lama
```
Opsional untuk Fase 1; bisa ditunda. Penugasan mengajar umumnya berlaku periode aktif.

### 3.5 (opsional) Backfill materi & absensi yang sudah ada
`materials`/`attendance_sessions` sudah punya `semester_id`. Tidak perlu diubah, cukup dipakai.

---

## 4. Model & Relasi

- **`StudentEnrollment`** (baru): belongsTo student, syncedClass, academicYear, semester.
- **`SyncedStudent`**:
  - tambah `is_active` ke `$fillable` + cast.
  - relasi `enrollments(): HasMany`.
  - helper `classForYear($academicYearId)` / `currentEnrollment()`.
- **`SyncedClass`**: relasi `enrollments(): HasMany`; helper `studentsForYear($academicYearId)`.
- **`Assignment`**: tambah `academic_year_id`,`semester_id` ke `$fillable`; relasi `academicYear`,`semester`.
- **`AcademicYear`**: relasi `enrollments(): HasMany`.

---

## 5. Perubahan `ApiSyncService`

### 5.1 `syncStudents()`
Setelah upsert `synced_students`:
1. Ambil TP aktif (`AcademicYear::current()`). Jika belum ada → skip enrollment (log warning).
2. `StudentEnrollment::updateOrCreate([student_id, academic_year_id=aktif], [class_id=kelas sekarang, is_active=true, source='sync'])`.
3. **JANGAN lagi menimpa `synced_students.class_id`** sebagai satu-satunya sumber kebenaran kelas
   historis. `class_id` di `synced_students` boleh tetap diisi sebagai "kelas terkini" (untuk
   kemudahan default), tapi kebenaran arsip diambil dari enrollment.

### 5.2 Tarik history (opsional, sekali jalan / berkala)
Method baru `syncStudentHistory()`:
- Untuk tiap siswa ter-sync, panggil `GET /students/{api_student_id}/history`.
- Untuk tiap entri `{className, academicYear}`:
  - Map `academicYear` (string "2024/2025") → `academic_years.id` (buat jika belum ada).
  - Map `className` → `synced_classes` by name.
  - `StudentEnrollment::updateOrCreate([student_id, academic_year_id], [class_id, source='history', is_active=false])`.
- Karena UtiVerse baru mulai, data history mungkin kosong — aman.

---

## 6. Konteks Periode (helper terpusat)

Buat helper untuk menentukan periode yang sedang dilihat:
- `PeriodContext::resolve($request)` → mengembalikan `academic_year_id` + `semester_id`.
  - Jika request punya `academic_year_id`/`semester_id` (mode arsip) → pakai itu (validasi ada).
  - Selain itu → periode aktif (`is_current`).
- Bisa berupa trait/service kecil dipakai controller.

Daftar siswa untuk konteks periode:
```php
SyncedStudent::whereHas('enrollments', fn($q) =>
    $q->where('academic_year_id', $ayId)->where('class_id', $classId)
)->get();
```

---

## 7. Refactor Controller (titik sentuh)

Ganti pola `SyncedStudent::where('class_id', X)` → berbasis enrollment periode terpilih.

- **AssignmentController**
  - `index` (siswa): filter assignment ke kelas siswa **pada periode aktif** + status published.
  - `grades`/`export`/`review`: daftar siswa dari enrollment (periode terpilih), assignment stamped.
  - `submit`/`gradeStudent`: verifikasi keanggotaan lewat enrollment periode assignment, bukan `class_id` sekarang.
  - `store`: set `academic_year_id`+`semester_id` assignment dari material.
- **MaterialController**: query siswa & tampilan berbasis enrollment; filter arsip di index guru.
- **AttendanceController**: daftar siswa saat `store` dari enrollment periode aktif; index filter periode.
- **AttendanceRecapController**: sudah pakai `semester_id` — tambah pilihan TP + daftar siswa dari enrollment.
- **DashboardController** (siswa): materi/tugas dari enrollment aktif.
- **StudentController**: filter per kelas + periode.
- **Arena/Game/Quiz**: verifikasi kelas siswa via enrollment aktif.

Prinsip aman: untuk **siswa** selalu pakai enrollment **aktif** (mereka hanya berinteraksi periode berjalan).
Untuk **guru/admin arsip** boleh memilih periode lampau (read-only).

---

## 8. UI (filter arsip)

- Tambah dropdown "Tahun Pelajaran" + "Semester" (default periode aktif) di:
  Absensi Index, Rekap Absensi, Nilai/Grade Index & Export, Materi Index.
- Data periode dikirim via props (sudah ada `currentAcademicYear`/`currentSemester` di shared).
- Kirim daftar semua `academicYears` (dengan `semesters`) untuk dropdown.
- Halaman "Arsip" khusus (opsional Fase 2): pilih TP → lihat ringkasan kelas, nilai, absensi.

---

## 9. Command `utiverse:reset-sync` (amankan)

- **Jangan** `truncate` synced tables (menghapus arsip via cascade).
- Ganti perilaku:
  - Opsi A (default baru): hanya **nonaktifkan** enrollment periode lama & set siswa lulus =
    tidak menghapus apa pun. Kenaikan kelas dilakukan lewat sync (enrollment baru).
  - Opsi B: pertahankan command untuk keperluan dev **reset total**, tapi beri flag eksplisit
    `--hard` + peringatan keras, dan default menolak jika ada data pembelajaran.
- Betulkan data: set semester aktif ke `ganjil` TP `2025/2026` (bug #8).

---

## 10. Alur "Naik Kelas" (target akhir)

1. Admin sekolah menjalankan kenaikan kelas di **API pusat** (`/students/promote`) untuk TP baru.
2. Di UtiVerse: buat TP baru + set aktif (Settings).
3. Jalankan **Sync Siswa**. Karena TP aktif berubah, sync membuat **enrollment baru** untuk siswa
   di kelas barunya. Enrollment TP lama tetap ada.
4. Data lama (materi/tugas/submission/absensi) tetap ada, stamped TP/semester lama.
5. Guru buka menu Arsip → pilih TP lama → lihat kelas, siswa, nilai, absensi persis seperti dulu.
6. Siswa hanya melihat periode aktif; riwayat pribadinya bisa ditampilkan (Fase 2).

---

## 11. Urutan Eksekusi (bertahap, aman)

1. **Migrasi**: `student_enrollments`; kolom periode di `assignments`; backfill assignment dari material;
   backfill enrollment dari `synced_students.class_id` untuk TP aktif.
2. **Model**: `StudentEnrollment`, update `SyncedStudent`/`SyncedClass`/`Assignment`.
3. **Sync**: enrollment di `syncStudents` + `syncStudentHistory`.
4. **Helper periode** + refactor query siswa (enrollment).
5. **Assignment store** set periode.
6. **UI filter arsip** (bertahap per halaman).
7. **Amankan reset command** + betulkan semester aktif.
8. **Test** alur naik kelas end-to-end.

Setiap langkah dijalankan + diuji sebelum lanjut, agar data 330 siswa & submission tak rusak.
