# Technical Specification — Sistem Absensi Digital SMA

> **Versi:** 1.0.0 · **Tanggal:** April 2026 · **Stack:** Laravel 13.x · MySQL 8.0+ · Blade + Tailwind + Flowbite + Alpine.js

---

## 1. Ringkasan Arsitektur

Aplikasi ini adalah monolith Laravel klasik dengan 4 panel role-based (super_admin, operator, teacher, student) ditambah satu halaman kiosk publik (diotorisasi via token, bukan session). Frontend full server-rendered (Blade), diperkaya Alpine.js untuk interaksi ringan (modal, toggle, form dinamis) dan `html5-qrcode` khusus di halaman scanner. Tidak ada SPA, tidak ada API JSON terpisah — semua route mengembalikan view Blade kecuali endpoint scan QR yang mengembalikan JSON. Scheduler Laravel menangani auto-absent dan alert via cron. Notifikasi in-app menggunakan Laravel Notifications dengan database driver (tabel `notifications`).

### Diagram Alur Data

```
┌─────────────────────────────────────────────────────────────────────┐
│                        BROWSER CLIENTS                              │
├──────────┬──────────┬──────────┬──────────┬─────────────────────────┤
│ Panel    │ Panel    │ Panel    │ Panel    │ Halaman Kiosk           │
│ Super    │ Operator │ Guru     │ Siswa    │ (Scanner QR)            │
│ Admin    │          │          │          │                         │
└────┬─────┴────┬─────┴────┬─────┴────┬─────┴────────────┬────────────┘
     │          │          │          │                   │
     │ session  │ session  │ session  │ session           │ kiosk_token
     │ auth     │ auth     │ auth     │ auth              │ (no session)
     ▼          ▼          ▼          ▼                   ▼
┌─────────────────────────────────────────────────────────────────────┐
│                     LARAVEL APPLICATION                             │
│                                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
│  │ Web Routes   │  │ Middleware   │  │ Controllers              │  │
│  │ (role-based  │──│ - auth       │──│ - ResourceControllers    │  │
│  │  prefixes)   │  │ - role       │  │ - KioskScanController    │  │
│  │              │  │ - kiosk.auth │  │ - LeaveRequestController │  │
│  └──────────────┘  └──────────────┘  │ - ReportController       │  │
│                                       │ - PromotionController    │  │
│                                       └──────────┬───────────────┘  │
│                                                  │                  │
│  ┌──────────────────────────────┐  ┌─────────────▼───────────────┐  │
│  │ Task Scheduler              │  │ Services / Actions          │  │
│  │ - AutoAbsentTeacher         │  │ - ScanProcessingService     │  │
│  │ - AutoAbsentStudent         │  │ - LeaveApprovalAction       │  │
│  │ - AutoNotTeaching           │  │ - PromotionAction           │  │
│  │ - AlertTeacherLateCheckIn   │  │ - QrRegenerateAction        │  │
│  │ - AlertStudentAbsenceRate   │  │ - AttendanceCorrectionAction│  │
│  │ - AlertTeacherNotTeaching   │  └─────────────┬───────────────┘  │
│  └──────────────────────────────┘                │                  │
│                                                  ▼                  │
│                                        ┌─────────────────┐         │
│                                        │  Eloquent Models │         │
│                                        │  + Notifications │         │
│                                        └────────┬────────┘         │
└─────────────────────────────────────────────────┼──────────────────┘
                                                  ▼
                                          ┌──────────────┐
                                          │   MySQL 8.0  │
                                          └──────────────┘
```

---

## 2. Detail Endpoint Utama (Non-Standar CRUD)

Endpoint standar CRUD (users, teachers, students, classrooms, subjects, academic_years, schedules, holidays, settings) menggunakan `Route::resource()` biasa. Berikut endpoint yang memiliki logika bisnis khusus:

### 2.1 Kiosk / Scan QR

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `GET` | `/kiosk/{token}` | `KioskController@show` | Validasi kiosk token → tampilkan halaman scanner. Middleware: `kiosk.auth` |
| `POST` | `/kiosk/{token}/scan` | `KioskScanController@process` | Terima QR payload via AJAX, return JSON. Lihat validation chain di §4. Middleware: `kiosk.auth`, throttle (opsional) |
| `GET` | `/kiosk/{token}/health` | `KioskController@health` | Endpoint ping untuk indikator koneksi. Return `{ "status": "ok" }` |

