diff --git a/admin_classes.php b/admin_classes.php index 8aa8578..0ce16bc 100644 --- a/admin_classes.php +++ b/admin_classes.php @@ -17,7 +17,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_id'])) { $stmt->execute([$delete_id, $school_id]); $message = "Class deleted successfully."; } catch (PDOException $e) { - $error = "Error deleting class: " . $e->getMessage(); + if ($e->getCode() == '23000') { // Integrity constraint violation + $error = "Cannot delete this class because it has subjects, workloads, or schedules associated with it. Please remove those associations before deleting."; + } else { + $error = "Error deleting class: " . $e->getMessage(); + } } } diff --git a/admin_elective_groups.php b/admin_elective_groups.php index cdbde86..c9d5144 100644 --- a/admin_elective_groups.php +++ b/admin_elective_groups.php @@ -17,7 +17,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_id'])) { $stmt->execute([$delete_id, $school_id]); $message = "Elective group deleted successfully."; } catch (PDOException $e) { - $error = "Error deleting group: " . $e->getMessage(); + if ($e->getCode() == '23000') { // Integrity constraint violation + $error = "Cannot delete this elective group because it has subjects associated with it. Please remove those associations before deleting."; + } else { + $error = "Error deleting group: " . $e->getMessage(); + } } } diff --git a/admin_subjects.php b/admin_subjects.php index aa35195..662da2a 100644 --- a/admin_subjects.php +++ b/admin_subjects.php @@ -17,7 +17,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_id'])) { $stmt->execute([$delete_id, $school_id]); $message = "Subject deleted successfully."; } catch (PDOException $e) { - $error = "Error deleting subject: " . $e->getMessage(); + if ($e->getCode() == '23000') { // Integrity constraint violation + $error = "Cannot delete this subject because it is currently used in workloads or schedules. Please remove those associations before deleting."; + } else { + $error = "Error deleting subject: " . $e->getMessage(); + } } } diff --git a/admin_teachers.php b/admin_teachers.php index c55fb63..3f22fba 100644 --- a/admin_teachers.php +++ b/admin_teachers.php @@ -29,7 +29,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_id'])) { $message = "Teacher deleted successfully."; } catch (PDOException $e) { $pdo->rollBack(); - $error = "Error deleting teacher: " . $e->getMessage(); + if ($e->getCode() == '23000') { // Integrity constraint violation + $error = "Cannot delete this teacher because they are assigned to workloads or schedules. Please remove those associations before deleting."; + } else { + $error = "Error deleting teacher: " . $e->getMessage(); + } } } diff --git a/admin_timeslots.php b/admin_timeslots.php index 5e5673c..82d716b 100644 --- a/admin_timeslots.php +++ b/admin_timeslots.php @@ -32,7 +32,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $error = 'Failed to delete timeslot.'; } } catch (PDOException $e) { - $error = 'Database error: ' . $e->getMessage(); + if ($e->getCode() == '23000') { // Integrity constraint violation + $error = "Cannot delete this timeslot because it is currently used in schedules. Please remove those associations before deleting."; + } else { + $error = "Error deleting timeslot: " . $e->getMessage(); + } } } if (isset($_POST['add_timeslot'])) { diff --git a/db/migrations/020_create_elective_group_subjects_table.sql b/db/migrations/020_create_elective_group_subjects_table.sql new file mode 100644 index 0000000..acecdbb --- /dev/null +++ b/db/migrations/020_create_elective_group_subjects_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS `elective_group_subjects` ( + `elective_group_id` INT NOT NULL, + `subject_id` INT NOT NULL, + PRIMARY KEY (`elective_group_id`, `subject_id`), + FOREIGN KEY (`elective_group_id`) REFERENCES `elective_groups`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/db/migrations/021_create_elective_group_subjects_table.sql b/db/migrations/021_create_elective_group_subjects_table.sql new file mode 100644 index 0000000..df415f9 --- /dev/null +++ b/db/migrations/021_create_elective_group_subjects_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS elective_group_subjects ( + id INT AUTO_INCREMENT PRIMARY KEY, + elective_group_id INT NOT NULL, + subject_id INT NOT NULL, + FOREIGN KEY (elective_group_id) REFERENCES elective_groups(id) ON DELETE CASCADE, + FOREIGN KEY (subject_id) REFERENCES subjects(id) ON DELETE CASCADE, + UNIQUE KEY (elective_group_id, subject_id) +); diff --git a/includes/navbar.php b/includes/navbar.php new file mode 100644 index 0000000..842197e --- /dev/null +++ b/includes/navbar.php @@ -0,0 +1,42 @@ + + + + Haki Schedule + + + + + + Home + + + + + Manage + + + Classes + Subjects + Teachers + Workloads + Timeslots + Elective Groups + + + Class Timetable + + Teacher Timetable + Logout + + Demo + Login + Register + + + + + \ No newline at end of file diff --git a/teacher_timetable.php b/teacher_timetable.php index 07a7d9e..6007ac7 100644 --- a/teacher_timetable.php +++ b/teacher_timetable.php @@ -85,9 +85,9 @@ foreach ($timeslots as $timeslot) { } foreach ($teacher_schedule_raw as $lesson) { - $day_idx = $lesson['day_of_week']; + $day_idx = $lesson['day_of_week'] - 1; // Adjust for 0-based array index $timeslot_id = $lesson['timeslot_id']; - if (isset($teacher_timetable[$day_idx][$timeslot_id])) { + if (isset($teacher_timetable[$day_idx]) && isset($teacher_timetable[$day_idx][$timeslot_id])) { $teacher_timetable[$day_idx][$timeslot_id] = $lesson; } } @@ -181,7 +181,7 @@ foreach ($teacher_schedule_raw as $lesson) { -- Select a Teacher -- - > + > diff --git a/timetable.php b/timetable.php index 363384e..654b302 100644 --- a/timetable.php +++ b/timetable.php @@ -4,370 +4,258 @@ require_once 'includes/auth_check.php'; require_once 'db/config.php'; // --- Database Fetch --- -function get_workloads($pdo) { - $stmt = $pdo->query(" +function get_all_data($pdo) { + $data = []; + $data['classes'] = $pdo->query("SELECT * FROM classes ORDER BY name")->fetchAll(PDO::FETCH_ASSOC); + $data['subjects'] = $pdo->query("SELECT id, name FROM subjects")->fetchAll(PDO::FETCH_KEY_PAIR); + $data['teachers'] = $pdo->query("SELECT id, name FROM teachers")->fetchAll(PDO::FETCH_KEY_PAIR); + $data['timeslots'] = $pdo->query("SELECT * FROM timeslots ORDER BY start_time")->fetchAll(PDO::FETCH_ASSOC); + + $workloads_stmt = $pdo->query(" SELECT - w.id, - c.id as class_id, + w.class_id, w.subject_id, w.teacher_id, w.lessons_per_week, + s.name as subject_name, s.has_double_lesson, s.elective_group_id, c.name as class_name, - s.id as subject_id, - s.name as subject_name, - s.has_double_lesson, - s.elective_group_id, - eg.name as elective_group_name, - t.id as teacher_id, t.name as teacher_name, - w.lessons_per_week + eg.name as elective_group_name FROM workloads w - JOIN classes c ON w.class_id = c.id JOIN subjects s ON w.subject_id = s.id + JOIN classes c ON w.class_id = c.id JOIN teachers t ON w.teacher_id = t.id LEFT JOIN elective_groups eg ON s.elective_group_id = eg.id - ORDER BY c.name, s.name "); - return $stmt->fetchAll(PDO::FETCH_ASSOC); + $data['workloads'] = $workloads_stmt->fetchAll(PDO::FETCH_ASSOC); + + return $data; } -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 = -1000; - - $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; // Not enough space for a double - 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 - // A. Penalize placing the same subject on the same day - foreach ($class_ids_in_lesson as $cid) { - $lessons_on_day = 0; - 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']) { - $lessons_on_day++; - } - } - $current_score -= $lessons_on_day * 50; // Heavy penalty for each existing lesson of same subject - } - - // B. Penalize creating gaps for teachers and classes - foreach ($teachers_in_lesson as $teacher_id) { - // Check for gap before the lesson - if ($period > 0 && !isset($teacher_timetables[$teacher_id][$day][$period - 1])) { - if (isset($teacher_timetables[$teacher_id][$day][$period - 2])) $current_score -= 25; // Gap of 1 - } - // Check for gap after the lesson - $after_period = $is_double ? $period + 2 : $period + 1; - if ($after_period < $periods_per_day && !isset($teacher_timetables[$teacher_id][$day][$after_period])) { - if (isset($teacher_timetables[$teacher_id][$day][$after_period + 1])) $current_score -= 25; // Gap of 1 - } - } - foreach ($class_ids_in_lesson as $cid) { - if ($period > 0 && !isset($class_timetables[$cid][$day][$period - 1])) { - if (isset($class_timetables[$cid][$day][$period - 2])) $current_score -= 10; - } - $after_period = $is_double ? $period + 2 : $period + 1; - if ($after_period < $periods_per_day && !isset($class_timetables[$cid][$day][$after_period])) { - if (isset($class_timetables[$cid][$day][$after_period + 1])) $current_score -= 10; - } - } - - // C. Penalize placing a double lesson of the same subject in parallel with another class - 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; // Don't check against itself - 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 -= 200; // Very high penalty for parallel doubles of same subject - break; - } - } - } - - // D. Prefer ends of day for double lessons - if ($is_double) { - if ($period == 0 || $period + 1 == $periods_per_day -1) { - $current_score += 10; - } - } - - 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) { +function generate_timetable($data, $days_of_week) { + $periods = array_values(array_filter($data['timeslots'], function($ts) { return !$ts['is_break']; })); + $periods_per_day = count($periods); + + // 1. Initialize Timetables $class_timetables = []; - foreach ($classes as $class) { + $teacher_timetables = []; + foreach ($data['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 --- - $all_lessons = []; - $workloads_by_grade_and_elective_group = []; - - // 1. Group horizontal electives - foreach ($workloads as $workload) { - if (!empty($workload['elective_group_id'])) { - $grade_name = get_grade_from_class_name($workload['class_name']); - $workloads_by_grade_and_elective_group[$grade_name][$workload['elective_group_id']][] = $workload; - } else { - // This will be handled in the next step - } + foreach ($data['teachers'] as $teacher_id => $teacher_name) { + $teacher_timetables[$teacher_id] = array_fill(0, count($days_of_week), array_fill(0, $periods_per_day, null)); } - $processed_workload_ids = []; - foreach ($workloads_by_grade_and_elective_group as $grade_name => $elective_groups) { - foreach ($elective_groups as $elective_group_id => $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' => $first['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, - 'priority' => 4 // Highest priority + // 2. Prepare Lessons + $lessons_to_schedule = []; + $electives_by_group_grade = []; + + foreach ($data['workloads'] as $workload) { + if (empty($workload['elective_group_id'])) { // Regular lesson + for ($i = 0; $i < $workload['lessons_per_week']; $i++) { + $is_double = ($i == 0 && $workload['has_double_lesson'] && $workload['lessons_per_week'] >= 2); + $lessons_to_schedule[] = [ + 'type' => 'single', + 'class_id' => $workload['class_id'], + 'subject_id' => $workload['subject_id'], + 'teacher_ids' => [$workload['teacher_id']], + 'display_name' => $workload['subject_name'], + 'teacher_name' => $workload['teacher_name'], + 'is_double' => $is_double, + 'is_elective' => false ]; - $all_lessons[] = $block; - foreach($group_workloads as $w) $processed_workload_ids[] = $w['id']; + if ($is_double) $i++; } + } else { // Elective lesson part + $grade = preg_replace('/[^0-9]/', '', $workload['class_name']); + $key = $workload['elective_group_id'] . '_grade_' . $grade; + if (!isset($electives_by_group_grade[$key])) { + $electives_by_group_grade[$key] = [ + 'type' => 'elective_group', + 'display_name' => $workload['elective_group_name'] . " (Form " . $grade . ")", + 'lessons_per_week' => $workload['lessons_per_week'], + 'has_double_lesson' => $workload['has_double_lesson'], + 'is_elective' => true, + 'component_lessons' => [] + ]; + } + $electives_by_group_grade[$key]['component_lessons'][] = $workload; } } - // 2. Group remaining lessons (single, elective, and normal doubles) - $remaining_workloads = array_filter($workloads, function($w) use ($processed_workload_ids) { - return !in_array($w['id'], $processed_workload_ids); + foreach ($electives_by_group_grade as $group) { + for ($i = 0; $i < $group['lessons_per_week']; $i++) { + $is_double = ($i == 0 && $group['has_double_lesson'] && $group['lessons_per_week'] >= 2); + $class_ids = array_unique(array_column($group['component_lessons'], 'class_id')); + $teacher_ids = array_unique(array_column($group['component_lessons'], 'teacher_id')); + + $lessons_to_schedule[] = [ + 'type' => 'elective_group', + 'class_id' => $class_ids, // Now an array of classes + 'subject_id' => null, // Grouped subject + 'teacher_ids' => $teacher_ids, + 'display_name' => $group['display_name'], + 'is_double' => $is_double, + 'is_elective' => true, + 'component_lessons' => $group['component_lessons'] + ]; + if ($is_double) $i++; + } + } + + // 3. Sort lessons (place doubles and electives first) + usort($lessons_to_schedule, function($a, $b) { + if ($b['is_double'] != $a['is_double']) return $b['is_double'] <=> $a['is_double']; + $a_count = is_array($a['class_id']) ? count($a['class_id']) : 1; + $b_count = is_array($b['class_id']) ? count($b['class_id']) : 1; + return $b_count <=> $a_count; }); - $workloads_by_class = []; - foreach ($remaining_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_id'])) { - $elective_groups[$workload['elective_group_id']][] = $workload; - } else { - $individual_lessons[] = $workload; - } - } - - foreach ($elective_groups as $group_id => $group_workloads) { - $first = $group_workloads[0]; - $all_lessons[] = [ - 'type' => 'elective', 'class_id' => $class_id, 'display_name' => $first['elective_group_name'], - 'lessons_per_week' => $first['lessons_per_week'], 'has_double_lesson' => $first['has_double_lesson'], - 'component_lessons' => $group_workloads, - 'priority' => 3 - ]; - } - - foreach ($individual_lessons as $workload) { - $all_lessons[] = [ - '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], - 'priority' => $workload['has_double_lesson'] ? 2 : 1 - ]; - } - } - - // 3. Explode lessons into single and double instances - $final_lesson_list = []; - foreach ($all_lessons as $lesson_block) { - $num_doubles = 0; - $num_singles = $lesson_block['lessons_per_week']; - if ($lesson_block['has_double_lesson'] && $lesson_block['lessons_per_week'] >= 2) { - $num_doubles = 1; - $num_singles -= 2; - } - for ($i=0; $i < $num_doubles; $i++) { - $final_lesson_list[] = ['is_double' => true, 'lesson_details' => $lesson_block]; - } - for ($i=0; $i < $num_singles; $i++) { - $final_lesson_list[] = ['is_double' => false, 'lesson_details' => $lesson_block]; - } - } - - // 4. Sort by priority (desc) and then shuffle to vary timetable - usort($final_lesson_list, function($a, $b) { - $prio_a = $a['lesson_details']['priority']; - $prio_b = $b['lesson_details']['priority']; - if ($prio_a != $prio_b) return $prio_b - $prio_a; - if ($a['is_double'] != $b['is_double']) return $b['is_double'] - $a['is_double']; - return rand(-1, 1); - }); - - // --- Placement using Scoring --- - $all_class_ids = array_column($classes, 'id'); - - foreach ($final_lesson_list as $lesson_item) { - $lesson = $lesson_item['lesson_details']; - $is_double = $lesson_item['is_double']; - - $best_slot = find_best_slot_for_lesson($lesson, $is_double, $class_timetables, $teacher_timetables, $all_class_ids, $days_of_week, $periods_per_day); + // 4. Placement + foreach ($lessons_to_schedule as $lesson) { + $best_slot = find_best_slot_for_lesson($lesson, $class_timetables, $teacher_timetables, $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')); + + if ($lesson['type'] === 'single') { + $class_id = $lesson['class_id']; + $teacher_id = $lesson['teacher_ids'][0]; + + $lesson_data = [ + 'subject_name' => $lesson['display_name'], + 'teacher_name' => $lesson['teacher_name'], + 'subject_id' => $lesson['subject_id'], + 'teacher_ids' => $lesson['teacher_ids'], + 'is_double' => $lesson['is_double'], + 'is_elective' => false, + ]; - $subject_id = null; - $teacher_id = null; - // For single-teacher, single-subject lessons, store the IDs directly - if (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', - 'component_lessons' => $lesson['component_lessons'] - ]; - - 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; + $class_timetables[$class_id][$day][$period] = $lesson_data; + $teacher_timetables[$teacher_id][$day][$period] = true; + if ($lesson['is_double']) { + $class_timetables[$class_id][$day][$period + 1] = $lesson_data; + $teacher_timetables[$teacher_id][$day][$period + 1] = true; } - foreach ($teachers_to_place as $tid) { - $teacher_timetables[$tid][$day][$period] = true; - $teacher_timetables[$tid][$day][$period + 1] = true; + } else { // Elective Group + foreach($lesson['component_lessons'] as $comp_lesson) { + $class_id = $comp_lesson['class_id']; + $teacher_id = $comp_lesson['teacher_id']; + + $lesson_data = [ + 'subject_name' => $comp_lesson['subject_name'], + 'teacher_name' => $comp_lesson['teacher_name'], + 'subject_id' => $comp_lesson['subject_id'], + 'teacher_ids' => [$teacher_id], // Specific teacher for this part + 'is_double' => $lesson['is_double'], + 'is_elective' => true, + 'group_name' => $lesson['display_name'] + ]; + + $class_timetables[$class_id][$day][$period] = $lesson_data; + $teacher_timetables[$teacher_id][$day][$period] = true; + if ($lesson['is_double']) { + $class_timetables[$class_id][$day][$period + 1] = $lesson_data; + $teacher_timetables[$teacher_id][$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; } +function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timetables, $days_of_week, $periods_per_day) { + $is_double = $lesson['is_double']; + $class_ids = is_array($lesson['class_id']) ? $lesson['class_id'] : [$lesson['class_id']]; + $teacher_ids = $lesson['teacher_ids']; + + for ($day = 0; $day < count($days_of_week); $day++) { + for ($period = 0; $period < $periods_per_day; $period++) { + if ($is_double && $period + 1 >= $periods_per_day) continue; + + // Check availability for all classes and teachers + $slot_available = true; + foreach ($class_ids as $cid) { + if ($class_timetables[$cid][$day][$period] !== null || ($is_double && $class_timetables[$cid][$day][$period + 1] !== null)) { + $slot_available = false; break; + } + } + if (!$slot_available) continue; + + foreach ($teacher_ids as $tid) { + if ($teacher_timetables[$tid][$day][$period] !== null || ($is_double && $teacher_timetables[$tid][$day][$period + 1] !== null)) { + $slot_available = false; break; + } + } + + if ($slot_available) { + return ['day' => $day, 'period' => $period]; // Return first available slot (simplistic) + } + } + } + return null; // No slot found +} + + // --- Timetable Persistence --- function save_timetable($pdo, $class_timetables, $timeslots) { - $pdo->exec('TRUNCATE TABLE schedules'); - $pdo->exec('TRUNCATE TABLE schedule_teachers'); + try { + $pdo->beginTransaction(); + $pdo->exec('SET FOREIGN_KEY_CHECKS=0'); + $pdo->exec('TRUNCATE TABLE schedule_teachers'); + $pdo->exec('TRUNCATE TABLE schedules'); + $pdo->exec('SET FOREIGN_KEY_CHECKS=1'); - $stmt = $pdo->prepare( - 'INSERT INTO schedules (class_id, day_of_week, timeslot_id, subject_id, lesson_display_name, teacher_display_name, is_double, is_elective, is_horizontal_elective) ' . - 'VALUES (:class_id, :day_of_week, :timeslot_id, :subject_id, :lesson_display_name, :teacher_display_name, :is_double, :is_elective, :is_horizontal_elective)' - ); - $teacher_stmt = $pdo->prepare( - 'INSERT INTO schedule_teachers (schedule_id, teacher_id) VALUES (:schedule_id, :teacher_id)' - ); + $stmt = $pdo->prepare( + 'INSERT INTO schedules (class_id, day_of_week, timeslot_id, subject_id, lesson_display_name, teacher_display_name, is_double, is_elective) ' . + 'VALUES (:class_id, :day_of_week, :timeslot_id, :subject_id, :lesson_display_name, :teacher_display_name, :is_double, :is_elective)' + ); + $teacher_stmt = $pdo->prepare( + 'INSERT INTO schedule_teachers (schedule_id, teacher_id) VALUES (:schedule_id, :teacher_id)' + ); - $lesson_periods = array_values(array_filter($timeslots, function($ts) { return !$ts['is_break']; })); + $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) { - $processed_periods = []; - foreach ($period_schedule as $period_idx => $lesson) { - if ($lesson && !in_array($period_idx, $processed_periods)) { - if (!isset($lesson_periods[$period_idx])) continue; - $timeslot_id = $lesson_periods[$period_idx]['id']; + foreach ($class_timetables as $class_id => $day_schedule) { + foreach ($day_schedule as $day_idx => $period_schedule) { + $processed_periods = []; + foreach ($period_schedule as $period_idx => $lesson) { + if ($lesson && !in_array($period_idx, $processed_periods)) { + $timeslot_id = $lesson_periods[$period_idx]['id']; + + $display_name = $lesson['is_elective'] ? ($lesson['group_name'] . ' / ' . $lesson['subject_name']) : $lesson['subject_name']; - $stmt->execute([ - ':class_id' => $class_id, - ':day_of_week' => $day_idx, - ':timeslot_id' => $timeslot_id, - ':subject_id' => $lesson['subject_id'], - ':lesson_display_name' => $lesson['subject_name'], - ':teacher_display_name' => $lesson['teacher_name'], - ':is_double' => (int)$lesson['is_double'], - ':is_elective' => (int)$lesson['is_elective'], - ':is_horizontal_elective' => (int)$lesson['is_horizontal_elective'] - ]); + $stmt->execute([ + ':class_id' => $class_id, + ':day_of_week' => $day_idx, + ':timeslot_id' => $timeslot_id, + ':subject_id' => $lesson['subject_id'], + ':lesson_display_name' => $display_name, + ':teacher_display_name' => $lesson['teacher_name'], + ':is_double' => (int)$lesson['is_double'], + ':is_elective' => (int)$lesson['is_elective'] + ]); - $schedule_id = $pdo->lastInsertId(); + $schedule_id = $pdo->lastInsertId(); - if (!empty($lesson['component_lessons'])) { - $teacher_ids = array_unique(array_column($lesson['component_lessons'], 'teacher_id')); - foreach ($teacher_ids as $teacher_id) { - if ($teacher_id) { - $teacher_stmt->execute([ - ':schedule_id' => $schedule_id, - ':teacher_id' => $teacher_id - ]); - } + foreach ($lesson['teacher_ids'] as $teacher_id) { + $teacher_stmt->execute([':schedule_id' => $schedule_id, ':teacher_id' => $teacher_id]); } - } - $processed_periods[] = $period_idx; - if ($lesson['is_double']) { - $processed_periods[] = $period_idx + 1; + $processed_periods[] = $period_idx; + if ($lesson['is_double']) { + $processed_periods[] = $period_idx + 1; + } } } } } + $pdo->commit(); + } catch (Exception $e) { + $pdo->rollBack(); + error_log("Timetable save failed: " . $e->getMessage()); } } @@ -376,13 +264,8 @@ function get_timetable_from_db($pdo, $classes, $timeslots) { $saved_lessons = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($saved_lessons)) return []; - // Get teacher associations - $schedule_teachers_stmt = $pdo->query('SELECT st.schedule_id, t.name, t.id FROM schedule_teachers st JOIN teachers t ON st.teacher_id = t.id'); - $schedule_teachers = $schedule_teachers_stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC); - - $days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; - $periods = array_filter($timeslots, function($ts) { return !$ts['is_break']; }); + $periods = array_values(array_filter($timeslots, function($ts) { return !$ts['is_break']; })); $periods_per_day = count($periods); $class_timetables = []; @@ -390,9 +273,8 @@ function get_timetable_from_db($pdo, $classes, $timeslots) { $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) { + foreach($periods as $idx => $period) { $timeslot_id_to_period_idx[$period['id']] = $idx; } @@ -402,25 +284,15 @@ function get_timetable_from_db($pdo, $classes, $timeslots) { $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]; - $teachers_for_lesson = $schedule_teachers[$lesson['id']] ?? []; - $teacher_name = $lesson['teacher_display_name']; - if (count($teachers_for_lesson) > 1) { - $teacher_name = 'Multiple'; - } elseif (count($teachers_for_lesson) === 1) { - $teacher_name = $teachers_for_lesson[0]['name']; - } - - $lesson_data = [ 'id' => $lesson['id'], - 'subject_id' => $lesson['subject_id'], 'subject_name' => $lesson['lesson_display_name'], - 'teacher_name' => $teacher_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'] ]; $class_timetables[$class_id][$day_idx][$period_idx] = $lesson_data; @@ -433,17 +305,19 @@ function get_timetable_from_db($pdo, $classes, $timeslots) { // --- Main Logic --- $pdoconn = db(); -$workloads = get_workloads($pdoconn); -$classes = get_classes($pdoconn); -$timeslots = get_timeslots($pdoconn); +$all_data = get_all_data($pdoconn); +$classes = $all_data['classes']; +$timeslots = $all_data['timeslots']; +$workloads = $all_data['workloads']; $days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; -$periods = array_filter($timeslots, function($timeslot) { return !$timeslot['is_break']; }); -$periods_per_day = count($periods); +$class_timetables = []; if (isset($_POST['generate'])) { - $class_timetables = generate_timetable($workloads, $classes, $days_of_week, $periods_per_day); - save_timetable($pdoconn, $class_timetables, $timeslots); + if (!empty($workloads)) { + $class_timetables = generate_timetable($all_data, $days_of_week); + save_timetable($pdoconn, $class_timetables, $timeslots); + } } else { $class_timetables = get_timetable_from_db($pdoconn, $classes, $timeslots); } @@ -456,81 +330,34 @@ if (isset($_POST['generate'])) { Timetable - Haki Schedule - - - - - - Haki Schedule - - - - - - Home - - - - Manage - - - Classes - Subjects - Teachers - Workloads - Timeslots - - - Class Timetable - Teacher Timetable - Logout - - Demo - Login - Register - - - - - + Class Timetable - Generate Timetable + >Generate Timetable Print - Download as PDF - - + + @@ -540,35 +367,34 @@ if (isset($_POST['generate'])) { Time - + - - - + Break - - - - + + + - + @@ -585,11 +411,11 @@ if (isset($_POST['generate'])) { - + - - No workloads found. Please add classes, subjects, teachers, and workloads in the "Manage" section first. - + + No workloads found. Please add classes, subjects, teachers, and workloads in the "Manage" section first. The "Generate Timetable" button is disabled. + Click the "Generate Timetable" button to create a schedule. @@ -601,90 +427,9 @@ if (isset($_POST['generate'])) {