classes = $classes; $this->teachers = $teachers; $this->workloads = $workloads; // Initialize empty timetables foreach ($this->classes as $class) { $this->class_timetables[$class['id']] = array_fill(0, count(self::DAYS_OF_WEEK), array_fill(0, self::PERIODS_PER_DAY, null)); } foreach ($this->teachers as $teacher) { $this->teacher_timetables[$teacher['id']] = array_fill(0, count(self::DAYS_OF_WEEK), array_fill(0, self::PERIODS_PER_DAY, null)); } } public function generate() { $this->lessons_to_schedule = $this->prepare_lessons(); $this->solve($this->lessons_to_schedule); // Run the solver return [ // The process always "succeeds" in running. Check unplaced_lessons for timetable completeness. 'success' => true, 'class_timetables' => $this->class_timetables, 'unplaced_lessons' => $this->unplaced_lessons, ]; } private function prepare_lessons() { $lessons = []; $electives = []; // Group electives foreach ($this->workloads as $workload) { if ($workload['elective_group']) { $electives[$workload['elective_group']][] = $workload; } } // Create elective lesson blocks foreach ($electives as $group_name => $group_workloads) { for ($i = 0; $i < $group_workloads[0]['lessons_per_week']; $i++) { $lessons[] = [ 'is_elective' => true, 'group_name' => $group_name, 'workloads' => $group_workloads, 'subject_name' => implode(' / ', array_map(fn($w) => $w['subject_name'], $group_workloads)), 'teacher_name' => 'Elective Group', 'subject_id' => $group_workloads[0]['subject_id'] // For color ]; } } // Create double and single lessons foreach ($this->workloads as $workload) { if ($workload['elective_group']) continue; $lessons_per_week = $workload['lessons_per_week']; if ($workload['has_double_lesson']) { if ($lessons_per_week >= 2) { $lessons[] = array_merge($workload, ['is_double' => true, 'duration' => 2]); $lessons_per_week -= 2; } } for ($i = 0; $i < $lessons_per_week; $i++) { $lessons[] = array_merge($workload, ['is_double' => false, 'duration' => 1]); } } // Sort lessons to prioritize more constrained ones (doubles and electives) usort($lessons, function($a, $b) { $a_score = ($a['is_elective'] ?? false) * 10 + ($a['is_double'] ?? false) * 5; $b_score = ($b['is_elective'] ?? false) * 10 + ($b['is_double'] ?? false) * 5; return $b_score <=> $a_score; }); return $lessons; } private function solve(&$lessons) { if (empty($lessons)) { return true; // Success: all lessons have been scheduled } $lesson = array_pop($lessons); // Get the next lesson to try placing $possible_slots = $this->get_possible_slots($lesson); shuffle($possible_slots); foreach ($possible_slots as $slot) { $this->place_lesson($lesson, $slot); if ($this->solve($lessons)) { return true; // Found a valid solution for the rest of the lessons } // Backtrack $this->unplace_lesson($lesson, $slot); } // If we get here, no slot worked for the current lesson. $this->unplaced_lessons[] = $lesson; // Record as unplaced // BUG FIX: The line below caused an infinite recursion and server crash. // It has been removed. By returning false, we signal the parent to backtrack. return false; } private function get_possible_slots($lesson) { $slots = []; for ($day = 0; $day < count(self::DAYS_OF_WEEK); $day++) { for ($period = 0; $period < self::PERIODS_PER_DAY; $period++) { $slot = ['day' => $day, 'period' => $period]; if ($this->is_slot_valid($lesson, $slot)) { $slots[] = $slot; } } } return $slots; } private function is_slot_valid($lesson, $slot) { $day = $slot['day']; $start_period = $slot['period']; $duration = $lesson['duration'] ?? 1; // Check if slot is a break for ($p = 0; $p < $duration; $p++) { $current_period = $start_period + $p; if ($current_period >= self::PERIODS_PER_DAY || in_array($current_period, self::BREAK_PERIODS)) { return false; // Slot is out of bounds or a break } } if ($lesson['is_elective']) { // Check all classes and teachers in the elective group foreach ($lesson['workloads'] as $workload) { if (!$this->check_resource_availability($workload['class_id'], $workload['teacher_id'], $day, $start_period, $duration)) { return false; } } } else { // Check for single/double lesson if (!$this->check_resource_availability($lesson['class_id'], $lesson['teacher_id'], $day, $start_period, $duration)) { return false; } // ** STRICT DISTRIBUTION RULE ** $lessons_on_day = 0; foreach ($this->class_timetables[$lesson['class_id']][$day] as $p) { if ($p && $p['subject_id'] === $lesson['subject_id']) { $lessons_on_day++; } } $max_per_day = ($lesson['lessons_per_week'] > count(self::DAYS_OF_WEEK)) ? 2 : 1; if ($lessons_on_day >= $max_per_day) { return false; } } return true; } private function check_resource_availability($class_id, $teacher_id, $day, $start_period, $duration) { for ($p = 0; $p < $duration; $p++) { $period = $start_period + $p; if (!empty($this->class_timetables[$class_id][$day][$period]) || !empty($this->teacher_timetables[$teacher_id][$day][$period])) { return false; // Conflict found } } return true; } private function place_lesson($lesson, $slot) { $day = $slot['day']; $start_period = $slot['period']; $duration = $lesson['duration'] ?? 1; $lesson_info = [ 'subject' => $lesson['subject_name'], 'teacher' => $lesson['teacher_name'], 'subject_id' => $lesson['subject_id'], ]; if ($lesson['is_elective']) { foreach ($lesson['workloads'] as $workload) { for ($p = 0; $p < $duration; $p++) { $this->class_timetables[$workload['class_id']][$day][$start_period + $p] = $lesson_info; $this->teacher_timetables[$workload['teacher_id']][$day][$start_period + $p] = $workload['class_id']; } } } else { for ($p = 0; $p < $duration; $p++) { $this->class_timetables[$lesson['class_id']][$day][$start_period + $p] = $lesson_info; $this->teacher_timetables[$lesson['teacher_id']][$day][$start_period + $p] = $lesson['class_id']; } } } private function unplace_lesson($lesson, $slot) { $day = $slot['day']; $start_period = $slot['period']; $duration = $lesson['duration'] ?? 1; if ($lesson['is_elective']) { foreach ($lesson['workloads'] as $workload) { for ($p = 0; $p < $duration; $p++) { $this->class_timetables[$workload['class_id']][$day][$start_period + $p] = null; $this->teacher_timetables[$workload['teacher_id']][$day][$start_period + $p] = null; } } } else { for ($p = 0; $p < $duration; $p++) { $this->class_timetables[$lesson['class_id']][$day][$start_period + $p] = null; $this->teacher_timetables[$lesson['teacher_id']][$day][$start_period + $p] = null; } } } } // --- Data Fetching --- $pdo = db(); $classes = $pdo->query("SELECT * FROM classes ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); $teachers = $pdo->query("SELECT * FROM teachers")->fetchAll(PDO::FETCH_ASSOC); $workloads = $pdo->query(" SELECT w.class_id, c.name as class_name, w.subject_id, s.name as subject_name, s.has_double_lesson, s.elective_group, w.teacher_id, t.name as teacher_name, w.lessons_per_week FROM workloads w JOIN classes c ON w.class_id = c.id JOIN subjects s ON w.subject_id = s.id JOIN teachers t ON w.teacher_id = t.id ")->fetchAll(PDO::FETCH_ASSOC); // --- Run Scheduler --- $scheduler = new Scheduler($classes, $teachers, $workloads); $result = $scheduler->generate(); $class_timetables = $result['class_timetables']; $raw_unplaced_lessons = $result['unplaced_lessons']; // Process unplaced lessons for display $unplaced_lessons_summary = []; foreach($raw_unplaced_lessons as $lesson) { $key = $lesson['is_elective'] ? $lesson['group_name'] : $lesson['class_name'] . '-' . $lesson['subject_name']; if (!isset($unplaced_lessons_summary[$key])) { $unplaced_lessons_summary[$key] = [ 'class' => $lesson['is_elective'] ? 'All Classes' : $lesson['class_name'], 'subject' => $lesson['is_elective'] ? "Elective: " . $lesson['group_name'] : $lesson['subject_name'], 'unplaced' => 0, 'total' => $lesson['is_elective'] ? $lesson['workloads'][0]['lessons_per_week'] : $lesson['lessons_per_week'] ]; } $unplaced_lessons_summary[$key]['unplaced']++; } // --- Color Helper --- $subject_colors = []; $color_palette = ['#FFADAD', '#FFD6A5', '#FDFFB6', '#CAFFBF', '#9BF6FF', '#A0C4FF', '#BDB2FF', '#FFC6FF', '#FFC8DD', '#D4A5A5']; function get_subject_color($subject_name, &$subject_colors, $palette) { if (!isset($subject_colors[$subject_name])) { $subject_colors[$subject_name] = $palette[count($subject_colors) % count($palette)]; } return $subject_colors[$subject_name]; } ?> Generated Timetable - Haki Schedule

Generated Timetable

Automatically generated, color-coded class schedules.

Scheduling Conflict

The system could not place all lessons. This is likely due to too many constraints (e.g., not enough available slots for a teacher). Please review workloads and constraints.

  • Could not place of lessons for in class .
Please create a class and assign workloads to generate a timetable.
$class): ?>

Timetable for

Time
2 ? 1 : 0) - ($period > 5 ? 1 : 0), 9 + $period + ($period > 2 ? 1 : 0) - ($period > 5 ? 1 : 0)); ?>