**Catatan penting:** Route kiosk **TIDAK** melewati middleware `auth` session. Otentikasi via `kiosk_token` di URL + middleware custom `KioskAuthMiddleware`.

### 2.2 Sesi Mengajar (Guru)

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `POST` | `/teacher/teaching/{schedule}/start` | `TeachingAttendanceController@start` | Buat record `TEACHING_ATTENDANCES` status `teaching` + record waktu mulai. Validasi: jadwal milik guru ini, hari cocok, belum ada record hari ini, bukan hari libur |
| `POST` | `/teacher/teaching/{schedule}/end` | `TeachingAttendanceController@end` | Update record: catat waktu selesai. Validasi: record harus sudah ada dan status `teaching` |

### 2.3 Absensi Mapel Siswa (Input oleh Guru)

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `GET` | `/teacher/teaching/{schedule}/attendance` | `SubjectAttendanceController@create` | Tampilkan form daftar siswa di kelas. Syarat: guru sudah klik "Mulai Sesi" hari ini |
| `POST` | `/teacher/teaching/{schedule}/attendance` | `SubjectAttendanceController@store` | Bulk insert/update `STUDENT_SUBJECT_ATTENDANCES`. Validasi: sesi aktif, guru authorized |

### 2.4 Guru Pengganti

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `POST` | `/admin/substitutes` | `SubstituteTeacherController@store` | Tetapkan guru pengganti. Validasi: jadwal valid, tanggal valid, guru pengganti aktif |
| `DELETE` | `/admin/substitutes/{id}` | `SubstituteTeacherController@destroy` | Batalkan penunjukan (hanya jika sesi belum dimulai) |

### 2.5 Approval Izin

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `PATCH` | `/admin/teacher-leaves/{id}/approve` | `TeacherLeaveController@approve` | Set status `approved`, trigger update absensi harian otomatis |
| `PATCH` | `/admin/teacher-leaves/{id}/reject` | `TeacherLeaveController@reject` | Set status `rejected` + reviewer notes |
| `PATCH` | `/admin/student-leaves/{id}/approve` | `StudentLeaveController@approve` | Sama, + update absensi harian & mapel siswa |
| `PATCH` | `/admin/student-leaves/{id}/reject` | `StudentLeaveController@reject` | Set status `rejected` |

**Note:** Prefix `/admin/` di sini bisa diakses oleh `super_admin` DAN `operator`. Gunakan middleware `role:super_admin,operator`.

### 2.6 Kenaikan Kelas

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `GET` | `/admin/promotions` | `PromotionController@index` | Tampilkan daftar siswa aktif + form keputusan. Hanya muncul jika Semester 2 sudah ditutup |
| `POST` | `/admin/promotions/execute` | `PromotionController@execute` | Proses bulk: naik/tinggal/lulus. **Irreversible.** Wajib confirmation modal. Gunakan DB transaction |

### 2.7 QR Token Management

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `POST` | `/admin/teachers/{id}/regenerate-qr` | `QrTokenController@regenerateTeacher` | Nonaktifkan token lama + buat baru dalam 1 transaksi |
| `POST` | `/admin/students/{id}/regenerate-qr` | `QrTokenController@regenerateStudent` | Sama |
| `GET` | `/admin/teachers/{id}/download-qr` | `QrTokenController@downloadTeacher` | Generate QR image dari token aktif |
| `GET` | `/admin/students/{id}/download-qr` | `QrTokenController@downloadStudent` | Sama |

### 2.8 Laporan & Ekspor

| Method | Path | Controller | Catatan |
|--------|------|------------|---------|
| `GET` | `/admin/reports/teacher-daily` | `ReportController@teacherDaily` | Filter: date range, teacher_id |
| `GET` | `/admin/reports/student-daily` | `ReportController@studentDaily` | Filter: date range, classroom_id |
| `GET` | `/admin/reports/teaching` | `ReportController@teaching` | Filter: semester_id, teacher_id |
| `GET` | `/admin/reports/student-subject` | `ReportController@studentSubject` | Filter: semester_id, classroom_id |
| `GET` | `/admin/reports/{type}/export-pdf` | `ReportExportController@pdf` | DomPDF generation |
| `GET` | `/admin/reports/{type}/export-excel` | `ReportExportController@excel` | Laravel Excel export |

