/home/techb158/workloadmatch.com/workloadmatch.com/Manager
Edit: /home/techb158/workloadmatch.com/workloadmatch.com/Manager/Create_Schedule_Algorithm_2.php (76505B)
prepare("SELECT Program_ID, Program_Name FROM Programs");
$stmtGroup->execute();
$programData = $stmtGroup->get_result()->fetch_all(MYSQLI_ASSOC);
$stmtGroup->close();
// =========================================
// 3. Define Utility Functions (Important Modularization)
// =========================================
// --------------- Helper functions (these should be at the end of the file) ---------------
/**
* Find the best course to "push" for a blocked slot (retake), using the same logic as normal slot selection.
*/
function getCourseToPush(
$specialCourseOnlyMode,
$specialCourseID,
$selectedCourses,
$remainingSessions,
$slotIndex,
$lockedSlot,
$courseSlotMap,
$activeCourses,
$usedToday,
$courseCodes
) {
if ($specialCourseOnlyMode) return $specialCourseID;
$candidates = array_filter($selectedCourses, function($cid) use (
$remainingSessions, $slotIndex, $lockedSlot, $courseSlotMap
) {
if (($remainingSessions[$cid] ?? 0) <= 0) return false;
if (isset($lockedSlot[$cid]) && $lockedSlot[$cid] !== $slotIndex) return false;
if (isset($courseSlotMap[$cid]) && $courseSlotMap[$cid] !== $slotIndex) return false;
return true;
});
$onlyCourseIDLeft = (count($activeCourses) === 1) ? array_key_first($activeCourses) : null;
return getNextCourseForSlot(
$remainingSessions,
$candidates,
$usedToday,
$courseCodes,
$onlyCourseIDLeft
);
}
/**
* Find the next course to assign to a slot (normal scheduling).
*/
function selectNextCourse(
$specialCourseOnlyMode,
$specialCourseID,
$selectedCourses,
$remainingSessions,
$slotIndex,
$lockedSlot,
$courseSlotMap,
$activeCourses,
$usedToday,
$courseCodes
) {
if ($specialCourseOnlyMode) return $specialCourseID;
$candidates = array_filter($selectedCourses, function($cid) use (
$remainingSessions, $slotIndex, $lockedSlot, $courseSlotMap
) {
if (($remainingSessions[$cid] ?? 0) <= 0) return false;
if (isset($lockedSlot[$cid]) && $lockedSlot[$cid] !== $slotIndex) return false;
if (isset($courseSlotMap[$cid]) && $courseSlotMap[$cid] !== $slotIndex) return false;
return true;
});
$onlyCourseIDLeft = (count($activeCourses) === 1) ? array_key_first($activeCourses) : null;
return getNextCourseForSlot(
$remainingSessions,
$candidates,
$usedToday,
$courseCodes,
$onlyCourseIDLeft
);
}
/**
* Locks a course to a slot if there are only 2 courses left and they're running in parallel.
*/
function applySlotLocking(
$activeCourses,
$remainingSessions,
$specialCourseID,
&$lockedSlot,
$slotIndex
) {
if (count($activeCourses) === 2) {
foreach ($remainingSessions as $cid => $cnt) {
if ($cnt > 0 && $cid !== $specialCourseID && !isset($lockedSlot[$cid])) {
$lockedSlot[$cid] = $slotIndex;
}
}
}
}
// =========================================
// 4. Handle POST Request to Generate Schedule
// =========================================
// Format the label of the time slot, especially for Saturday sessions
function format_time_slot_label($slot, $day) {
if ($day === 'Saturday') {
if (stripos($slot, 'evening') !== false) return 'Morning (Sat)';
if (stripos($slot, 'morning') !== false) return 'Morning';
}
return $slot;
}
// Format the time range of the session based on the slot and the day
function format_time_slot_range($slot, $start, $end, $day) {
if ($day === 'Saturday') {
if (stripos($slot, 'morning') !== false) return "08:30 AM - 12:30 PM";
if (stripos($slot, 'evening') !== false) return "09:00 AM - 01:00 PM";
}
return date('h:i A', strtotime($start)) . ' - ' . date('h:i A', strtotime($end));
}
// Get group configuration from database
function get_group_info($mysqli, $Group_ID) {
$stmt = $mysqli->prepare("SELECT Time_Slot, Time_From, Time_To, class_days, Weekend_Class, Group_Name FROM Manager_Group_Name WHERE Group_ID = ?");
$stmt->bind_param("i", $Group_ID);
$stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
// Retrieve time slots for the group
function fetch_slot_times($mysqli, $slotLabels, $managerStart, $managerEnd) {
$timeFrom = [];
$timeTo = [];
foreach ($slotLabels as $label) {
$label = trim($label);
$stmt = $mysqli->prepare("SELECT Time_From, Time_To FROM Time_Slot_Programs WHERE Time_Slot = ?");
$stmt->bind_param("s", $label);
$stmt->execute();
$result = $stmt->get_result();
$validSlotFound = false;
while ($row = $result->fetch_assoc()) {
$slotStart = new DateTime($row['Time_From']);
$slotEnd = new DateTime($row['Time_To']);
if ($slotStart >= $managerStart && $slotEnd <= $managerEnd) {
$timeFrom[] = $slotStart->format("H:i:s");
$timeTo[] = $slotEnd->format("H:i:s");
$validSlotFound = true;
break;
}
}
if (!$validSlotFound && count($slotLabels) === 1) {
$timeFrom[] = $managerStart->format("H:i:s");
$timeTo[] = $managerEnd->format("H:i:s");
}
}
return [$timeFrom, $timeTo];
}
// Collect all holiday dates in the current year
function get_holidays($mysqli, $Start_Date) {
$holidays = [];
$res = $mysqli->query("SELECT Event_Title, Event_Start, Event_End FROM Events WHERE Calendar_Year = YEAR('$Start_Date')");
while ($row = $res->fetch_assoc()) {
$start = new DateTime($row['Event_Start']);
$end = new DateTime($row['Event_End']);
while ($start <= $end) {
$dateStr = $start->format('Y-m-d');
$holidays[$dateStr] = $row['Event_Title']; // π include the title
$start->modify('+1 day');
}
}
return $holidays;
}
// Collect all retake dates for the group and program
/*function get_retake_dates($mysqli, $Program_ID, $Group_ID) {
$retakeDates = [];
$stmt = $mysqli->prepare("SELECT Type, Retake_Date FROM Retake_Records WHERE Program_ID = ? AND Group_ID = ?");
$stmt->bind_param("ii", $Program_ID, $Group_ID);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
$retakeDates[$row['Retake_Date']] = $row['Type']; // e.g., ['2025-09-10' => 'Midterm']
}
return $retakeDates;
}*/
// In get_retake_dates() - ensure consistent structure
function get_retake_dates($mysqli, $Program_ID, $Group_ID) {
$retakeDates = [];
//$sql = "SELECT Group_Slot_ID, Time_Slot_Programs_ID, Time_Slot, Time_From, Time_To FROM Group_Slot_Mapping WHERE Group_ID = ?";
$sql = "
SELECT
rr.Time_Slot_Programs_ID,
rr.Retake_Date,
rr.Type,
gsm.Group_Slot_ID,
gsm.Time_Slot_Programs_ID,
gsm.Time_Slot,
gsm.Time_From,
gsm.Time_To
FROM Retake_Records AS rr
INNER JOIN Group_Slot_Mapping AS gsm
ON rr.Time_Slot_Programs_ID = gsm.Time_Slot_Programs_ID
AND rr.Group_ID = gsm.Group_ID
WHERE rr.Program_ID = ?
AND rr.Group_ID = ?
";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("ii", $Program_ID, $Group_ID);
$stmt->execute();
$res = $stmt->get_result();
//echo "
Retake SQL result rows: " . $res->num_rows . " ";
while ($row = $res->fetch_assoc()) {
$retakeDates[$row['Retake_Date']][$row['Group_Slot_ID']] = [
'label' => $row['Time_Slot'],
'from' => $row['Time_From'],
'to' => $row['Time_To'],
'type' => $row['Type']
];
}
return $retakeDates;
}
function get_retake_dates_info($mysqli, $Program_ID, $Group_ID) {
$retakeDates = [];
$stmt = $mysqli->prepare("
SELECT Retake_Date, Type, Time_Slot_Programs_ID
FROM Retake_Records
WHERE Program_ID = ? AND Group_ID = ?
");
$stmt->bind_param("ii", $Program_ID, $Group_ID);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
// Store as [date][slotID] = type
$retakeDates[$row['Retake_Date']][$row['Time_Slot_Programs_ID']] = $row['Type'];
}
return $retakeDates;
/*
$retakeDates = [];
$sql = "
SELECT
rr.Retake_Date,
rr.Type,
gsm.Group_Slot_ID
FROM Retake_Records AS rr
INNER JOIN Group_Slot_Mapping AS gsm
ON rr.Time_Slot_Programs_ID = gsm.Time_Slot_Programs_ID
AND rr.Group_ID = gsm.Group_ID
WHERE rr.Program_ID = ?
AND rr.Group_ID = ?
";
$stmt = $mysqli->prepare($sql);
$stmt->bind_param("ii", $Program_ID, $Group_ID);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
// now keyed by [date][slotIndex]
$date = $row['Retake_Date'];
$slotIdx = intval($row['Group_Slot_ID']) - 1; // zero-based
$retakeDates[$row['Retake_Date']][$row['Group_Slot_ID']] = $row['Type'];
//$retakeDates[$date][$slotIdx] = $row['Type'];
}
return $retakeDates;*/
}
// Calculate the average duration of one session
function calculate_session_length($timeFrom, $timeTo) {
$sessionLength = 0;
foreach ($timeFrom as $i => $from) {
$fromTime = new DateTime($from);
$toTime = new DateTime($timeTo[$i]);
$sessionLength += ($toTime->getTimestamp() - $fromTime->getTimestamp()) / 3600;
}
$slotCount = count($timeFrom);
return $slotCount > 0 ? $sessionLength / $slotCount : 3;
}
// Calculate the number of sessions required for each course
function calculate_course_sessions($mysqli, $Program_ID, $sessionLength) {
$courseSessions = [];
$res = $mysqli->query("SELECT Course_ID, Course_Time FROM Courses WHERE Program_ID = $Program_ID");
while ($row = $res->fetch_assoc()) {
$courseID = $row['Course_ID'];
$courseHours = $row['Course_Time'];
$courseSessions[$courseID] = ceil($courseHours / $sessionLength);
}
return $courseSessions;
}
// Generate all valid class dates, excluding holidays, retakes, and July
function generate_valid_dates($Start_Date, $End_Date, $classDays, $holidayDates, $retakeDates) {
$validDates = [];
$cur = new DateTime($Start_Date);
$end = $End_Date ? new DateTime($End_Date) : null;
while (!$end || $cur <= $end) {
$day = $cur->format('l');
$dateStr = $cur->format('Y-m-d');
$month = (int) $cur->format('m');
// if ($month === 7 || !in_array($day, $classDays) || array_key_exists($dateStr, $holidayDates) || array_key_exists($dateStr, $retakeDates)) {
// $cur->modify('+1 day');
// continue;
//}
if ($month === 7
|| !in_array($day, $classDays)
|| array_key_exists($dateStr, $holidayDates)) {
$cur->modify('+1 day');
continue;
}
$validDates[] = $dateStr;
$cur->modify('+1 day');
if ($end === null && count($validDates) > 365) break; // Prevent infinite loop in fallback
}
return $validDates;
}
// Build calendar excluding holidays, retakes, and July
function build_schedule_calendar($Start_Date, $End_Date, $classDays, $timeSlots, $mysqli, $Program_ID, $Group_ID) {
$start = new DateTime($Start_Date);
$end = $End_Date ? new DateTime($End_Date) : new DateTime('+1 year');
$calendar = [];
$holidayDates = get_holidays($mysqli, $Start_Date);
$retakeDates = get_retake_dates($mysqli, $Program_ID, $Group_ID);
$cur = clone $start;
while ($cur <= $end) {
$day = $cur->format('l');
$dateStr = $cur->format('Y-m-d');
$month = (int) $cur->format('m');
if ($month !== 7 && in_array($day, $classDays) && !in_array($dateStr, $holidayDates) && !in_array($dateStr, $retakeDates)) {
foreach ($timeSlots as $slot) {
$calendar[$dateStr][trim($slot)] = 'free';
}
}
$cur->modify('+1 day');
}
return $calendar;
}
// β
You can now call these functions in your main POST logic to keep code readable and modular.
// Helper function for fair rotation
/*function getNextCourseForSlot($remainingSessions, $courseList, $usedToday) {
$available = array_filter($courseList, function($courseID) use ($remainingSessions) {
return ($remainingSessions[$courseID] ?? 0) > 0;
});
$unusedToday = array_filter($available, fn($courseID) => !in_array($courseID, $usedToday));
if (!empty($unusedToday)) {
return reset($unusedToday);
}
// Allow same course in both slots if only one remains
if (count($available) === 1) {
return reset($available);
}
return null;
}*/
/**
* Returns the next best course ID to schedule for a given slot, given remaining sessions, priorities, and special cases.
*
* @param array $remainingSessions courseID => number left
* @param array $candidates list of course IDs eligible for this slot
* @param array $usedToday list of course IDs already scheduled *today* (to avoid doubling same course per day)
* @param array $courseCodes map: courseID => course code
* @param int|null $onlyCourseIDLeft if only one course remains, its ID
* @return int|null the selected course ID or null if none eligible
*/
function getNextCourseForSlot($remainingSessions, $candidates, $usedToday, $courseCodes = [], $onlyCourseIDLeft = null) {
// 1. Filter to courses that have sessions left
$available = array_filter($candidates, function($cid) use ($remainingSessions) {
return ($remainingSessions[$cid] ?? 0) > 0;
});
// 2. Prefer a course not already used today
foreach ($available as $cid) {
if (!in_array($cid, $usedToday)) {
return $cid;
}
}
// 3. If only one course left (last), allow re-use
if (count($available) === 1 && ($onlyCourseIDLeft === reset($available))) {
return reset($available);
}
// 4. Special: If two courses left and one is a "special" course code, prefer it
if (count($available) === 2 && !empty($courseCodes)) {
foreach ($available as $cid) {
$code = $courseCodes[$cid] ?? '';
if (in_array($code, ['961-238', '960-746'])) {
return $cid;
}
}
}
// 5. If all else fails, just return the first available
return reset($available) ?: null;
}
$scheduleOutput = []; // Default
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
//if (isset($_POST['reset']) || isset($_POST['Start_Date'])) {
// unset($_SESSION['deletedSaturdays']);
//}
// Step 1: Retrieve Form Data
$Program_ID = intval($_POST['Program_ID']);
$Group_ID = intval($_POST['Group_ID']);
$Start_Date = $_POST['Start_Date'];
$End_Date = $_POST['End_Date'] ?? date('Y-m-d', strtotime('+1 year', strtotime($Start_Date)));
$selectedCourses = $_POST['Selected_Courses'] ?? [];
$courseSessions = $_POST['Course_Sessions'] ?? [];
$priorityCourses = $_POST['Priority_Course'] ?? [];
$group = get_group_info($mysqli, $Group_ID);
if (!$group) {
echo "Group info not found.";
exit;
}
$classDays = explode(',', $group['class_days']);
if (!$group['Weekend_Class'] && in_array('Saturday', $classDays)) {
$classDays = array_filter($classDays, fn($day) => $day !== 'Saturday');
}
$slotLabels = explode(',', $group['Time_Slot']);
$managerStart = new DateTime(trim($group['Time_From']));
$managerEnd = new DateTime(trim($group['Time_To']));
//list($timeFrom, $timeTo) = fetch_slot_times($mysqli, $slotLabels, $managerStart, $managerEnd);
// Example (replace with your actual fetch code):
$all_slot_program_ids = [];
$group_slot_labels = [];
$group_time_from = [];
$group_time_to = [];
$stmt = $mysqli->prepare("SELECT Group_Slot_ID,Time_Slot_Programs_ID, Time_Slot, Time_From, Time_To FROM Group_Slot_Mapping WHERE Group_ID = ?");
$stmt->bind_param("i", $Group_ID);
$stmt->execute();
$res = $stmt->get_result();
$slotLabels = $timeFrom = $timeTo = [];
while ($row = $res->fetch_assoc()) {
$slotLabels[] = $row['Time_Slot'];
$timeFrom[] = $row['Time_From'];
$timeTo[] = $row['Time_To'];
$group_slot_id[] = $row['Group_Slot_ID'];
$slotProgramID = $row['Time_Slot_Programs_ID'];
$all_slot_program_ids[] = $slotProgramID;
$group_slot_labels[$slotProgramID] = $row['Time_Slot'];
$group_time_from[$slotProgramID] = $row['Time_From'];
$group_time_to[$slotProgramID] = $row['Time_To'];
}
$sessionLength = calculate_session_length($timeFrom, $timeTo);
$courseSessions = calculate_course_sessions($mysqli, $Program_ID, $sessionLength);
$holidayDates = get_holidays($mysqli, $Start_Date);
$retakeDates = get_retake_dates($mysqli, $Program_ID, $Group_ID);
$validDates = generate_valid_dates($Start_Date, $End_Date, $classDays, $holidayDates, $retakeDates);
$suggestedSessions = calculate_course_sessions($mysqli, $Program_ID, $sessionLength);
$courseSessions = [];
foreach ($selectedCourses as $courseID) {
$custom = $_POST['Course_Sessions'][$courseID] ?? null;
if (is_numeric($custom) && intval($custom) > 0) {
$courseSessions[$courseID] = intval($custom);
} else {
$courseSessions[$courseID] = $suggestedSessions[$courseID] ?? 0;
}
}
//echo "
π
Valid Dates for Scheduling (Excluding July) "; print_r($validDates); echo " ";
// π Additional scheduling logic ....
// Output valid dates
//echo "
π
Valid Dates for Scheduling (Excluding July) "; print_r($validDates); echo " ";
$totalSessionsNeeded = array_sum($courseSessions);
$availableDays = count($validDates);
// Estimate sessions per day (based on number of slots)
$sessionsPerDay = count($slotLabels);
// Calculate how many days are needed to finish all sessions
$daysRequired = ceil($totalSessionsNeeded / $sessionsPerDay);
// Get expected end date
$expectedEndDate = $validDates[$daysRequired - 1] ?? end($validDates); // fallback to last available if out of bounds
//echo "
π
Expected End Date Based on Andragogical Days ";
//echo "
$expectedEndDate (" . (new DateTime($expectedEndDate))->format('l') . ")
";
// Sort by course priority
usort($selectedCourses, function ($a, $b) use ($priorityCourses) {
$priorityA = $priorityCourses[$a] ?? PHP_INT_MAX;
$priorityB = $priorityCourses[$b] ?? PHP_INT_MAX;
return $priorityA <=> $priorityB;
});
// echo "
π Sorted Courses (Priority First) "; print_r($selectedCourses); echo " ";
// Prepare for fair scheduling (one course per slot)
// Initialize
$schedule = [];
$remainingSessions = [];
foreach ($selectedCourses as $courseID) {
$remainingSessions[$courseID] = $courseSessions[$courseID] ?? 0;
}
// Queue of all courses
$courseQueue = $selectedCourses;
$currentCourses = [
0 => null, // Slot 1 (e.g. Morning)
1 => null // Slot 2 (e.g. Afternoon)
];
// Assign initial two courses
// Assign initial courses to slots without repeating
$courseQueueCopy = $selectedCourses;
$assignedCourses = [];
foreach ([0, 1] as $slotIndex) {
foreach ($courseQueueCopy as $courseID) {
if (!in_array($courseID, $assignedCourses) && $remainingSessions[$courseID] > 0) {
$currentCourses[$slotIndex] = $courseID;
$assignedCourses[] = $courseID;
break;
}
}
}
// Fetch all course names into array
$courseNames = [];
$courseRes = $mysqli->query("SELECT Course_ID, Course_Name FROM Courses");
while ($row = $courseRes->fetch_assoc()) {
$courseNames[$row['Course_ID']] = $row['Course_Name'];
}
// --- Setup slot/course/retake tracking structures ---
$courseSlotMap = []; // Tracks which slot a course is "locked" to (courseID => slotIndex)
$specialCourseOnlyMode = false; // If only 1 "special" course is left, triggers special scheduling logic
$specialCourseID = null; // Holds the ID of the "special" course (e.g. 961-238, 960-746)
$lockedSlot = []; // Tracks if a course is locked to a slot (courseID => slotIndex)
$pushedCourse = []; // If a course is "pushed" (blocked by a retake), store it for later in this slot (slotIndex => courseID)
$numSlots = count($slotLabels); // Number of time slots per day
// Setup before main loop
//$__special_sessions_adjusted = false; // <-- Place before foreach ($validDates...)
//
//
//
//// Map of course IDs to codes
//$courseCodes = [];
//$courseRes = $mysqli->query("SELECT Course_ID, Course_Code FROM Courses");
//while ($row = $courseRes->fetch_assoc()) {
// $courseCodes[$row['Course_ID']] = $row['Course_Code'];
//}
//// Initial session setup (already in your code)
//$remainingSessions = [];
//foreach ($selectedCourses as $courseID) {
// $remainingSessions[$courseID] = $courseSessions[$courseID] ?? 0;
//}
//
//// SPECIAL LOGIC: Only ONE course left, and it's a special course
//$specialCourseCodes = ['961-238', '960-746'];
//$specialCoursesLeft = [];
//foreach ($remainingSessions as $cid => $sessions) {
// if ($sessions > 0 && in_array($courseCodes[$cid] ?? '', $specialCourseCodes)) {
// $specialCoursesLeft[] = $cid;
// }
//}
//if (count(array_filter($remainingSessions, fn($s) => $s > 0)) === 1 && count($specialCoursesLeft) === 1 && $numSlots > 1) {
// $specialCourseID = $specialCoursesLeft[0];
// $remainingSessions[$specialCourseID] = ceil($remainingSessions[$specialCourseID] / $numSlots);
// // Optionally: echo "
Adjusted special course $specialCourseID to " . $remainingSessions[$specialCourseID] . " sessions ";
//}
//// Debug: show remaining sessions after adjustment
//echo "
DEBUG: Remaining Sessions After Special Adjustment\n";
//print_r($remainingSessions);
//echo " ";
// --- Main scheduling loop over each valid date ---
foreach ($validDates as $date) {
$usedToday = []; // Reset courses used for this day
$dayName = (new DateTime($date))->format('l'); // Get the day of week, e.g. "Monday"
$activeCourses = array_filter($remainingSessions, fn($s) => $s > 0); // Only courses that still need sessions
$activeCourseIDs = array_keys($activeCourses); // Their IDs
// --- Detect special course only mode (e.g. last course is a "special" one) ---
$specialCourseOnlyMode = false;
$specialCourseID = null;
if (count($activeCourseIDs) === 1) {
$cid = $activeCourseIDs[0];
$code = $courseCodes[$cid] ?? '';
/* if (count($activeCourseIDs) === 1 && !$__special_sessions_adjusted) {
$cid = $activeCourseIDs[0];
$code = $courseCodes[$cid] ?? '';
if (in_array($code, $specialCourseCodes) && $numSlots > 1) {
//$remainingSessions[$cid] = ceil($remainingSessions[$cid] / $numSlots);
//$__special_sessions_adjusted = true;
}
}*/
if (in_array($code, ['961-238', '960-746'])) {
//if (in_array($code, ['', ''])) {
$specialCourseOnlyMode = true;
$specialCourseID = $cid;
// π‘ ADJUST THE SESSION COUNT if we allow special course in both slots
// Only adjust ONCE!
if ($numSlots > 1 && !$__special_sessions_adjusted) {
$remainingSessions[$specialCourseID] = ceil($remainingSessions[$specialCourseID] / $numSlots);
$__special_sessions_adjusted = true;
}
}
}
// ========== SINGLE SLOT LOGIC ==========
if ($numSlots === 1) {
$slotIndex = 0;
$from = $timeFrom[$slotIndex] ?? null;
$to = $timeTo[$slotIndex] ?? null;
$slotLabel = $slotLabels[$slotIndex];
$groupSlot = $group_slot_id[$slotIndex] ?? 0;
// If there's a retake for this date (regardless of slot), show retake only
//if (isset($retakeDates[$date])) {
// For old format where $retakeDates[$date] is string or array
//$retakeType = is_array($retakeDates[$date]) ? reset($retakeDates[$date]) : $retakeDates[$date];
// Look for any slot on this date
$slotIndex = 0;
$groupSlotId = $group_slot_id[$slotIndex]; // should be 32
if (isset($retakeDates[$date][$groupSlotId])) {
$retake = $retakeDates[$date][$groupSlotId];
$schedule[] = [
'Date' => $date,
'Slot' => $retake['label'] ?? $slotLabels[0], // <-- Use label from retakeDates if present, else default
'Time' => format_time_slot_range($retake['label'], $retake['from'], $retake['to'], $dayName),
'Course_ID' => null,
'Course_Name' => "Retake: " . htmlspecialchars($retake['type']),
'Start_Time' => $retake['from'],
'End_Time' => $retake['to'],
'Group_Slot_ID'=> $groupSlotId,
'Reserve_Course' => 0,
'IsRetake' => true,
'Retake_Type' => $retake['type']
];
continue;
}
// If a pushed (previously blocked) course exists, assign it now
if (isset($pushedCourse[$slotIndex])) {
$courseID = $pushedCourse[$slotIndex];
unset($pushedCourse[$slotIndex]);
} else {
// Otherwise, pick next available course normally
if ($specialCourseOnlyMode) {
$courseID = $specialCourseID;
} else {
$candidates = array_filter($selectedCourses, function($cid) use ($remainingSessions) {
return ($remainingSessions[$cid] ?? 0) > 0;
});
$onlyCourseIDLeft = (count($activeCourses) === 1) ? array_key_first($activeCourses) : null;
$courseID = getNextCourseForSlot(
$remainingSessions,
$candidates,
$usedToday,
$courseCodes,
$onlyCourseIDLeft
);
}
if (!$courseID || ($remainingSessions[$courseID] ?? 0) <= 0) continue;
$usedToday[] = $courseID;
}
$courseName = $courseNames[$courseID] ?? "Course #$courseID";
$schedule[] = [
'Date' => $date,
'Slot' => "Slot 1",
'Time' => format_time_slot_range($slotLabel, $from, $to, $dayName),
'Course_ID' => $courseID,
'Course_Name' => $courseName,
'Start_Time' => $from,
'End_Time' => $to,
'Group_Slot_ID' => $group_slot_id[$slotIndex] ?? 0,
'Reserve_Course' => 0
];
$remainingSessions[$courseID]--;
if (array_sum($remainingSessions) <= 0) break;
}
// ========== MULTI-SLOT LOGIC (2 or more slots) ==========
else {
// Loop through each slot (e.g. morning/afternoon)
foreach (array_keys($slotLabels) as $slotIndex) {
$from = $timeFrom[$slotIndex] ?? null;
$to = $timeTo[$slotIndex] ?? null;
$slotLabel = $slotLabels[$slotIndex];
$groupSlotId = $group_slot_id[$slotIndex] ?? 0;
if (!$from || !$to) continue;
// --- Special course only mode: skip non-morning slots
//if ($specialCourseOnlyMode && $slotIndex !== 0) continue;
// --- 1. RETAKE HANDLING ---
if (!empty($retakeDates[$date][$groupSlotId])) {
$slotInfo = $retakeDates[$date][$groupSlotId];
$retakeType = $slotInfo['type'] ?? '';
$slotLabel = $slotInfo['label'] ?? $slotLabel;
$from = $slotInfo['from'] ?? $from;
$to = $slotInfo['to'] ?? $to;
$schedule[] = [
'Date' => $date,
'Slot' => $slotLabel,
'Time' => format_time_slot_range($slotLabel, $from, $to, $dayName),
'Course_ID' => null,
'Course_Name' => "Retake: " . htmlspecialchars($retakeType),
'Start_Time' => $from,
'End_Time' => $to,
'Group_Slot_ID' => $groupSlotId,
'Reserve_Course' => 0,
'IsRetake' => true,
'Retake_Type' => $retakeType
];
// Optionally push the blocked course for this slot
if (!isset($pushedCourse[$slotIndex])) {
$courseToPush = getCourseToPush(
$specialCourseOnlyMode,
$specialCourseID,
$selectedCourses,
$remainingSessions,
$slotIndex,
$lockedSlot,
$courseSlotMap,
$activeCourses,
$usedToday,
$courseCodes
);
if ($courseToPush !== null) {
$pushedCourse[$slotIndex] = $courseToPush;
}
}
continue; // Only skip normal class for this *slot*, not the whole date!
}
// --- 2. PUSHED COURSE HANDLING (if retake blocked a course previously) ---
if (isset($pushedCourse[$slotIndex])) {
$courseID = $pushedCourse[$slotIndex];
unset($pushedCourse[$slotIndex]);
// If no remaining sessions for that course, skip
if (($remainingSessions[$courseID] ?? 0) <= 0) continue;
} else {
// --- 3. NORMAL COURSE SCHEDULING ---
/* $courseID = selectNextCourse(
$specialCourseOnlyMode,
$specialCourseID,
$selectedCourses,
$remainingSessions,
$slotIndex,
$lockedSlot,
$courseSlotMap,
$activeCourses,
$usedToday,
$courseCodes
);
if (!$courseID || ($remainingSessions[$courseID] ?? 0) <= 0) continue;
$usedToday[] = $courseID; */
if (count($activeCourses) === 1) {
// Only one course left, allow it in all slots (don't check $usedToday)
$courseID = array_key_first($activeCourses);
} else {
// Usual course selection logic
$courseID = selectNextCourse(
$specialCourseOnlyMode,
$specialCourseID,
$selectedCourses,
$remainingSessions,
$slotIndex,
$lockedSlot,
$courseSlotMap,
$activeCourses,
$usedToday,
$courseCodes
);
if (!$courseID || ($remainingSessions[$courseID] ?? 0) <= 0) continue;
// Prevent same course twice if more than one course left
if ($courseID && !in_array($courseID, $usedToday)) {
$usedToday[] = $courseID;
}
}
}
// --- 4. SLOT LOCKING LOGIC ---
applySlotLocking(
$activeCourses,
$remainingSessions,
$specialCourseID,
$lockedSlot,
$slotIndex
);
// Lock this course to this slot, if not already locked
if (!isset($courseSlotMap[$courseID])) {
$courseSlotMap[$courseID] = $slotIndex;
}
// --- 5. SCHEDULE THE COURSE ---
$schedule[] = [
'Date' => $date,
'Slot' => "Slot " . ($slotIndex + 1),
'Time' => format_time_slot_range($slotLabel, $from, $to, $dayName),
'Course_ID' => $courseID,
'Course_Name' => $courseNames[$courseID] ?? "Course #$courseID",
'Start_Time' => $from,
'End_Time' => $to,
'Group_Slot_ID' => $groupSlotId,
'Reserve_Course' => 0
];
$remainingSessions[$courseID]--;
// If all sessions are scheduled, break out of both loops
if (array_sum($remainingSessions) <= 0) break 2;
}
} // End multi-slot else
} // End foreach validDates
/* // --- Make sure you already have getNextCourseForSlot() defined elsewhere in your file ---
echo "
--- DEBUGGING ARRAYS --- ";
echo "DEBUG Program_ID: $Program_ID, Group_ID: $Group_ID ";
echo "Selected Courses:\n";
print_r($selectedCourses);
echo "\nCourse Sessions (per course):\n";
print_r($courseSessions);
echo "\nPriority Courses:\n";
print_r($priorityCourses);
echo "\nSlot Labels:\n";
print_r($slotLabels);
echo "\nTime From (per slot):\n";
print_r($timeFrom);
echo "\nTime To (per slot):\n";
print_r($timeTo);
echo "\nGroup Slot IDs:\n";
print_r($group_slot_id);
echo "\nAll Slot Program IDs:\n";
print_r($all_slot_program_ids);
echo "\nGroup Slot Labels (SlotProgramID => Label):\n";
print_r($group_slot_labels);
echo "\nGroup Time From (SlotProgramID => Time):\n";
print_r($group_time_from);
echo "\nGroup Time To (SlotProgramID => Time):\n";
print_r($group_time_to);
echo "\nHoliday Dates:\n";
print_r($holidayDates);
echo "\nRetake Dates:\n";
print_r($retakeDates);
echo "\nValid Dates:\n";
print_r($validDates);
echo "\nCourse Names (CourseID => Name):\n";
print_r($courseNames);
echo "\nCourse Slot Map (CourseID => SlotProgramID):\n";
print_r($courseSlotMap);
echo "\nRemaining Sessions (CourseID => Remaining):\n";
print_r($remainingSessions);
echo "\nPushed Course (SlotProgramID => CourseID):\n";
print_r($pushedCourse);
echo "\nLocked Slot (CourseID => SlotProgramID):\n";
print_r($lockedSlot);
echo "\nSchedule (final result):\n";
print_r($schedule);
echo " "; */
// π Identify last scheduled session for each course
$lastSessions = [];
foreach ($schedule as $index => $entry) {
$courseID = $entry['Course_ID'];
$lastSessions[$courseID] = $index; // keeps overwriting until the last index
}
//echo "
π Final Generated Schedule "; print_r($schedule); echo " ";
//echo "
π Generated Schedule Table ";
//echo "
Group: " . htmlspecialchars($groupName) . "
";
if (!empty($schedule)) {
// Collect all displayed dates: scheduled, holidays, and retakes
$displayDates = [];
// 1. From scheduled sessions
foreach ($schedule as $row) {
$displayDates[$row['Date']][] = $row; // grouped by date
}
// 2. Add holidays (if no course scheduled on them)
foreach ($holidayDates as $hDate => $title) {
if (!isset($displayDates[$hDate])) {
$displayDates[$hDate] = []; // Add empty row so we render it
}
}
foreach ($retakeDates as $rDate => $slots) {
if (!isset($displayDates[$rDate])) {
$displayDates[$rDate] = [];
}
// For each slot with a retake on this date
foreach ($slots as $groupSlotId => $slotInfo) {
// Check if we already have a retake for this slot/date
$alreadyRetake = false;
foreach ($displayDates[$rDate] as $row) {
if (!empty($row['IsRetake']) && ($row['Slot'] == ($slotInfo['label'] ?? ''))) {
$alreadyRetake = true;
break;
}
}
if (!$alreadyRetake) {
$displayDates[$rDate][] = [
'Slot' => $slotInfo['label'] ?? null, // e.g., "Evening"
'Time' => isset($slotInfo['from'], $slotInfo['to'], $slotInfo['label'])
? format_time_slot_range($slotInfo['label'], $slotInfo['from'], $slotInfo['to'], (new DateTime($rDate))->format('l'))
: null,
'Course_ID' => null,
'IsRetake' => true,
'Retake_Type' => $slotInfo['type'] ?? null
];
}
}
}
//echo "
";
//echo "\nSisplayDates (final all result):\n";
//print_r($displayDates);
//
//echo " ";
// Sort the dates chronologically
ksort($displayDates);
} else {
echo "
π« No schedule generated β check if valid time slots, sessions, or dates are missing.
";
}
}
/*
$scheduleOutput = []; // Default
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
//if (isset($_POST['reset']) || isset($_POST['Start_Date'])) {
// unset($_SESSION['deletedSaturdays']);
//}
// Step 1: Retrieve Form Data
$Program_ID = intval($_POST['Program_ID']);
$Group_ID = intval($_POST['Group_ID']);
$Start_Date = $_POST['Start_Date'];
$End_Date = $_POST['End_Date'] ?? date('Y-m-d', strtotime('+1 year', strtotime($Start_Date)));
$selectedCourses = $_POST['Selected_Courses'] ?? [];
$courseSessions = $_POST['Course_Sessions'] ?? [];
$priorityCourses = $_POST['Priority_Course'] ?? [];
$group = get_group_info($mysqli, $Group_ID);
if (!$group) {
echo "Group info not found.";
exit;
}
$classDays = explode(',', $group['class_days']);
if (!$group['Weekend_Class'] && in_array('Saturday', $classDays)) {
$classDays = array_filter($classDays, fn($day) => $day !== 'Saturday');
}
$slotLabels = explode(',', $group['Time_Slot']);
$managerStart = new DateTime(trim($group['Time_From']));
$managerEnd = new DateTime(trim($group['Time_To']));
//list($timeFrom, $timeTo) = fetch_slot_times($mysqli, $slotLabels, $managerStart, $managerEnd);
$stmt = $mysqli->prepare("SELECT Group_Slot_ID, Time_Slot, Time_From, Time_To FROM Group_Slot_Mapping WHERE Group_ID = ?");
$stmt->bind_param("i", $Group_ID);
$stmt->execute();
$res = $stmt->get_result();
$slotLabels = $timeFrom = $timeTo = [];
while ($row = $res->fetch_assoc()) {
$slotLabels[] = $row['Time_Slot'];
$timeFrom[] = $row['Time_From'];
$timeTo[] = $row['Time_To'];
$group_slot_id[] = $row['Group_Slot_ID'];
}
$sessionLength = calculate_session_length($timeFrom, $timeTo);
$courseSessions = calculate_course_sessions($mysqli, $Program_ID, $sessionLength);
$holidayDates = get_holidays($mysqli, $Start_Date);
$retakeDates = get_retake_dates($mysqli, $Program_ID, $Group_ID);
$validDates = generate_valid_dates($Start_Date, $End_Date, $classDays, $holidayDates, $retakeDates);
//echo "
π
Valid Dates for Scheduling (Excluding July) "; print_r($validDates); echo " ";
// π Additional scheduling logic ....
// Output valid dates
//echo "
π
Valid Dates for Scheduling (Excluding July) "; print_r($validDates); echo " ";
$totalSessionsNeeded = array_sum($courseSessions);
$availableDays = count($validDates);
// Estimate sessions per day (based on number of slots)
$sessionsPerDay = count($slotLabels);
// Calculate how many days are needed to finish all sessions
$daysRequired = ceil($totalSessionsNeeded / $sessionsPerDay);
// Get expected end date
$expectedEndDate = $validDates[$daysRequired - 1] ?? end($validDates); // fallback to last available if out of bounds
//echo "
π
Expected End Date Based on Andragogical Days ";
//echo "
$expectedEndDate (" . (new DateTime($expectedEndDate))->format('l') . ")
";
// Sort by course priority
usort($selectedCourses, function ($a, $b) use ($priorityCourses) {
$priorityA = $priorityCourses[$a] ?? PHP_INT_MAX;
$priorityB = $priorityCourses[$b] ?? PHP_INT_MAX;
return $priorityA <=> $priorityB;
});
// echo "
π Sorted Courses (Priority First) "; print_r($selectedCourses); echo " ";
// Prepare for fair scheduling (one course per slot)
// Initialize
$schedule = [];
$remainingSessions = [];
foreach ($selectedCourses as $courseID) {
$remainingSessions[$courseID] = $courseSessions[$courseID] ?? 0;
}
// Queue of all courses
$courseQueue = $selectedCourses;
$currentCourses = [
0 => null, // Slot 1 (e.g. Morning)
1 => null // Slot 2 (e.g. Afternoon)
];
// Assign initial two courses
// Assign initial courses to slots without repeating
$courseQueueCopy = $selectedCourses;
$assignedCourses = [];
foreach ([0, 1] as $slotIndex) {
foreach ($courseQueueCopy as $courseID) {
if (!in_array($courseID, $assignedCourses) && $remainingSessions[$courseID] > 0) {
$currentCourses[$slotIndex] = $courseID;
$assignedCourses[] = $courseID;
break;
}
}
}
// Fetch all course names into array
$courseNames = [];
$courseRes = $mysqli->query("SELECT Course_ID, Course_Name FROM Courses");
while ($row = $courseRes->fetch_assoc()) {
$courseNames[$row['Course_ID']] = $row['Course_Name'];
}
// before you start scheduling:
$courseSlotMap = [];
// Start scheduling loop
// Start scheduling loop
foreach ($validDates as $date) {
$usedToday = []; // Reset per day
$dayName = (new DateTime($date))->format('l');
foreach ([0, 1] as $slotIndex) {
$from = $timeFrom[$slotIndex] ?? null;
$to = $timeTo[$slotIndex] ?? null;
// skip morning slot (index 0) if it's a retake day
if (isset($retakeDates[$date]) && $slotIndex === 0) {
continue;
}
if (!$from || !$to) continue;
// 1) Filter only courses eligible for this slot
$candidates = array_filter($selectedCourses, function($cid) use ($remainingSessions, $courseSlotMap, $slotIndex) {
// must still have sessions remaining
if (empty($remainingSessions[$cid])) {
return false;
}
// if we've already locked this course to the other slot, skip it
if (isset($courseSlotMap[$cid]) && $courseSlotMap[$cid] !== $slotIndex) {
return false;
}
return true;
});
// 2) Pick from that filtered list
$courseID = getNextCourseForSlot($remainingSessions, $candidates, $usedToday);
if (!$courseID) continue;
// 3) Lock it into this slot if it wasn't already
if (!isset($courseSlotMap[$courseID])) {
$courseSlotMap[$courseID] = $slotIndex;
}
$slotLabel = $slotLabels[$slotIndex];
$courseName = $courseNames[$courseID] ?? "Course #$courseID";
// 4) Save the scheduled session
//$schedule[] = [
// 'Date' => $date,
// 'Slot' => "Slot " . ($slotIndex + 1),
// 'Time' => format_time_slot_range($slotLabel, $from, $to, $dayName),
// 'Course_ID' => $courseID
//];
// 4) Save the scheduled session (with times + slot ID)
$schedule[] = [
'Date' => $date,
'Slot' => "Slot " . ($slotIndex + 1),
'Time' => format_time_slot_range($slotLabel, $from, $to, $dayName),
'Course_ID' => $courseID,
'Course_Name' => $courseNames[$courseID] ?? '', // β add this
'Start_Time' => $from,
'End_Time' => $to,
'Group_Slot_ID' => $group_slot_id[$slotIndex] ?? 0,
'Reserve_Course' => 0
];
// 5) Decrement and mark used
$remainingSessions[$courseID]--;
$usedToday[] = $courseID;
// 6) Stop if done
if (array_sum($remainingSessions) <= 0) {
break 2;
}
}
}
// π Identify last scheduled session for each course
$lastSessions = [];
foreach ($schedule as $index => $entry) {
$courseID = $entry['Course_ID'];
$lastSessions[$courseID] = $index; // keeps overwriting until the last index
}
//echo "
π Final Generated Schedule "; print_r($schedule); echo " ";
//echo "
π Generated Schedule Table ";
//echo "
Group: " . htmlspecialchars($groupName) . "
";
if (!empty($schedule)) {
// Collect all displayed dates: scheduled, holidays, and retakes
$displayDates = [];
// 1. From scheduled sessions
foreach ($schedule as $row) {
$displayDates[$row['Date']][] = $row; // grouped by date
}
// 2. Add holidays (if no course scheduled on them)
foreach ($holidayDates as $hDate => $title) {
if (!isset($displayDates[$hDate])) {
$displayDates[$hDate] = []; // Add empty row so we render it
}
}
// 3. Add retakes
//foreach ($retakeDates as $rDate => $type) {
// if (!isset($displayDates[$rDate])) {
// $displayDates[$rDate] = [];
// }
//}
// 3. Ensure every retake date has a morning placeholder
foreach ($retakeDates as $rDate => $type) {
// make sure the date key exists
if (!isset($displayDates[$rDate])) {
$displayDates[$rDate] = [];
}
// detect if we already have a slot-1 (afternoon) row but no slot-0
$hasMorning = false;
foreach ($displayDates[$rDate] as $row) {
if (isset($row['Slot']) && trim($row['Slot']) === 'Slot 1') {
$hasMorning = true;
break;
}
}
if (!$hasMorning) {
array_unshift($displayDates[$rDate], [
'Slot' => null,
'Time' => null,
'Course_ID' => null,
'IsRetake' => true // our flag
]);
}
}
// Sort the dates chronologically
ksort($displayDates);
} else {
echo "
π« No schedule generated β check if valid time slots, sessions, or dates are missing.
";
}
}
*/
?>
Generate Group Schedule
";
if (!empty($displayDates)) {
echo "";
// Fetch group info before rendering table
$stmt = $mysqli->prepare("SELECT Group_Name FROM Manager_Group_Name WHERE Group_ID = ?");
$stmt->bind_param("i", $Group_ID);
$stmt->execute();
$stmt->bind_result($groupName);
$stmt->fetch();
$stmt->close();
// Show group name before the table
echo "";
echo "
";
echo "
";
echo "Date Slot Time Course Action ";
$ajaxScheduleData = [];
foreach ($displayDates as $date => $rows) {
$dayName = (new DateTime($date))->format('l');
$isHoliday = array_key_exists($date, $holidayDates);
$isRetake = array_key_exists($date, $retakeDates);
// π‘ Case: No sessions on this day
if (empty($rows)) {
$label = "No Sessions";
$bg = "#F9F9F9";
if ($isHoliday) {
$label = "Holiday: " . htmlspecialchars($holidayDates[$date]);
$bg = "#FFD700";
} elseif ($isRetake) {
$label = "Retake: " . htmlspecialchars($retakeDates[$date]);
$bg = "#CCE5FF";
}
echo "
($dayName) $date
- - $label - ";
continue;
}
// ποΈ Loop through scheduled classes on that day
foreach ($rows as $row) {
// --- Determine slot name correctly ---
if (!empty($row['IsRetake'])) {
// For retakes, the Slot already contains the correct human label (e.g., "Evening")
$slotName = $row['Slot'] ?? '-';
} else if ($numSlots > 1) {
// For multi-slot, use the slot index to get the label
$slotIndex = (int) filter_var($row['Slot'], FILTER_SANITIZE_NUMBER_INT) - 1;
$slotName = isset($slotLabels[$slotIndex])
? format_time_slot_label($slotLabels[$slotIndex], $dayName)
: 'Unknown';
} else {
// For single slot, use the only slot label
$slotName = $slotLabels[0] ?? '-';
}
// --- Display row ---
if (!empty($row['IsRetake'])) {
echo "
($dayName) $date
{$slotName}
{$row['Time']}
" . htmlspecialchars($row['Retake_Type']) . "
-
";
continue;
}
//$slotIndex = (int) filter_var($row['Slot'], FILTER_SANITIZE_NUMBER_INT) - 1;
//$slotName = isset($slotLabels[$slotIndex])
// ? format_time_slot_label($slotLabels[$slotIndex], $dayName)
// : 'Unknown';
//
//if (!empty($row['IsRetake'])) {
// echo "
// ($dayName) $date
// {$slotName}
// {$row['Time']}
// " . htmlspecialchars($row['Retake_Type']) . "
// -
// ";
// continue;
//}
// βββ now your existing βrealβ row rendering βββ
$courseName = $courseNames[$row['Course_ID']] ?? "Course ID {$row['Course_ID']}";
// More reliable way to find session index
$index = null;
foreach ($schedule as $i => $entry) {
if (
$entry['Course_ID'] === $row['Course_ID'] &&
$entry['Date'] === $row['Date'] &&
$entry['Slot'] === $row['Slot']
) {
$index = $i;
break;
}
}
$isExam = isset($lastSessions[$row['Course_ID']]) && $index === $lastSessions[$row['Course_ID']];
// Highlight
$style = "";
if ($isExam) {
$style = "style='background-color: #FFCCCC; font-weight: bold;'";
} elseif ($isHoliday) {
$style = "style='background-color: #FFF5D1;'";
} elseif ($isRetake) {
$style = "style='background-color: #D9F0FF;'";
}
$slotIndex = (int) filter_var($row['Slot'], FILTER_SANITIZE_NUMBER_INT) - 1;
$slotName = isset($slotLabels[$slotIndex])
? format_time_slot_label($slotLabels[$slotIndex], $dayName)
: 'Unknown';
$isSaturday = ($dayName === 'Saturday');
$actionBtn = $isSaturday
? "π§Ή Replace "
: "-";
$ajaxScheduleData[] = [
'program_id' => $Program_ID,
'course_id' => $row['Course_ID'],
'course_name' => $courseNames[$row['Course_ID']] ?? '',
'group_id' => $Group_ID,
'group_slot_id' => $row['Group_Slot_ID'],
'reserve_course' => $row['Reserve_Course'],
'time_slot' => $slotName,
'start_date' => $row['Date'],
'end_date' => $row['Date'],
'start_time' => $row['Start_Time'],
'end_time' => $row['End_Time']
];
echo "";
echo "($dayName) {$row['Date']} ";
echo "{$slotName} ";
echo "{$row['Time']} ";
echo "{$courseName}" . ($isExam ? " (Exam Day)" : "") . " ";
echo "{$actionBtn} ";
echo " ";
}
}
echo " Date Slot Time Course Action ";
echo "
πΎ Save Schedule
";
// *** Right here, after $ajaxScheduleData is fully built: ***
echo "\n";
}
?>