better now to fix jondiko timetable
This commit is contained in:
parent
b40170f220
commit
3c2845cfdf
174
timetable.php
174
timetable.php
@ -32,10 +32,10 @@ function get_all_data($pdo) {
|
||||
|
||||
// --- Main Scheduling Engine ---
|
||||
function generate_timetable($data, $days_of_week) {
|
||||
error_log("generate_timetable: Starting generation...");
|
||||
|
||||
$periods = array_values(array_filter($data['timeslots'], function($ts) { return !$ts['is_break']; }));
|
||||
$periods_per_day = count($periods);
|
||||
error_log("generate_timetable: Periods per day: $periods_per_day");
|
||||
|
||||
|
||||
// 1. Initialize Timetables
|
||||
$class_timetables = [];
|
||||
@ -46,29 +46,15 @@ function generate_timetable($data, $days_of_week) {
|
||||
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));
|
||||
}
|
||||
error_log("generate_timetable: Initialized " . count($class_timetables) . " class timetables and " . count($teacher_timetables) . " teacher timetables.");
|
||||
|
||||
|
||||
// 2. Prepare Lessons
|
||||
$lessons_to_schedule = [];
|
||||
$electives_by_group_grade = [];
|
||||
|
||||
// First, group elective workloads
|
||||
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
|
||||
];
|
||||
if ($is_double) $i++;
|
||||
}
|
||||
} else { // Elective lesson part
|
||||
if (!empty($workload['elective_group_id'])) {
|
||||
$grade = preg_replace('/[^0-9]/', '', $workload['class_name']);
|
||||
$key = $workload['elective_group_id'] . '_grade_' . $grade;
|
||||
if (!isset($electives_by_group_grade[$key])) {
|
||||
@ -85,27 +71,65 @@ function generate_timetable($data, $days_of_week) {
|
||||
}
|
||||
}
|
||||
|
||||
// Process regular lessons
|
||||
foreach ($data['workloads'] as $workload) {
|
||||
if (empty($workload['elective_group_id'])) { // Regular lesson
|
||||
$lessons_to_create = (int)$workload['lessons_per_week'];
|
||||
$has_double = (bool)$workload['has_double_lesson'];
|
||||
$num_singles = $lessons_to_create;
|
||||
|
||||
if ($has_double && $lessons_to_create >= 2) {
|
||||
// Create one double lesson
|
||||
$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' => true, 'is_elective' => false
|
||||
];
|
||||
// The rest are singles
|
||||
$num_singles = $lessons_to_create - 2;
|
||||
}
|
||||
|
||||
// Create the single lessons
|
||||
for ($i = 0; $i < $num_singles; $i++) {
|
||||
$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' => false, 'is_elective' => false
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process elective groups
|
||||
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_create = (int)$group['lessons_per_week'];
|
||||
$has_double = (bool)$group['has_double_lesson'];
|
||||
$class_ids = array_unique(array_column($group['component_lessons'], 'class_id'));
|
||||
$teacher_ids = array_unique(array_column($group['component_lessons'], 'teacher_id'));
|
||||
$num_singles = $lessons_to_create;
|
||||
|
||||
if ($has_double && $lessons_to_create >= 2) {
|
||||
// Create one double lesson
|
||||
$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']
|
||||
'type' => 'elective_group', 'class_id' => $class_ids, 'subject_id' => null,
|
||||
'teacher_ids' => $teacher_ids, 'display_name' => $group['display_name'],
|
||||
'is_double' => true, 'is_elective' => true, 'component_lessons' => $group['component_lessons']
|
||||
];
|
||||
// The rest are singles
|
||||
$num_singles = $lessons_to_create - 2;
|
||||
}
|
||||
|
||||
// Create the single lessons
|
||||
for ($i = 0; $i < $num_singles; $i++) {
|
||||
$lessons_to_schedule[] = [
|
||||
'type' => 'elective_group', 'class_id' => $class_ids, 'subject_id' => null,
|
||||
'teacher_ids' => $teacher_ids, 'display_name' => $group['display_name'],
|
||||
'is_double' => false, 'is_elective' => true, 'component_lessons' => $group['component_lessons']
|
||||
];
|
||||
if ($is_double) $i++;
|
||||
}
|
||||
}
|
||||
|
||||
error_log("generate_timetable: Prepared " . count($lessons_to_schedule) . " lessons to schedule.");
|
||||
|
||||
|
||||
// 3. Shuffle and then sort lessons (place doubles and electives first)
|
||||
shuffle($lessons_to_schedule);
|
||||
@ -122,7 +146,7 @@ function generate_timetable($data, $days_of_week) {
|
||||
$period_popularity = array_fill(0, $periods_per_day, 0);
|
||||
foreach ($lessons_to_schedule as $index => $lesson) {
|
||||
$lesson_label = $lesson['display_name'] . (is_array($lesson['class_id']) ? ' for ' . count($lesson['class_id']) . ' classes' : ' for class ' . $lesson['class_id']);
|
||||
error_log("generate_timetable: Attempting to place lesson #" . ($index + 1) . ": " . $lesson_label);
|
||||
|
||||
|
||||
$best_slot = find_best_slot_for_lesson($lesson, $class_timetables, $teacher_timetables, $days_of_week, $periods_per_day, $data['workloads'], $data['timeslots'], $period_popularity);
|
||||
|
||||
@ -134,7 +158,7 @@ function generate_timetable($data, $days_of_week) {
|
||||
if ($lesson['is_double']) {
|
||||
$period_popularity[$period + 1]++;
|
||||
}
|
||||
error_log("generate_timetable: Found best slot for lesson #" . ($index + 1) . " at Day $day, Period $period.");
|
||||
|
||||
|
||||
if ($lesson['type'] === 'single') {
|
||||
$class_id = $lesson['class_id'];
|
||||
@ -187,11 +211,11 @@ function generate_timetable($data, $days_of_week) {
|
||||
}
|
||||
} else {
|
||||
$lessons_failed++;
|
||||
error_log("generate_timetable: FAILED to find slot for lesson #" . ($index + 1) . ": " . $lesson_label);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
error_log("generate_timetable: Placement complete. Placed: $lessons_placed, Failed: $lessons_failed.");
|
||||
|
||||
return $class_timetables;
|
||||
}
|
||||
|
||||
@ -217,11 +241,12 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
$lessons_per_week_for_subject = $lesson['component_lessons'][0]['lessons_per_week'];
|
||||
}
|
||||
|
||||
|
||||
for ($day = 0; $day < count($days_of_week); $day++) {
|
||||
for ($period = 0; $period < $periods_per_day; $period++) {
|
||||
// Basic availability check
|
||||
if ($is_double && $period + 1 >= $periods_per_day) continue;
|
||||
if ($is_double && $period + 1 >= $periods_per_day) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent placing double lessons across breaks
|
||||
if ($is_double) {
|
||||
@ -238,13 +263,21 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
}
|
||||
|
||||
if ($first_original_index === -1 || $second_original_index === -1 || $second_original_index !== $first_original_index + 1) {
|
||||
continue; // This slot spans a break, so it's invalid for a double lesson.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$slot_available = true;
|
||||
foreach ($class_ids as $cid) {
|
||||
if (!isset($class_timetables[$cid]) || $class_timetables[$cid][$day][$period] !== null || ($is_double && $class_timetables[$cid][$day][$period + 1] !== null)) {
|
||||
if (!isset($class_timetables[$cid])) {
|
||||
$slot_available = false;
|
||||
break;
|
||||
}
|
||||
if ($class_timetables[$cid][$day][$period] !== null) {
|
||||
$slot_available = false;
|
||||
break;
|
||||
}
|
||||
if ($is_double && $class_timetables[$cid][$day][$period + 1] !== null) {
|
||||
$slot_available = false;
|
||||
break;
|
||||
}
|
||||
@ -252,14 +285,22 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
if (!$slot_available) continue;
|
||||
|
||||
foreach ($teacher_ids as $tid) {
|
||||
if (!isset($teacher_timetables[$tid]) || $teacher_timetables[$tid][$day][$period] !== null || ($is_double && isset($teacher_timetables[$tid][$day][$period + 1]) && $teacher_timetables[$tid][$day][$period + 1] !== null)) {
|
||||
if (!isset($teacher_timetables[$tid])) {
|
||||
$slot_available = false;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
if ($teacher_timetables[$tid][$day][$period] !== null) {
|
||||
$slot_available = false;
|
||||
break;
|
||||
}
|
||||
if ($is_double && isset($teacher_timetables[$tid][$day][$period + 1]) && $teacher_timetables[$tid][$day][$period + 1] !== null) {
|
||||
$slot_available = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($slot_available) {
|
||||
$current_score = 100; // Start with a base score for an available slot
|
||||
$current_score = 100;
|
||||
|
||||
// Rule 1: Penalize placing the same subject on the same day for a class
|
||||
$subject_on_day_count = 0;
|
||||
@ -270,7 +311,6 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
if ($lesson['type'] === 'single' && isset($existing_lesson['subject_id']) && $existing_lesson['subject_id'] == $subject_id) {
|
||||
$subject_on_day_count++;
|
||||
}
|
||||
// Check for electives by group name
|
||||
if ($lesson['type'] === 'elective_group' && isset($existing_lesson['group_name']) && $existing_lesson['group_name'] == $lesson['display_name']) {
|
||||
$subject_on_day_count++;
|
||||
}
|
||||
@ -278,13 +318,11 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
}
|
||||
}
|
||||
|
||||
// Only apply penalty if the number of lessons is less than or equal to the number of days
|
||||
if ($lessons_per_week_for_subject <= count($days_of_week)) {
|
||||
if ($subject_on_day_count > 0) {
|
||||
$current_score -= 50; // Heavy penalty
|
||||
$current_score -= 50;
|
||||
}
|
||||
} else {
|
||||
// If we must repeat, penalize stacking more than 2 in one day
|
||||
if ($subject_on_day_count > 1) {
|
||||
$current_score -= 25;
|
||||
}
|
||||
@ -295,23 +333,22 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
foreach ($class_ids as $cid) {
|
||||
$lessons_on_day += count(array_filter($class_timetables[$cid][$day]));
|
||||
}
|
||||
// The penalty increases quadratically to strongly avoid busy days
|
||||
$current_score -= $lessons_on_day * $lessons_on_day;
|
||||
$day_fullness_penalty = $lessons_on_day * 2; // Reduced penalty
|
||||
$current_score -= $day_fullness_penalty;
|
||||
|
||||
// Rule 3: Penalize consecutive lessons for a teacher (teacher fatigue)
|
||||
// Rule 3: Penalize consecutive lessons for a teacher
|
||||
foreach ($teacher_ids as $tid) {
|
||||
// Check period before
|
||||
if ($period > 0 && $teacher_timetables[$tid][$day][$period - 1] !== null) {
|
||||
$current_score -= 15;
|
||||
}
|
||||
// Check period after (don't check for double lessons as that's intended)
|
||||
if (!$is_double && $period < $periods_per_day - 1 && $teacher_timetables[$tid][$day][$period + 1] !== null) {
|
||||
$current_score -= 15;
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 4: Penalize placing in a timeslot that is already popular across all classes
|
||||
$current_score -= $period_popularity[$period] * 5;
|
||||
// Rule 4: Penalize popular timeslots
|
||||
$popularity_penalty = $period_popularity[$period] * 5;
|
||||
$current_score -= $popularity_penalty;
|
||||
|
||||
if ($current_score > $best_score) {
|
||||
$best_score = $current_score;
|
||||
@ -320,6 +357,7 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $best_slot;
|
||||
}
|
||||
|
||||
@ -327,21 +365,21 @@ function find_best_slot_for_lesson($lesson, &$class_timetables, &$teacher_timeta
|
||||
// --- Timetable Persistence ---
|
||||
function save_timetable($pdo, $class_timetables, $timeslots) {
|
||||
if (empty($class_timetables)) {
|
||||
error_log("save_timetable: Attempted to save an empty timetable. Aborting.");
|
||||
|
||||
return;
|
||||
}
|
||||
error_log("save_timetable: Starting timetable save process.");
|
||||
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
error_log("save_timetable: Transaction started.");
|
||||
|
||||
|
||||
// It's better to delete from the child table first to avoid foreign key issues.
|
||||
$pdo->exec('DELETE FROM schedule_teachers');
|
||||
error_log("save_timetable: Deleted data from schedule_teachers.");
|
||||
|
||||
|
||||
$pdo->exec('DELETE FROM schedules');
|
||||
error_log("save_timetable: Deleted data from schedules.");
|
||||
|
||||
|
||||
$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) ' .
|
||||
@ -359,7 +397,7 @@ function save_timetable($pdo, $class_timetables, $timeslots) {
|
||||
foreach ($period_schedule as $period_idx => $lesson) {
|
||||
if ($lesson && !in_array($period_idx, $processed_periods)) {
|
||||
if (!isset($lesson_periods[$period_idx]['id'])) {
|
||||
error_log("save_timetable: Missing timeslot for period index {$period_idx}. Skipping lesson.");
|
||||
|
||||
continue;
|
||||
}
|
||||
$timeslot_id = $lesson_periods[$period_idx]['id'];
|
||||
@ -396,18 +434,18 @@ function save_timetable($pdo, $class_timetables, $timeslots) {
|
||||
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->commit();
|
||||
error_log("save_timetable: Timetable save completed successfully. Transaction committed.");
|
||||
|
||||
} else {
|
||||
error_log("save_timetable: Warning: No active transaction to commit.");
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("save_timetable: An exception occurred. " . $e->getMessage());
|
||||
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
error_log("save_timetable: Transaction rolled back.");
|
||||
|
||||
} else {
|
||||
error_log("save_timetable: No active transaction to roll back.");
|
||||
|
||||
}
|
||||
// Re-throw the exception to see the error on the screen if display_errors is on
|
||||
throw $e;
|
||||
@ -441,7 +479,7 @@ function get_timetable_from_db($pdo, $classes, $timeslots, $days_of_week) {
|
||||
$timeslot_id = $lesson['timeslot_id'];
|
||||
|
||||
if ($day_idx >= count($days_of_week)) {
|
||||
error_log("Invalid day_idx {$day_idx} found for lesson ID {$lesson['id']}. Skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user