---

## 3. Catatan Penting Skema Database

### 3.1 Relasi Non-Trivial

| Relasi | Penjelasan |
|--------|------------|
| `USERS ↔ TEACHERS / STUDENTS` | One-to-one. `USERS` menyimpan kredensial, `TEACHERS`/`STUDENTS` menyimpan data profil. Tidak semua user punya profil (super_admin & operator tidak). Buat observer atau event listener untuk auto-create profil saat user dengan role teacher/student dibuat. |
| `SCHEDULES` (composite unique) | Unique constraint pada `[semester_id, day_of_week, session_order, classroom_id]` DAN `[semester_id, day_of_week, session_order, teacher_id]` untuk mencegah konflik jadwal. Implementasikan via migration `unique()` + validasi di FormRequest. |
| `CLASSROOM_HOLIDAYS` (pivot) | Pivot table tanpa timestamps. Hanya digunakan jika `HOLIDAYS.scope = 'per_class'`. Saat scope `whole_school`, tabel ini tidak terisi untuk holiday tersebut. |
| `SUBSTITUTE_TEACHERS` | Unique constraint pada `[schedule_id, date]` — hanya 1 pengganti per jadwal per tanggal. Perhatikan: `original_teacher_id` harus cocok dengan `SCHEDULES.teacher_id`. |
| `TEACHING_ATTENDANCES.teacher_id` | Bisa berbeda dari `SCHEDULES.teacher_id` jika ada guru pengganti. Selalu isi dari guru yang benar-benar mengajar. |

### 3.2 Pilihan Tipe Data Penting

| Kolom | Tipe | Alasan |
|-------|------|--------|
| `check_in_time` / `check_out_time` | `TIME` (bukan `TIMESTAMP`) | Kita hanya butuh jam, tanggal sudah ada di kolom `date` terpisah |
| `date` (di tabel absensi) | `DATE` | Bukan `TIMESTAMP`. Digunakan sebagai bagian dari unique constraint `[entity_id, date]` |
| `HOLIDAYS.start_date` / `end_date` | `DATE` | Range tanggal libur. Query-nya: `WHERE date BETWEEN start_date AND end_date` |
| `token` (QR & Kiosk) | `STRING(64)` | Gunakan `Str::random(64)` — cukup panjang, cryptographically random via Laravel |
| `status` fields | `STRING` (enum via PHP) | Jangan pakai MySQL `ENUM` — sulit di-migrate. Definisikan sebagai PHP enum atau const di model, validasi via `Rule::in()` |
| `SETTINGS.value` | `STRING` | Semua value disimpan sebagai string, di-cast sesuai kolom `type`. Buat accessor/helper: `Setting::get('key')` yang otomatis cast |

### 3.3 Strategi Soft Delete

**Tidak menggunakan soft delete.** Alasan:
- Data master (guru, siswa, kelas, mapel) menggunakan flag `is_active` — lebih fleksibel karena bisa diaktifkan kembali.
- Data transaksional (absensi, leave requests) tidak dihapus sama sekali — historical data.
- `USERS.is_active` digunakan untuk menonaktifkan login tanpa menghapus data.

> **Implikasi:** Semua query daftar aktif harus selalu filter `->where('is_active', true)`. Buat scope `scopeActive()` di setiap model yang punya flag ini.

### 3.4 Indexing yang Perlu Ditambahkan

```
-- Absensi harian (query paling sering)
TEACHER_DAILY_ATTENDANCES: INDEX(teacher_id, date)  -- + UNIQUE
STUDENT_DAILY_ATTENDANCES: INDEX(student_id, date)  -- + UNIQUE

-- Absensi per sesi
TEACHING_ATTENDANCES: INDEX(schedule_id, date)       -- + UNIQUE
STUDENT_SUBJECT_ATTENDANCES: INDEX(schedule_id, date, student_id) -- + UNIQUE

-- QR lookup saat scan (critical path)
TEACHER_QR_TOKENS: INDEX(token, is_active)
STUDENT_QR_TOKENS: INDEX(token, is_active)

-- Kiosk token lookup
KIOSK_TOKENS: INDEX(token, is_active)

-- Holiday check (setiap hari, setiap scan)
HOLIDAYS: INDEX(start_date, end_date)

-- Leave request approval queue
TEACHER_LEAVE_REQUESTS: INDEX(status)
STUDENT_LEAVE_REQUESTS: INDEX(status)
```

