query(" SELECT w.id, c.id as class_id, c.name as class_name, s.id as subject_id, s.name as subject_name, s.has_double_lesson, s.elective_group, t.id as 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 ORDER BY c.name, s.name "); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function get_classes($pdo) { return $pdo->query("SELECT * FROM classes ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); } function get_timeslots($pdo) { return $pdo->query("SELECT * FROM timeslots ORDER BY start_time")->fetchAll(PDO::FETCH_ASSOC); } // --- Helper Functions --- function get_grade_from_class_name($class_name) { if (preg_match('/^(Grade\s+\d+)/i', $class_name, $matches)) { return $matches[1]; } return $class_name; } // --- Scoring and Placement Logic --- function find_best_slot_for_lesson($lesson, $is_double, &$class_timetables, &$teacher_timetables, $all_class_ids, $days_of_week, $periods_per_day) { $best_slot = null; $highest_score = -1; $class_id = $lesson['type'] === 'horizontal_elective' ? null : $lesson['class_id']; $teachers_in_lesson = array_unique(array_column($lesson['component_lessons'], 'teacher_id')); $class_ids_in_lesson = $lesson['type'] === 'horizontal_elective' ? $lesson['participating_class_ids'] : [$class_id]; for ($day = 0; $day < count($days_of_week); $day++) { for ($period = 0; $period < $periods_per_day; $period++) { $current_score = 100; // Base score // 1. Check basic availability $slot_available = true; if ($is_double) { if ($period + 1 >= $periods_per_day) continue; foreach ($class_ids_in_lesson as $cid) { if (isset($class_timetables[$cid][$day][$period]) || isset($class_timetables[$cid][$day][$period + 1])) { $slot_available = false; break; } } if (!$slot_available) continue; foreach ($teachers_in_lesson as $teacher_id) { if (isset($teacher_timetables[$teacher_id][$day][$period]) || isset($teacher_timetables[$teacher_id][$day][$period + 1])) { $slot_available = false; break; } } } else { foreach ($class_ids_in_lesson as $cid) { if (isset($class_timetables[$cid][$day][$period])) { $slot_available = false; break; } } if (!$slot_available) continue; foreach ($teachers_in_lesson as $teacher_id) { if (isset($teacher_timetables[$teacher_id][$day][$period])) { $slot_available = false; break; } } } if (!$slot_available) continue; // 2. Apply scoring rules foreach ($class_ids_in_lesson as $cid) { for ($p = 0; $p < $periods_per_day; $p++) { if (isset($class_timetables[$cid][$day][$p]) && $class_timetables[$cid][$day][$p]['subject_name'] === $lesson['display_name']) { $current_score -= 50; } } } foreach ($teachers_in_lesson as $teacher_id) { if ($period > 0 && isset($teacher_timetables[$teacher_id][$day][$period - 1])) $current_score -= 15; $after_period = $is_double ? $period + 2 : $period + 1; if ($after_period < $periods_per_day && isset($teacher_timetables[$teacher_id][$day][$after_period])) $current_score -= 15; } if ($is_double) { $subject_name_to_check = $lesson['display_name']; foreach ($all_class_ids as $cid) { if (in_array($cid, $class_ids_in_lesson)) continue; if (isset($class_timetables[$cid][$day][$period]) && $class_timetables[$cid][$day][$period]['is_double'] && $class_timetables[$cid][$day][$period]['subject_name'] === $subject_name_to_check) { $current_score -= 500; break; } } } if ($current_score > $highest_score) { $highest_score = $current_score; $best_slot = ['day' => $day, 'period' => $period]; } } } return $best_slot; } // --- Main Scheduling Engine --- function generate_timetable($workloads, $classes, $days_of_week, $periods_per_day) { $class_timetables = []; foreach ($classes as $class) { $class_timetables[$class['id']] = array_fill(0, count($days_of_week), array_fill(0, $periods_per_day, null)); } $teacher_timetables = []; // --- Lesson Preparation --- $horizontal_elective_doubles = []; $horizontal_elective_singles = []; $other_workloads = []; $workloads_by_grade_and_elective_group = []; foreach ($workloads as $workload) { if (!empty($workload['elective_group'])) { $grade_name = get_grade_from_class_name($workload['class_name']); $workloads_by_grade_and_elective_group[$grade_name][$workload['elective_group']][] = $workload; } else { $other_workloads[] = $workload; } } foreach ($workloads_by_grade_and_elective_group as $grade_name => $elective_groups) { foreach ($elective_groups as $elective_group_name => $group_workloads) { $participating_class_ids = array_unique(array_column($group_workloads, 'class_id')); if (count($participating_class_ids) > 1) { $first = $group_workloads[0]; $block = [ 'type' => 'horizontal_elective', 'display_name' => $elective_group_name, 'participating_class_ids' => $participating_class_ids, 'lessons_per_week' => $first['lessons_per_week'], 'has_double_lesson' => $first['has_double_lesson'], 'component_lessons' => $group_workloads ]; if ($block['has_double_lesson'] && $block['lessons_per_week'] >= 2) { $horizontal_elective_doubles[] = $block; for ($i = 0; $i < $block['lessons_per_week'] - 2; $i++) $horizontal_elective_singles[] = $block; } else { for ($i = 0; $i < $block['lessons_per_week']; $i++) $horizontal_elective_singles[] = $block; } } else { foreach($group_workloads as $workload) $other_workloads[] = $workload; } } } $double_lessons = []; $single_lessons = []; $workloads_by_class = []; foreach ($other_workloads as $workload) { $workloads_by_class[$workload['class_id']][] = $workload; } foreach ($workloads_by_class as $class_id => $class_workloads) { $elective_groups = []; $individual_lessons = []; foreach ($class_workloads as $workload) { if (!empty($workload['elective_group'])) { $elective_groups[$workload['elective_group']][] = $workload; } else { $individual_lessons[] = $workload; } } foreach ($elective_groups as $group_name => $group_workloads) { $first = $group_workloads[0]; $block = [ 'type' => 'elective', 'class_id' => $class_id, 'display_name' => $group_name, 'lessons_per_week' => $first['lessons_per_week'], 'has_double_lesson' => $first['has_double_lesson'], 'component_lessons' => $group_workloads ]; if ($block['has_double_lesson'] && $block['lessons_per_week'] >= 2) { $double_lessons[] = $block; for ($i = 0; $i < $block['lessons_per_week'] - 2; $i++) $single_lessons[] = $block; } else { for ($i = 0; $i < $block['lessons_per_week']; $i++) $single_lessons[] = $block; } } foreach ($individual_lessons as $workload) { $lesson = [ 'type' => 'single', 'class_id' => $workload['class_id'], 'display_name' => $workload['subject_name'], 'lessons_per_week' => $workload['lessons_per_week'], 'has_double_lesson' => $workload['has_double_lesson'], 'component_lessons' => [$workload] ]; if ($lesson['has_double_lesson'] && $lesson['lessons_per_week'] >= 2) { $double_lessons[] = $lesson; for ($i = 0; $i < $lesson['lessons_per_week'] - 2; $i++) $single_lessons[] = $lesson; } else { for ($i = 0; $i < $lesson['lessons_per_week']; $i++) $single_lessons[] = $lesson; } } } // --- Placement using Scoring --- $all_class_ids = array_column($classes, 'id'); $all_lessons_in_order = [ 'horizontal_doubles' => $horizontal_elective_doubles, 'doubles' => $double_lessons, 'horizontal_singles' => $horizontal_elective_singles, 'singles' => $single_lessons ]; foreach ($all_lessons_in_order as $type => $lessons) { shuffle($lessons); foreach ($lessons as $lesson) { $is_double = ($type === 'doubles' || $type === 'horizontal_doubles'); $best_slot = find_best_slot_for_lesson($lesson, $is_double, $class_timetables, $teacher_timetables, $all_class_ids, $days_of_week, $periods_per_day); if ($best_slot) { $day = $best_slot['day']; $period = $best_slot['period']; $class_ids_to_place = ($lesson['type'] === 'horizontal_elective') ? $lesson['participating_class_ids'] : [$lesson['class_id']]; $teachers_to_place = array_unique(array_column($lesson['component_lessons'], 'teacher_id')); $subject_id = null; $teacher_id = null; if ($lesson['type'] === 'single' && count($lesson['component_lessons']) === 1) { $subject_id = $lesson['component_lessons'][0]['subject_id']; $teacher_id = $lesson['component_lessons'][0]['teacher_id']; } $lesson_info = [ 'subject_id' => $subject_id, 'teacher_id' => $teacher_id, 'subject_name' => $lesson['display_name'], 'teacher_name' => count($teachers_to_place) > 1 ? 'Multiple' : $lesson['component_lessons'][0]['teacher_name'], 'is_double' => $is_double, 'is_elective' => $lesson['type'] === 'elective', 'is_horizontal_elective' => $lesson['type'] === 'horizontal_elective' ]; if ($is_double) { foreach ($class_ids_to_place as $cid) { $class_timetables[$cid][$day][$period] = $lesson_info; $class_timetables[$cid][$day][$period + 1] = $lesson_info; } foreach ($teachers_to_place as $tid) { $teacher_timetables[$tid][$day][$period] = true; $teacher_timetables[$tid][$day][$period + 1] = true; } } else { foreach ($class_ids_to_place as $cid) $class_timetables[$cid][$day][$period] = $lesson_info; foreach ($teachers_to_place as $tid) $teacher_timetables[$tid][$day][$period] = true; } } } } return $class_timetables; } // --- Timetable Persistence --- function save_timetable($pdo, $class_timetables, $timeslots) { $pdo->exec('TRUNCATE TABLE schedules'); $stmt = $pdo->prepare( 'INSERT INTO schedules (class_id, day_of_week, timeslot_id, subject_id, teacher_id, lesson_display_name, teacher_display_name, is_double, is_elective, is_horizontal_elective) ' . 'VALUES (:class_id, :day_of_week, :timeslot_id, :subject_id, :teacher_id, :lesson_display_name, :teacher_display_name, :is_double, :is_elective, :is_horizontal_elective)' ); $lesson_periods = array_values(array_filter($timeslots, function($ts) { return !$ts['is_break']; })); foreach ($class_timetables as $class_id => $day_schedule) { foreach ($day_schedule as $day_idx => $period_schedule) { foreach ($period_schedule as $period_idx => $lesson) { if ($lesson) { if (!isset($lesson_periods[$period_idx])) continue; $timeslot_id = $lesson_periods[$period_idx]['id']; $stmt->execute([ ':class_id' => $class_id, ':day_of_week' => $day_idx, ':timeslot_id' => $timeslot_id, ':subject_id' => $lesson['subject_id'], ':teacher_id' => $lesson['teacher_id'], ':lesson_display_name' => $lesson['subject_name'], ':teacher_display_name' => $lesson['teacher_name'], ':is_double' => $lesson['is_double'], ':is_elective' => $lesson['is_elective'], ':is_horizontal_elective' => $lesson['is_horizontal_elective'] ]); } } } } } function get_timetable_from_db($pdo, $classes, $timeslots) { $stmt = $pdo->query('SELECT * FROM schedules ORDER BY id'); $saved_lessons = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($saved_lessons)) return []; $days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; $periods = array_filter($timeslots, function($ts) { return !$ts['is_break']; }); $periods_per_day = count($periods); $class_timetables = []; foreach ($classes as $class) { $class_timetables[$class['id']] = array_fill(0, count($days_of_week), array_fill(0, $periods_per_day, null)); } $lesson_periods = array_values($periods); $timeslot_id_to_period_idx = []; foreach($lesson_periods as $idx => $period) { $timeslot_id_to_period_idx[$period['id']] = $idx; } foreach ($saved_lessons as $lesson) { $class_id = $lesson['class_id']; $day_idx = $lesson['day_of_week']; $timeslot_id = $lesson['timeslot_id']; if (!isset($timeslot_id_to_period_idx[$timeslot_id]) || !isset($class_timetables[$class_id])) continue; $period_idx = $timeslot_id_to_period_idx[$timeslot_id]; $class_timetables[$class_id][$day_idx][$period_idx] = [ 'id' => $lesson['id'], 'subject_id' => $lesson['subject_id'], 'teacher_id' => $lesson['teacher_id'], 'subject_name' => $lesson['lesson_display_name'], 'teacher_name' => $lesson['teacher_display_name'], 'is_double' => (bool)$lesson['is_double'], 'is_elective' => (bool)$lesson['is_elective'], 'is_horizontal_elective' => (bool)$lesson['is_horizontal_elective'] ]; } return $class_timetables; } // --- Main Logic --- $pdoconn = db(); $workloads = get_workloads($pdoconn); $classes = get_classes($pdoconn); $timeslots = get_timeslots($pdoconn); $days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; $periods = array_filter($timeslots, function($timeslot) { return !$timeslot['is_break']; }); $periods_per_day = count($periods); if (isset($_POST['generate'])) { $class_timetables = generate_timetable($workloads, $classes, $days_of_week, $periods_per_day); save_timetable($pdoconn, $class_timetables, $timeslots); } else { $class_timetables = get_timetable_from_db($pdoconn, $classes, $timeslots); } ?>