# Dokumentasi Teknis: Sistem Multi-Sesi Absensi

## Arsitektur Sistem

### Alur Absensi Multi-Sesi

```
┌─────────────────────────────────────────────────────────────────┐
│ 1. Peserta Scan QR Code                                        │
│    - Halaman: /scan                                             │
│    - Request: GET dengan unique_code & waktu (masuk/pulang)    │
└────────────────────────┬────────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────────┐
│ 2. System Request: SELECT id_sesi yang is_active = 1           │
│    - Models: SesiModel::getActiveSessions()                     │
│    - Result: Array sesi aktif untuk dropdown                   │
└────────────────────────┬────────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────────┐
│ 3. Peserta Pilih Sesi dari Dropdown                            │
│    - Request: POST dengan id_sesi                              │
│    - Validasi: id_sesi required, tidak boleh kosong            │
└────────────────────────┬────────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────────┐
│ 4. Scan Controller: Validasi Waktu Sesi                        │
│    - Method: PresensiPesertaModel::validateSessionTime()       │
│    - Input: id_sesi, jam_absen, tanggal                        │
│    - Process:                                                   │
│      a. Cek sesi exists dan is_active                          │
│      b. Cek jam_absen <= jam_selesai (sesi belum berakhir)    │
│      c. Hitung terlambat = MAX(0, jam_absen - jam_mulai)      │
│    - Output: { valid, terlambat_menit, pesan }                │
└────────────────────────┬────────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────────┐
│ 5a. VALID: Simpan Presensi                                     │
│    - Method: PresensiPesertaModel::absenMasuk()                │
│    - Insert: id_peserta, id_sesi, tanggal, jam_masuk           │
│    - id_kehadiran = Hadir (enum value)                         │
│    - Notify: WhatsApp (jika enabled)                           │
│    - Response: Success view dengan data absensi                │
└────────────────────────┬────────────────────────────────────────┘
         ┌───────────────┘
         │
│ 5b. INVALID: Show Error                                        │
│    - Response: Error view dengan pesan validasi                │
│    - Contoh: "Sesi telah berakhir pada jam 09:00"             │
└────────────────────────────────────────────────────────────────┘
```

---

## Komponen Utama

### 1. **Model: SesiModel** (`app/Models/SesiModel.php`)

```php
public function getActiveSessions()
  → Ambil semua sesi dengan is_active = 1
  → Return: Array of sessions
  
public function getSession($id)
  → Ambil detail sesi berdasarkan ID
  → Return: Object sesi
```

### 2. **Model: PresensiPesertaModel** (`app/Models/PresensiPesertaModel.php`)

#### Method Existing:
```php
public function cekAbsen($id, $date, $idSesi)
  → Cek apakah peserta sudah absen untuk sesi ini hari ini
  → Return: false | id_presensi
  
public function absenMasuk($id, $date, $time, $idSesi, $idGrup)
  → Simpan data absensi masuk
  
public function getPresensiByIdPesertaTanggalSesi($id, $date, $idSesi)
  → Ambil data presensi spesifik sesi
```

#### Method Baru (Validasi Sesi):
```php
public function validateSessionTime($idSesi, $jamAbsen, $tanggal)
  → Validasi waktu absen terhadap jam sesi
  → Return: [
      'valid' => true/false,
      'terlambat_menit' => int,
      'pesan' => string
    ]
```

### 3. **Controller: Scan** (`app/Controllers/Scan.php`)

#### Flow Absensi Masuk - Peserta:

```php
public function absenMasuk($type, $result)
  ├─ Case TipeUser::Peserta:
  │  ├─ Get id_sesi dari request
  │  ├─ Validate id_sesi not empty
  │  ├─ Call validateSessionTime()
  │  │  ├─ If invalid: Show error + exit
  │  │  └─ If valid: Continue
  │  ├─ Call absenMasuk() untuk simpan
  │  ├─ Send WhatsApp notification
  │  └─ Return success view
```

### 4. **Routes** (`app/Config/Routes.php`)

```php
// Admin sesi management
$routes->group('sesi', function ($routes) {
  $routes->get('/', 'Admin\Sesi::index');
  $routes->get('create', 'Admin\Sesi::create');
  $routes->get('create-today', 'Admin\Sesi::createToday');
  $routes->post('store', 'Admin\Sesi::store');
  $routes->get('edit/(:any)', 'Admin\Sesi::edit/$1');
  $routes->post('update', 'Admin\Sesi::update');
  $routes->get('delete/(:any)', 'Admin\Sesi::delete/$1');
});

// Public scan (dengan sesi)
$routes->group('scan', function ($routes) {
  $routes->get('', 'Scan::index');
  $routes->get('masuk', 'Scan::index/Masuk');
  $routes->post('cek', 'Scan::cekKode');
});
```

---

## Database Schema

### Tabel: tb_sesi