---

## 4. Aturan Bisnis & Validasi

### 4.1 Scan QR — Validation Chain (FR-08.3)

Setiap scan yang masuk di `KioskScanController@process` melewati chain ini **secara berurutan**. Gagal di step manapun → return error + stop.

1. **Kiosk Token valid?** — Token dari URL ada di DB, `is_active = true`, `expires_at > now()`
2. **QR Token dikenal?** — Cari di `TEACHER_QR_TOKENS` lalu `STUDENT_QR_TOKENS` (where `is_active = true`)
3. **Pemilik aktif?** — `TEACHERS.is_active` atau `STUDENTS.is_active` = `true`
4. **Bukan hari libur?** — Cek `HOLIDAYS` dimana hari ini masuk dalam range DAN `is_daily_attendance_active = false`. Untuk siswa, cek juga `CLASSROOM_HOLIDAYS`
5. **Double scan check** — Jika ada record hari ini dan `check_in_time` ada:
   - Jika `check_out_time` sudah ada → tolak ("sudah tercatat")
   - Jika selisih waktu scan terakhir < 1 menit → tolak ("coba beberapa saat lagi", FR-08.4)
   - Else → catat sebagai `check_out_time`
6. **Catat kehadiran** — Scan pertama: buat record + set `check_in_time` + tentukan status `present` / `late`

**Tips implementasi:** Buat sebagai service class `ScanProcessingService` dengan method yang return DTO/result object berisi `success`, `message`, `type` (check_in/check_out/rejected), dan `data` (info untuk card feedback).

### 4.2 Penentuan Status Hadir/Terlambat

```
if check_in_time <= Setting::get('attendance.late_threshold')
    → status = 'present'
else
    → status = 'late'
```

Setting key: `attendance.late_threshold_teacher` dan `attendance.late_threshold_student` (format `HH:MM`).

### 4.3 Semester & Tahun Ajaran

- Hanya **1 semester aktif** di seluruh sistem pada satu waktu (`SEMESTERS.is_active = true`)
- Saat aktivasi semester baru, semester lama **wajib** di-set `is_active = false` terlebih dahulu. Lakukan dalam 1 DB transaction
- Setiap `ACADEMIC_YEARS` wajib punya tepat 2 semester — validasi saat create/edit
- Semester aktif = acuan untuk menampilkan jadwal, membuat sesi absensi mengajar, dll

### 4.4 Kenaikan Kelas (FR-05)

- **Precondition:** Semester 2 tahun ajaran terkini sudah ditutup (tidak aktif). Jika belum → fitur non-accessible
- **Keputusan per siswa:** `naik` / `tinggal` / `lulus`
  - `naik`: pindahkan `STUDENTS.classroom_id` ke kelas baru yang dipilih admin
  - `tinggal`: `classroom_id` bisa tetap atau pindah kelas lain di tingkat yang sama
  - `lulus` (khusus XII): set `STUDENTS.is_active = false` + `USERS.is_active = false`
- **Irreversible** — tambahkan confirmation dialog yang tegas. Pertimbangkan menyimpan log di `SETTINGS` atau tabel audit sederhana
- **Wrap dalam DB::transaction** — jika 1 siswa gagal, rollback semua

### 4.5 Izin / Leave Request

- Flow: `pending` → `approved` | `rejected` | `canceled`
- `canceled` hanya bisa dari pemilik request dan hanya saat masih `pending`
- Saat `approved`:
  - **Guru:** Loop tanggal `start_date` s/d `end_date`, update/create `TEACHER_DAILY_ATTENDANCES.status` → `permission` atau `sick` (berdasarkan jenis izin yang perlu ditambahkan — **lihat catatan di bawah**)
  - **Siswa:** Sama + update `STUDENT_SUBJECT_ATTENDANCES` pada tanggal tersebut jika sudah ada record
- Skip tanggal yang merupakan hari libur
- `is_admin_input = true` → buat langsung dengan status `approved`, skip flow approval

> [!WARNING]
> **Gap di skema:** Tabel `TEACHER_LEAVE_REQUESTS` / `STUDENT_LEAVE_REQUESTS` tidak punya kolom `leave_type` (sakit/izin/cuti). Ini diperlukan untuk menentukan apakah status absensi harian di-update ke `permission` atau `sick`. **Solusi:** Tambahkan kolom `type ENUM('permission', 'sick', 'other')` atau gunakan field `reason` sebagai pembeda (kurang reliable). **Rekomendasi: tambahkan kolom `type`.**

