loadGuruList(); $kelasList = $this->loadKelasList($semester, $tahunAjaran); $ruanganRobotkuIds = $this->loadRuanganRobotku(); $allSlots = $this->buildAllSlots(); $dataset = new SchedulingDataset( guruList: $guruList, kelasList: $kelasList, ruanganRobotkuIds: $ruanganRobotkuIds, allSlots: $allSlots, ); $this->validate($dataset); return $dataset; } /** * Load semua guru yang aktif mengajar & punya level_maksimal. * * @return array Map guru_id => Guru */ private function loadGuruList(): array { // Ambil semua guru aktif yang punya level_maksimal terisi. // Guru administratif tanpa level_maksimal otomatis dikecualikan. $guruRows = DB::table('guru') ->select('id', 'nama', 'level_maksimal') ->where('status', 'Aktif') ->whereNotNull('level_maksimal') ->orderBy('id') ->get(); // Ambil semua ketersediaan sekaligus (satu query), lalu di-index per guru_id. $guruIds = $guruRows->pluck('id')->toArray(); $ketersediaanByGuru = $this->loadKetersediaanByGuru($guruIds); $result = []; foreach ($guruRows as $row) { $result[$row->id] = new Guru( id: (int) $row->id, nama: (string) $row->nama, levelMaksimal: (int) $row->level_maksimal, ketersediaan: $ketersediaanByGuru[$row->id] ?? [], ); } return $result; } /** * Load ketersediaan_mengajars dan grup berdasarkan guru_id. * * @param array $guruIds * @return array> Map guru_id => (map slot_key => true) */ private function loadKetersediaanByGuru(array $guruIds): array { if (empty($guruIds)) { return []; } // sesi_id di tabel ketersediaan mengacu ke tabel sesi (id 1-5). // Kita asumsikan sesi.id === nomor sesi (dikonfirmasi user). $rows = DB::table('ketersediaan_mengajars') ->select('guru_id', 'hari', 'sesi_id') ->whereIn('guru_id', $guruIds) ->get(); $result = []; foreach ($rows as $row) { $slotKey = "{$row->hari}-{$row->sesi_id}"; $result[$row->guru_id][$slotKey] = true; } return $result; } /** * Load semua kelas aktif untuk semester tertentu. * * @return array Map kelas_id => Kelas */ private function loadKelasList(string $semester, string $tahunAjaran): array { // Join dengan programs untuk ambil level_number kelas. $rows = DB::table('kelas') ->join('programs', 'kelas.program_id', '=', 'programs.id') ->select( 'kelas.id', 'kelas.nama_kelas', 'programs.level_number', 'kelas.frekuensi_per_minggu', 'kelas.is_ekstrakurikuler', 'kelas.hari_fix', 'kelas.sesi_fix', 'kelas.ruangan_fix_id', ) ->where('kelas.semester', $semester) ->where('kelas.tahun_ajaran', $tahunAjaran) ->where('kelas.status', 'Aktif') ->orderBy('kelas.id') ->get(); $result = []; foreach ($rows as $row) { // level_number bisa null jika data programs belum lengkap if ($row->level_number === null) { throw new \RuntimeException( "Kelas '{$row->nama_kelas}' (id={$row->id}) merujuk ke program tanpa level_number. " . "Isi level_number di tabel programs terlebih dahulu." ); } $result[$row->id] = new Kelas( id: (int) $row->id, nama: (string) $row->nama_kelas, level: (int) $row->level_number, frekuensiPerMinggu: (int) $row->frekuensi_per_minggu, isEkstrakurikuler: (bool) $row->is_ekstrakurikuler, hariFix: $row->hari_fix !== null ? (int) $row->hari_fix : null, sesiFix: $row->sesi_fix !== null ? (int) $row->sesi_fix : null, ruanganFixId: $row->ruangan_fix_id !== null ? (int) $row->ruangan_fix_id : null, ); } return $result; } /** * Load 5 ruangan Robotku (lokasi_tipe='robotku'). * Ruangan sekolah tidak masuk pool ini karena tidak dipakai kelas reguler. * * @return array Array id ruangan */ private function loadRuanganRobotku(): array { return DB::table('ruangan') ->where('lokasi_tipe', 'robotku') ->where('status', 'aktif') ->orderBy('id') ->pluck('id') ->map(fn($id) => (int) $id) ->toArray(); } /** * Generate semua slot valid: 6 hari × 5 sesi = 30 slot. * * @return array */ private function buildAllSlots(): array { $slots = []; foreach (self::HARI_LIST as $hari) { foreach (self::SESI_LIST as $sesi) { $slots[] = new SchedulingSlot($hari, $sesi); } } return $slots; } /** * Validasi dataset: pastikan tidak ada kondisi mustahil sebelum GA jalan. * Kalau ada masalah, throw exception dengan pesan jelas. */ private function validate(SchedulingDataset $dataset): void { // 1. Minimal harus ada guru if (empty($dataset->guruList)) { throw new \RuntimeException( "Tidak ada guru dengan status='Aktif' dan level_maksimal terisi. " . "Sistem tidak bisa menjadwalkan." ); } // 2. Minimal harus ada kelas if (empty($dataset->kelasList)) { throw new \RuntimeException( "Tidak ada kelas aktif untuk semester ini. " . "Cek filter semester & tahun_ajaran." ); } // 3. Minimal harus ada ruangan Robotku if (empty($dataset->ruanganRobotkuIds)) { throw new \RuntimeException( "Tidak ada ruangan Robotku (lokasi_tipe='robotku') yang aktif." ); } // 4. Untuk tiap kelas ekstrakurikuler, ruangan_fix_id harus valid // (ruangan tersebut harus benar-benar ada di tabel ruangan) $ruanganIdsEks = collect($dataset->kelasEkstrakurikuler()) ->pluck('ruanganFixId') ->unique() ->toArray(); if (!empty($ruanganIdsEks)) { $existingRuangan = DB::table('ruangan') ->whereIn('id', $ruanganIdsEks) ->pluck('id') ->toArray(); $missing = array_diff($ruanganIdsEks, $existingRuangan); if (!empty($missing)) { throw new \RuntimeException( "Kelas ekstrakurikuler merujuk ke ruangan_fix_id yang tidak ada di database: " . implode(', ', $missing) ); } } // 5. Untuk tiap level yang butuh guru, minimal ada 1 guru yang mampu. // (Feasibility check paling dasar untuk H4) $levelKelas = collect($dataset->kelasList)->pluck('level')->unique(); foreach ($levelKelas as $level) { $guruMampu = $dataset->guruMampuLevel($level); if (empty($guruMampu)) { throw new \RuntimeException( "Tidak ada guru dengan level_maksimal >= {$level}. " . "Kelas level {$level} tidak dapat dijadwalkan." ); } } } }