/home/techb158/workloadmatch.com/Manager
Edit: /home/techb158/workloadmatch.com/Manager/Create_Schedule_Algorithm_3.php (89594B)
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
);
//##################################################################
/* $onlyCourseIDLeft = (count($activeCourses) === 1)
? array_key_first($activeCourses)
: null;
$courseID = getNextCourseForSlot(
$remainingSessions,
array_keys($activeCourses),
$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
$forceSecondSpecialAfternoon = !empty($_POST['force_second_special_afternoon']);
$lastCourseBothSlots = !empty($_POST['last_course_both_slots']);
$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
/////######################################
/**
* Pre-schedules every special course into $schedule,
* tagging their last session as an exam.
*
* @param array &$schedule The schedule array to append into
* @param array $specialCourseIDs Ordered list of special course IDs
* @param array &$remainingSessions Course_ID => sessions remaining
* @param array $allSlots Flat list of slot metadata
* @param array $courseNames Course_ID => human name
*/
/**
* Pre-schedules every special course into $schedule,
* tagging their last session as an exam. Returns the last date/slot used.
*
* @return array [$lastDate, $lastSlotIndex]
*/
/**
* Pre-schedules every special course into $schedule,
* tagging their last session as an exam and forcing
* the final session of the 2nd & 4th specials into an afternoon slot.
*
* @return array [$lastDate, $lastSlotIndex]
*/
function scheduleSpecialCourses(
array &$schedule,
array $specialCourseIDs,
array &$remainingSessions,
array $allSlots,
array $courseNames,
bool $forceSecondSpecialAfternoon // ← here signature to accept the new flag
): array {
$spPtr = 0;
$lastSpecialDate = null;
$lastSpecialSlot = null;
foreach ($specialCourseIDs as $idx => $spCid) {
$toDo = $remainingSessions[$spCid] ?? 0;
while ($toDo > 0 && isset($allSlots[$spPtr])) {
$slot = $allSlots[$spPtr];
/* // Always force the last session (exam) into the Afternoon slot (slotIndex == 1)
if ($toDo === 1 && $slot['slotIndex'] === 0) {
// Search forward for afternoon slot on same day
for ($j = $spPtr; $j < count($allSlots); $j++) {
if (
$allSlots[$j]['date'] === $slot['date'] &&
$allSlots[$j]['slotIndex'] === 1
) {
$slot = $allSlots[$j];
$spPtr = $j;
break;
}
}
} */
// If user asked to force the *second* special into afternoon…
if (
$forceSecondSpecialAfternoon
&& $toDo === 1 // last session
&& $slot['slotIndex'] === 0
) {
// scan forward to same-day slotIndex=1
for ($j = $spPtr; $j < count($allSlots); $j++) {
if (
$allSlots[$j]['date'] === $slot['date']
&& $allSlots[$j]['slotIndex'] === 1
) {
$slot = $allSlots[$j];
$spPtr = $j + 1;
break;
}
}
}
$spPtr++;
if (!$slot['from'] || !$slot['to']) continue;
$isExam = ($toDo === 1);
$schedule[] = [
'Date' => $slot['date'],
'SlotIndex' => $slot['slotIndex'],
'Slot' => $slot['slotLabel'],
'Time' => format_time_slot_range(
$slot['slotLabel'],
$slot['from'],
$slot['to'],
$slot['dayName']
),
'Course_ID' => $spCid,
'Course_Name' => $courseNames[$spCid] ?? '',
'Start_Time' => $slot['from'],
'End_Time' => $slot['to'],
'Group_Slot_ID' => $slot['groupSlotId'],
'Reserve_Course' => 0,
'IsExam' => $isExam,
];
$lastSpecialDate = $slot['date'];
$lastSpecialSlot = $slot['slotIndex'];
$toDo--;
$remainingSessions[$spCid]--;
}
unset($remainingSessions[$spCid]);
}
return [$lastSpecialDate, $lastSpecialSlot];
}
// --- SPECIAL COURSES SCHEDULING --- (RUNS ONCE BEFORE main foreach)
// ------------------------------------------------------------------------
// ➊ Prepare your schedule array and remainingSessions as you already do…
$schedule = []; // clear any prior entries
// $remainingSessions is already set from your courseSessions logic
// ➀ Build $specialCourseIDs from your Courses table & specialCourseCodes
$specialCourseCodes = ['CST53001','961-012','SEC-CST53001','960-501'];
$specialCourseIDs = [];
$cRes = $mysqli->query("
SELECT Course_ID, Course_Code
FROM Courses
");
while ($r = $cRes->fetch_assoc()) {
if (in_array($r['Course_Code'], $specialCourseCodes)) {
$specialCourseIDs[] = $r['Course_ID'];
}
}
// Global queue for any course bumped by a retake
$pushQueue = [];
//##############################################################
//##############################################################
// ➁ Build $allSlots from $validDates and your slot/time data
$allSlots = [];
foreach ($validDates as $date) {
foreach (array_keys($slotLabels) as $si) {
$gid = $group_slot_id[$si] ?? 0;
if (!empty($retakeDates[$date][$gid]) || !empty($holidayDates[$date])) {
continue;
}
$allSlots[] = [
'date' => $date,
'slotIndex' => $si,
'slotLabel' => $slotLabels[$si],
'from' => $timeFrom[$si] ?? null,
'to' => $timeTo[$si] ?? null,
'groupSlotId'=> $gid,
'dayName' => (new DateTime($date))->format('l'),
];
}
}
// ➋ Front-load specials via the new function
// front-load all specials, capturing where they ended
list($lastSpecialDate, $lastSpecialSlot) = scheduleSpecialCourses(
$schedule,
$specialCourseIDs,
$remainingSessions,
$allSlots,
$courseNames, // ← pass this in now
$forceSecondSpecialAfternoon // ← here pass your flag
);
/////######################################
// Mark already-used date/slot pairs
$occupiedSlots = [];
foreach ($schedule as $s) {
$occupiedSlots[$s['Date']][$s['SlotIndex']] = true;
}
/////######################################
// --- 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 {
////##########################--- END SPECIAL SCHEDULING ---#################################
// Loop through each slot (e.g. morning/afternoon)
foreach (array_keys($slotLabels) as $slotIndex) {
// ────────────────────────────────────────────────────────────────────────────
// Only skip slots strictly before the exam date, or the morning of the exam date
// skip everything up through that exam‐morning
// Don't double-book: skip if already occupied by a special course
if (!empty($occupiedSlots[$date][$slotIndex])) {
continue;
}
// ─────────────────────────────────────────
// If we're down to one course, skip the morning slot…
/* $onlyCourseLeft = (count($activeCourses) === 1);
if ($onlyCourseLeft && $slotIndex === 0) {
continue;
} */
// Determine if we’re down to exactly one active course
$onlyCourseLeft = count($activeCourses) === 1;
// Only skip the morning slot *if* they did NOT check “both slots”
if (
!$lastCourseBothSlots
&& $onlyCourseLeft
&& $slotIndex === 0
) {
continue;
}
// ─────────────────────────────────────────
// Now do your normal multi-slot scheduling for regular courses!
$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,
'SlotIndex' => $slotIndex, // ← NEW
'Slot' => $slotLabel,
//'Slot' => "Slot " . ($slotIndex + 1),
'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;
}
if ($courseToPush !== null) {
$pushQueue[] = $courseToPush; // enqueue at tail
}
}
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; */
//##################################################################
// --- 3. NORMAL COURSE SCHEDULING ---
// ————— HERE: ONLY-ONE-COURSE-LEFT SKIP MORNING —————
/* $onlyCourseIDLeft = (count($activeCourses) === 1)
? array_key_first($activeCourses)
: null;
if ($onlyCourseIDLeft !== null && $slotIndex === 0) {
// we’re down to one course; skip the morning slot so it lands in the afternoon
continue;
}
// ————————————————————————————————————————————————
// --- 3. NORMAL COURSE SCHEDULING ---
$courseID = getNextCourseForSlot(
$remainingSessions,
array_keys($activeCourses),
$usedToday,
$courseCodes,
$onlyCourseIDLeft
);
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 --- We close it we need to show the last 2 courses one in the morning and other in afternoon
/* 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,
'SlotIndex' => $slotIndex, // ← NEW
//'Slot' => $slotLabels[$slotIndex], // e.g. "Morning" or "Afternoon"
'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 " "; */
/* echo "
";
foreach ($schedule as $row) {
echo "{$row['Date']} slotIndex={$row['SlotIndex']} course={$row['Course_ID']} IsExam=".($row['IsExam']?1:0)."\n";
}
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;
// instead of parsing numbers, just use the string
// $slotName = $row['Slot'];
$slotName = isset($slotLabels[$slotIndex])
? format_time_slot_label($slotLabels[$slotIndex], $dayName)
: 'Unknown';
} else {
// For single slot, use the only slot label
$slotName = $slotLabels[0] ?? '-';
}
// Universal slot label resolver
/* if (!empty($row['IsRetake'])) {
$slotName = $row['Slot'] ?? '-';
} else {
// If Slot is numeric (e.g., "Slot 1") use slotLabels, else show as is
if (preg_match('/^Slot (\d+)$/', $row['Slot'], $m)) {
$slotIdx = intval($m[1]) - 1;
$slotName = $slotLabels[$slotIdx] ?? $row['Slot'];
} else {
$slotName = $row['Slot']; // Already human label
}
} */
// --- 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';
if(!($dayName === 'Saturday'))
{
if (preg_match('/^Slot (\d+)$/', $row['Slot'], $m)) {
$slotIdx = intval($m[1]) - 1;
$slotName = $slotLabels[$slotIdx] ?? $row['Slot'];
} else {
$slotName = $row['Slot']; // Already human label (Morning, Saturday, etc)
}
}
// If Slot is numeric (e.g., "Slot 1") use slotLabels, else show as is
/* if (preg_match('/^Slot (\d+)$/', $row['Slot'], $m)) {
$slotIdx = intval($m[1]) - 1;
$slotName = $slotLabels[$slotIdx] ?? $row['Slot'];
} else {
$slotName = $row['Slot']; // Already human label
} */
// for both single- and multi-slot, trust the Slot label you already set
//$slotName = $row['Slot'];
$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";
}
?>