### 4.6 Guru Pengganti (FR-13)

- Saat ada record di `SUBSTITUTE_TEACHERS` untuk `schedule_id` + `date`:
  - Sesi mengajar muncul di panel **guru pengganti**, bukan guru asli
  - `TEACHING_ATTENDANCES.teacher_id` diisi ID guru pengganti
  - `STUDENT_SUBJECT_ATTENDANCES`: `submitted_by` mengacu pada user guru pengganti
- Query jadwal guru (hari ini) harus selalu cek `SUBSTITUTE_TEACHERS` dulu

### 4.7 Holiday Check Logic

```php
// Pseudo-code: apakah tanggal X adalah hari libur bagi entity?
function isHoliday(Date $date, ?int $classroomId = null): bool
{
    $holiday = Holiday::where('start_date', '<=', $date)
        ->where('end_date', '>=', $date)
        ->where('is_daily_attendance_active', false)
        ->first();

    if (!$holiday) return false;
    if ($holiday->scope === 'whole_school') return true;
    if ($classroomId && $holiday->classrooms->contains($classroomId)) return true;

    return false;
}
```

**Penting:** Untuk guru, cukup cek scope `whole_school`. Untuk siswa, cek juga `per_class` berdasarkan `classroom_id`-nya.

---

## 5. Scheduler & Queue

Semua task didaftarkan di `routes/console.php` (Laravel 13.x) atau `app/Console/Kernel.php`. Tidak menggunakan queue worker — semua synchronous via scheduler.

### 5.1 Daftar Scheduled Tasks

| Command | Schedule | Deskripsi |
|---------|----------|-----------|
| `app:auto-absent-teachers` | Daily, jam dari `Setting('attendance.auto_absent_time')`, default `10:00` | Cek guru aktif yg belum punya record hari ini → buat record status `absent` + `is_auto = true`. Skip hari libur. |
| `app:auto-absent-students` | Daily, jam sama seperti di atas | Sama untuk siswa. Cek per kelas (untuk holiday scope `per_class`). |
| `app:auto-not-teaching` | Setiap 15 menit (jam sekolah) | Cek `SCHEDULES` yang sudah lewat `end_time` tapi belum ada `TEACHING_ATTENDANCES` → buat record `not_teaching` + `is_auto = true`. Cek guru pengganti. |
| `app:alert-teacher-late-checkin` | Daily, jam dari `Setting('alert.teacher_late_time')`, default `07:30` | Kirim notifikasi ke super_admin & operator jika ada guru yang belum check-in. |
| `app:alert-student-absence-rate` | Daily, jam dari `Setting('alert.absence_check_time')`, default `09:00` | Hitung % siswa absent per kelas. Jika melebihi threshold → kirim alert. |
| `app:alert-teacher-not-teaching` | Setiap 15 menit | Jika ada jadwal yang sudah lewat waktu mulai tapi guru belum klik "Mulai Sesi" → kirim alert. |

### 5.2 Tips Implementasi Scheduler

- Semua command harus cek `isHoliday()` di awal — return early jika hari libur
- Log output ke Laravel log (bukan stdout) agar bisa di-debug: `$this->info()` + `Log::info()`
- Gunakan `withoutOverlapping()` untuk command yang bisa lama (auto-absent dengan banyak siswa)
- Jalankan via cron entry standar: `* * * * * cd /path && php artisan schedule:run >> /dev/null 2>&1`

---

## 6. Keamanan & Autentikasi

### 6.1 Autentikasi

- **Session-based**, custom controller (bukan Breeze/Fortify/Jetstream)
- Login: `AuthController@login` → validasi email + password → `Auth::attempt()` → redirect by role
- Cek `USERS.is_active` sebelum `Auth::attempt()` — atau setelah attempt, cek flag dan logout jika `false`
- Logout: `Auth::logout()` + `$request->session()->invalidate()` + regenerate token

### 6.2 Middleware Stack