```sql
CREATE TABLE tb_sesi (
  id_sesi INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  nama_sesi VARCHAR(100) NOT NULL,
  jam_mulai TIME NULL,
  jam_selesai TIME NULL,
  is_active TINYINT(1) DEFAULT 1 NOT NULL
);

-- Index
ALTER TABLE tb_sesi ADD INDEX idx_is_active (is_active);
```

### Tabel: tb_presensi_peserta (dengan sesi)

```sql
CREATE TABLE tb_presensi_peserta (
  id_presensi INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  id_peserta INT UNSIGNED NOT NULL,
  id_sesi INT UNSIGNED NOT NULL,        ← Reference ke sesi
  id_grup INT UNSIGNED NULL,
  tanggal DATE NOT NULL,
  jam_masuk TIME NULL,
  jam_keluar TIME NULL,
  id_kehadiran INT NOT NULL,
  keterangan VARCHAR(255) NULL,
  created_at DATETIME NULL,
  updated_at DATETIME NULL,
  
  -- Indexes
  UNIQUE KEY unique_absen (id_peserta, id_sesi, tanggal),
  INDEX idx_sesi (id_sesi),
  INDEX idx_tanggal (tanggal),
  
  -- Foreign Keys
  FOREIGN KEY (id_peserta) REFERENCES tb_peserta(id_peserta) ON DELETE CASCADE,
  FOREIGN KEY (id_sesi) REFERENCES tb_sesi(id_sesi) ON DELETE RESTRICT,
  FOREIGN KEY (id_kehadiran) REFERENCES tb_kehadiran(id_kehadiran)
);
```

---

## Validasi Logika

### validateSessionTime() - Pseudocode

```
FUNCTION validateSessionTime(idSesi, jamAbsen, tanggal)
  
  1. Query: sesi = SELECT * FROM tb_sesi WHERE id_sesi = idSesi
     IF sesi not found:
        RETURN { valid: false, pesan: "Sesi tidak ditemukan" }
  
  2. IF sesi.is_active = 0:
        RETURN { valid: false, pesan: "Sesi tidak aktif" }
  
  3. IF jamAbsen > sesi.jam_selesai:
        RETURN { 
          valid: false, 
          pesan: "Sesi telah berakhir pada jam " + sesi.jam_selesai 
        }
  
  4. Compute keterlambatan:
        IF jamAbsen > sesi.jam_mulai:
          terlambat = DIFF_MINUTES(jamAbsen, sesi.jam_mulai)
        ELSE:
          terlambat = 0
  
  5. RETURN { 
       valid: true, 
       terlambat_menit: terlambat, 
       pesan: "Absensi diterima" + (terlambat > 0 ? " (Terlambat " + terlambat + " menit)" : "")
     }
```

---

## Implementasi Fitur Terbaru

### Perubahan Controller Scan (v2)

**Sebelumnya (v1):**
```php
// Tidak validasi waktu sesi, hanya cek duplikasi
$sudahAbsen = $this->presensiPesertaModel->cekAbsen($id, $date, $currentSesi);
if ($sudahAbsen) return error("Sudah absen");
$this->presensiPesertaModel->absenMasuk($id, $date, $time, $currentSesi);
```

**Sekarang (v2):**
```php
// 1. Cek sesi dipilih
if (empty($currentSesi)) return error("Sesi belum dipilih");

// 2. Cek duplikasi
$sudahAbsen = $this->presensiPesertaModel->cekAbsen($id, $date, $currentSesi);
if ($sudahAbsen) return error("Sudah absen untuk sesi ini");

// 3. Validasi waktu sesi (NEW)
$validasi = $this->presensiPesertaModel->validateSessionTime($currentSesi, $time, $date);
if (!$validasi['valid']) return error($validasi['pesan']);

// 4. Simpan absensi
$this->presensiPesertaModel->absenMasuk($id, $date, $time, $currentSesi);

// 5. Notify dengan info keterlambatan
if ($validasi['terlambat_menit'] > 0) {
  $message .= " (Terlambat " . $validasi['terlambat_menit'] . " menit)";
}
```

---

## Error Scenarios & Handling

| Scenario | Error Message | HTTP Code | Action |
|----------|---------------|-----------|--------|
| id_sesi kosong | "Sesi absensi belum dipilih" | 200 | Show form kembali |
| Sesi tidak exist | "Sesi tidak ditemukan" | 200 | Show error |
| Sesi tidak aktif | "Sesi tidak aktif" | 200 | Show error |
| Jam absen > jam selesai | "Sesi telah berakhir pada jam XX:XX" | 200 | Show error |
| Sudah absen sesi ini | "Anda sudah absen untuk sesi ini" | 200 | Show error |
| Valid + tepat waktu | Success + "Absensi diterima" | 200 | Show success |
| Valid + terlambat | Success + "Terlambat XX menit" | 200 | Show success |

---

## Testing & QA

### Unit Test Cases