| Middleware | Alias | Diterapkan Pada |
|------------|-------|-----------------|
| `Authenticate` | `auth` | Semua route panel (admin, operator, teacher, student) |
| `RoleMiddleware` | `role:xxx` | Per group route. Contoh: `role:super_admin`, `role:super_admin,operator`, `role:teacher` |
| `KioskAuthMiddleware` | `kiosk.auth` | Route kiosk. Validasi token dari parameter URL |
| `PreventRequestsDuringMaintenance` | bawaan | Global |
| `VerifyCsrfToken` | bawaan | Global (exclude route kiosk scan jika perlu — atau tetap pakai CSRF via meta tag di halaman kiosk) |

### 6.3 Route Groups

```php
// routes/web.php - Struktur utama
Route::middleware(['auth', 'role:super_admin'])->prefix('admin')->name('admin.')->group(/* ... */);
Route::middleware(['auth', 'role:super_admin,operator'])->prefix('operator')->name('operator.')->group(/* ... */);
Route::middleware(['auth', 'role:teacher'])->prefix('teacher')->name('teacher.')->group(/* ... */);
Route::middleware(['auth', 'role:student'])->prefix('student')->name('student.')->group(/* ... */);
Route::middleware(['kiosk.auth'])->prefix('kiosk')->name('kiosk.')->group(/* ... */);
```

> [!IMPORTANT]
> **Operator vs Admin routes:** Banyak fitur yang diakses baik oleh `super_admin` maupun `operator` (monitoring absensi, koreksi, approval izin). Ada 2 pendekatan:
> 1. **Route terpisah** (`/admin/...` dan `/operator/...`) dengan view yang sama — duplikasi route
> 2. **Route shared** di bawah `/admin/...` dengan middleware `role:super_admin,operator` — lebih DRY
>
> **Rekomendasi:** Pendekatan 2. Bedakan hanya dashboard dan menu sidebar via `@can` / `@role` di Blade.

### 6.4 Proteksi File Upload

- Lampiran izin disimpan di `storage/app/leave-attachments/` (disk `local`, bukan `public`)
- Akses file via route khusus dengan middleware auth:
  ```
  GET /attachments/leave/{type}/{id} → LeaveAttachmentController@show
  ```
- Controller cek: user = pemilik request ∨ user = super_admin/operator
- Return `Storage::download()` — file tidak pernah expose langsung

### 6.5 QR Token Security

- Token: `Str::random(64)` — 64 karakter alphanumeric random
- Satu token aktif per individu pada satu waktu
- Regenerate = DB transaction: `old_token.is_active = false` + `new_token.is_active = true`
- Token **bukan** UUID atau ID-based — tidak bisa ditebak
- Kiosk Token: sama + punya `expires_at` — selalu cek expiry

---

## 7. Konfigurasi Environment

### 7.1 Variabel `.env` Khusus Aplikasi

```env
# === Standar Laravel (sudah ada) ===
APP_NAME="Absensi Digital SMA"
APP_ENV=production
APP_URL=https://absensi.sekolah.sch.id
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=absensi_sma
SESSION_DRIVER=database   # atau file — file lebih simple untuk single server
SESSION_LIFETIME=480       # 8 jam = 1 hari sekolah

# === Custom ===
# Tidak banyak. Mayoritas konfigurasi operasional disimpan di tabel SETTINGS
# agar bisa diubah tanpa akses server.
```

### 7.2 Settings di Database (tabel `SETTINGS`)

Berikut daftar setting kunci beserta default value yang perlu di-seed:

| Key | Group | Type | Default | Deskripsi |
|-----|-------|------|---------|-----------|
| `attendance.late_threshold_teacher` | attendance | string | `"07:15"` | Batas jam check-in guru — lewat ini = `late` |
| `attendance.late_threshold_student` | attendance | string | `"07:15"` | Batas jam check-in siswa |
| `attendance.auto_absent_time` | attendance | string | `"10:00"` | Jam eksekusi auto-absent scheduler |
| `attendance.scan_cooldown_seconds` | attendance | integer | `60` | Durasi cooldown antar scan (detik) |
| `alert.teacher_late_time` | alert | string | `"07:30"` | Jam kirim alert guru belum check-in |
| `alert.absence_check_time` | alert | string | `"09:00"` | Jam kirim alert tingkat absensi siswa |
| `alert.student_absence_threshold` | alert | integer | `30` | Threshold % siswa absent per kelas untuk trigger alert |
| `alert.teaching_grace_minutes` | alert | integer | `15` | Menit toleransi sebelum alert guru belum mulai sesi |
| `system.school_name` | system | string | `"SMA ..."` | Nama sekolah (ditampilkan di UI & laporan) |
| `system.school_address` | system | string | `""` | Alamat sekolah (untuk header laporan) |
| `system.school_logo` | system | string | `""` | Path logo sekolah |

### 7.3 Helper untuk Akses Setting

```php
// app/Helpers/SettingHelper.php atau app/Models/Setting.php
public static function get(string $key, $default = null)
{
    $setting = cache()->rememberForever("setting.{$key}", function () use ($key) {
        return static::where('key', $key)->first();
    });

    if (!$setting) return $default;

    return match ($setting->type) {
        'integer' => (int) $setting->value,
        'boolean' => filter_var($setting->value, FILTER_VALIDATE_BOOLEAN),
        'json'    => json_decode($setting->value, true),
        default   => $setting->value,
    };
}
```

**Ingat:** Flush cache setting setiap kali admin update: `cache()->forget("setting.{$key}")`.

---

## 8. Catatan untuk Pengembangan (Solo Dev)

### 8.1 Kapan Boleh Skip Service Layer

| Situasi | Pendekatan |
|---------|------------|
| CRUD standar (master data, settings, holidays) | Langsung di controller. Tidak perlu service. Gunakan FormRequest untuk validasi. |
| Logika bisnis kompleks (scan processing, leave approval, kenaikan kelas, regenerate QR) | **Wajib** buat Service atau Action class. Ini yang akan berubah/bertambah paling sering. |
| Scheduler commands | Boleh taruh logika langsung di command, atau panggil service. Kalau logikanya < 30 baris, langsung di command saja. |
| Koreksi absensi manual | Buat `AttendanceCorrectionAction` — dipakai oleh admin controller DAN operator controller. |

**Rule of thumb:** Kalau logika yang sama dipanggil dari > 1 tempat, extract ke service/action.

### 8.2 Struktur Folder yang Disarankan

```
app/
├── Actions/                    # Single-purpose action classes
│   ├── ApproveLeaveAction.php
│   ├── ProcessQrScanAction.php
│   ├── RegenerateQrTokenAction.php
│   └── ExecutePromotionAction.php
├── Console/
│   └── Commands/
│       ├── AutoAbsentTeachers.php
│       ├── AutoAbsentStudents.php
│       ├── AutoNotTeaching.php
│       └── Send...Alert.php
├── Http/
│   ├── Controllers/
│   │   ├── Auth/
│   │   │   └── AuthController.php
│   │   ├── Admin/                # super_admin routes
│   │   ├── Operator/             # operator-specific (jika ada)
│   │   ├── Teacher/
│   │   ├── Student/
│   │   └── Kiosk/
│   │       ├── KioskController.php
│   │       └── KioskScanController.php
│   ├── Middleware/
│   │   ├── RoleMiddleware.php
│   │   └── KioskAuthMiddleware.php
│   └── Requests/                # FormRequest classes
├── Models/
├── Notifications/
│   ├── TeacherLateCheckInNotification.php
│   ├── HighAbsenceRateNotification.php
│   └── LeaveStatusChangedNotification.php
└── Services/                    # Kalau lebih nyaman service pattern
```

### 8.3 Dokumentasi Kode yang Cukup

- **Jangan** docblock setiap method CRUD — buang waktu
- **Do** docblock method yang punya aturan bisnis rumit (scan processing, leave approval chain)
- **Do** tulis `// WHY:` comment saat keputusan tidak obvious (misal: kenapa query pakai raw, kenapa validasi urut tertentu)
- **Do** buat `README.md` di root project berisi: cara setup, cara jalankan scheduler, dan daftar seeder yang wajib dijalankan
- **Do** tulis `@see FR-08.3` di atas method scan processing — link ke requirement agar gampang trace

### 8.4 Debugging Tips