```php
// Test: validateSessionTime()

// Case 1: Valid absen - tepat waktu
$result = validateSessionTime(1, "07:00", "2026-07-10");
Assert: $result['valid'] == true
Assert: $result['terlambat_menit'] == 0

// Case 2: Valid absen - terlambat
$result = validateSessionTime(1, "07:30", "2026-07-10");
Assert: $result['valid'] == true
Assert: $result['terlambat_menit'] == 30

// Case 3: Invalid - sesi belum ada
$result = validateSessionTime(999, "07:00", "2026-07-10");
Assert: $result['valid'] == false
Assert: contains($result['pesan'], "tidak ditemukan")

// Case 4: Invalid - sesi sudah berakhir
$result = validateSessionTime(1, "09:01", "2026-07-10");
Assert: $result['valid'] == false
Assert: contains($result['pesan'], "telah berakhir")
```

### Integration Test

```
1. Create 2 sessions: Sesi 1 (07:00-09:00), Sesi 2 (09:15-11:15)
2. Peserta A absen Sesi 1 at 07:30 → Success (terlambat 30 min)
3. Peserta A absen Sesi 1 again → Error (sudah absen)
4. Peserta A absen Sesi 2 at 09:20 → Success (terlambat 5 min)
5. Peserta A absen Sesi 1 at 09:02 → Error (sesi berakhir)
6. Nonaktifkan Sesi 2 → Hilang dari dropdown
7. Aktifkan Sesi 2 kembali → Muncul di dropdown
```

---

## Performance Considerations

### Query Optimization

```sql
-- Index penting untuk performa
ALTER TABLE tb_presensi_peserta 
  ADD UNIQUE INDEX idx_absen (id_peserta, id_sesi, tanggal);

ALTER TABLE tb_presensi_peserta 
  ADD INDEX idx_sesi (id_sesi);

ALTER TABLE tb_sesi 
  ADD INDEX idx_active (is_active);
```

### Expected Query Time

- `getActiveSessions()`: ~2ms (dengan is_active index)
- `cekAbsen()`: ~5ms (dengan unique index)
- `validateSessionTime()`: ~1ms (in-memory, no query)

---

## Upgrade Guide

### Dari Sistem Lama (Single Session/Day)

Jika sebelumnya hanya 1 absensi per hari:

1. **Migration sudah tersedia**: `2026-07-09-000001_CreateAttendanceRefactorTables.php`
2. **Data lama tidak akan hilang**: Presensi lama tetap ada di tb_presensi_siswa
3. **New data ke tb_presensi_peserta**: Dengan kolom id_sesi

### Cara Migrate Data (Optional)

```php
// Script untuk copy data lama ke struktur baru
// Buat sesi tunggal untuk hari-hari sebelumnya
$sesi = $sesiModel->insert([
  'nama_sesi' => 'Absensi Harian',
  'jam_mulai' => '07:00',
  'jam_selesai' => '16:00',
  'is_active' => 0
]);

// Bisa copy data lama ke tb_presensi_peserta dengan id_sesi ini
// (Developer bisa buat migration khusus jika diperlukan)
```

---

## Troubleshooting untuk Developer

### Issue: validateSessionTime() tidak bisa find sesi

**Solusi:**
- Pastikan SesiModel sudah di-instantiate di PresensiPesertaModel
- Cek namespace import: `use App\Models\SesiModel;`
- Verify tabel tb_sesi exists dan has correct columns

### Issue: Keterlambatan tidak terhitung

**Solusi:**
- Cek server timezone di `.env`: `app.timezone = Asia/Jakarta`
- Verify Time class parsing: `Time::parse($date . ' ' . $jam)`
- Debug dengan: `log_message('info', 'Jam: ' . $jam . ', Diff: ' . $diff)`

### Issue: Peserta bisa absen setelah jam_selesai

**Solusi:**
- Verify update di Scan.php sudah deployed
- Cek validasi di `validateSessionTime()` line: `if ($jamAbsen > $jamSelesai)`
- Clear cache jika ada caching layer

---

## Future Enhancements

### v2.1 (Planned)

- [ ] **Multi-Session Absensi Pulang**: Cada sesi punya jam pulang sendiri
- [ ] **Session Exemptions**: Set peserta tertentu exempt dari validasi jam
- [ ] **Auto-create Sesi Template**: Buat 4-8 sesi otomatis dengan pattern
- [ ] **Session Scheduling**: Schedule sesi baru untuk minggu depan

### v2.2 (Proposed)

- [ ] **Smart Late Calculation**: Hitung keterlambatan dengan configurable grace period
- [ ] **Attendance Analytics**: Dashboard keterlambatan per sesi/peserta
- [ ] **Bulk Session Import**: Upload CSV sesi definitions
- [ ] **Mobile App Integration**: Native app dengan pemilihan sesi

---

**Dokumentasi Versi**: 1.0
**Last Updated**: 2026-07-10
**Kompatibel dengan**: CodeIgniter 4.4+, PHP 8.0+