| Masalah | Tools/Pendekatan |
|---------|------------------|
| Query lambat di laporan | `DB::enableQueryLog()` + `php artisan tinker` untuk test query. Pastikan index sudah benar (§3.4). |
| Scheduler tidak jalan | Cek cron entry. Tambahkan `->appendOutputTo(storage_path('logs/scheduler.log'))` ke setiap command untuk tracing. |
| Scan QR error tapi tidak jelas | Log setiap step di validation chain ke `Log::channel('scan')`. Buat custom log channel di `config/logging.php`. |
| Memory leak halaman kiosk | Pastikan `html5-qrcode` scanner di-stop dan di-restart dengan benar. Hati-hati event listener yang tidak di-remove di Alpine.js `x-init`/`destroy`. |
| Race condition saat regenerate QR | Selalu wrap dalam `DB::transaction()` + gunakan `lockForUpdate()` saat query token lama. |
| Data absensi tidak muncul | Cek semester aktif. Hampir selalu karena `SEMESTERS.is_active = false` atau jadwal filter salah. |

### 8.5 Seeder yang Wajib Ada

```
DatabaseSeeder
├── RoleSeeder           → (Implisit — role ada di USERS.role)
├── SuperAdminSeeder     → Buat 1 akun super_admin default
├── SettingsSeeder       → Seed semua key di tabel SETTINGS (§7.2)
└── (Dev only)
    ├── TeacherSeeder
    ├── StudentSeeder
    ├── ClassroomSeeder
    ├── AcademicYearSeeder
    └── ScheduleSeeder
```

### 8.6 Urutan Implementasi yang Disarankan

Untuk solo dev, build secara **vertikal** (satu fitur end-to-end) bukan horizontal (semua model dulu, semua controller dulu):

| Phase | Fitur | Alasan |
|-------|-------|--------|
| 1 | Auth + Role Middleware + Layout dasar per panel | Foundation. Semua fitur lain butuh ini. |
| 2 | Master Data (Users, Teachers, Students, Classrooms, Subjects) + Settings | CRUD standar, pemanasan. Sekalian setup Form Request pattern. |
| 3 | Academic Years + Semesters + Schedules | Fondasi untuk absensi. Validasi konflik jadwal penting. |
| 4 | QR Token + Kiosk Token + Halaman Scanner + Absensi Harian | Core feature. Paling kompleks. Kerjakan setelah fondasi stabil. |
| 5 | Sesi Mengajar + Absensi Mapel + Guru Pengganti | Bergantung pada jadwal dan absensi harian. |
| 6 | Leave Request + Approval + Auto-update absensi | Fitur pendukung. |
| 7 | Scheduler (auto-absent, alert) | Bergantung pada semua data absensi sudah benar. |
| 8 | Holidays + Kalender | Cross-cutting — akan mempengaruhi banyak fitur yang sudah ada. |
| 9 | Laporan + Ekspor PDF/Excel | Bisa dikerjakan terakhir karena read-only. |
| 10 | Kenaikan Kelas | Fitur akhir tahun, bisa dikerjakan paling akhir. |
| 11 | Notifikasi In-App | Polish — bisa ditambahkan setelah core semua jalan. |

### 8.7 Hal yang Mudah Lupa

> [!CAUTION]
> Checklist ini sering terlewat saat implementasi — bookmark dan cek rutin.

- [ ] **Cek hari libur** di SETIAP proses yang membuat record absensi (scan, auto-absent, sesi mengajar, approval izin)
- [ ] **Cek guru pengganti** di SETIAP query jadwal hari ini untuk guru (sesi mengajar, auto-not-teaching)
- [ ] **Cek semester aktif** — jangan hardcode semester ID, selalu ambil dari `Semester::active()`
- [ ] **Unique constraint** `[teacher_id/student_id, date]` di tabel absensi harian — cegah duplikat record
- [ ] **File upload validation** — restrict MIME type (pdf, jpg, png), max size (2MB), dan rename file (jangan pakai nama asli)
- [ ] **Timezone** — set `APP_TIMEZONE=Asia/Jakarta` di `.env` dan `config/app.php`. Semua operasi time-sensitive (scan, late check, scheduler) bergantung pada ini
- [ ] **Seed Settings** — aplikasi akan error di banyak tempat jika tabel `SETTINGS` kosong
- [ ] **Flush setting cache** setelah update — atau user akan bingung kenapa perubahan tidak efektif
- [ ] **CSRF di halaman kiosk** — halaman kiosk pakai form POST (scan), pastikan meta tag CSRF ada dan dikirim via AJAX header
- [ ] **is_active check saat login** — jangan lupa cek di `AuthController`, bukan cuma di middleware

---

> **Dokumen ini adalah panduan implementasi, bukan kontrak. Sesuaikan dengan kebutuhan aktual saat coding. yang penting: ship, iterate, improve.**
