/home/techb158/workloadmatch.com/Manager/Inc
Edit: /home/techb158/workloadmatch.com/Manager/Inc/AIScheduleOptimizer.php (11919B)
db = $db;
$this->engine = new ScheduleEngine($db);
$this->generator = new ScheduleGenerator($db);
$this->provider = $provider;
}
public function setProvider(ProviderInterface $provider): void {
$this->provider = $provider;
}
public function hasProvider(): bool {
return $this->provider !== null;
}
public function getLastResponse(): array {
return $this->lastResponse;
}
public function optimize(int $programId, int $groupId, string $startDate, ?string $endDate = null, array $selectedCourses = [], array $priorityCourses = []): array {
$group = $this->engine->getGroupInfo($groupId);
if (!$group) {
throw new RuntimeException("Group not found: $groupId");
}
$courseSessions = $this->buildCourseSessionData($programId, $groupId);
$teacherData = $this->buildTeacherData($programId);
$constraints = $this->buildConstraints($programId, $groupId, $startDate, $group);
if ($this->provider) {
try {
return $this->optimizeWithAI($programId, $groupId, $startDate, $endDate, $selectedCourses, $priorityCourses, $courseSessions, $teacherData, $constraints, $group);
} catch (\Throwable $e) {
error_log("AI optimization failed, falling back to deterministic: " . $e->getMessage());
return $this->fallbackGenerate($programId, $groupId, $startDate, $endDate, $selectedCourses, $priorityCourses);
}
}
return $this->fallbackGenerate($programId, $groupId, $startDate, $endDate, $selectedCourses, $priorityCourses);
}
private function buildCourseSessionData(int $programId, int $groupId): array {
$group = $this->engine->getGroupInfo($groupId);
$slotLabels = explode(',', $group['Time_Slot']);
$managerStart = new DateTime(trim($group['Time_From']));
$managerEnd = new DateTime(trim($group['Time_To']));
[$timeFrom, $timeTo] = $this->engine->fetchSlotTimes($slotLabels, $managerStart, $managerEnd);
$sessionLength = $this->engine->calculateSessionLength($timeFrom, $timeTo);
$sessions = $this->engine->calculateCourseSessions($programId, $sessionLength);
$names = $this->engine->getCourseNames();
$result = [];
foreach ($sessions as $courseId => $count) {
$result[] = [
'course_id' => $courseId,
'course_name' => $names[$courseId] ?? "Course #$courseId",
'sessions' => $count,
];
}
return $result;
}
private function buildTeacherData(int $programId): array {
$stmt = $this->db->prepare("
SELECT tp.Teacher_ID, tp.First_Name, tp.Last_Name, tp.Seniority_ID,
tp.Time_Slot AS Availability, tp.Load_Hours,
ttl.Top_Teacher_ID
FROM teacher_profile tp
LEFT JOIN top_teacher_list ttl ON tp.Teacher_ID = ttl.Teacher_ID AND ttl.Program_ID = ?
");
$stmt->bind_param('i', $programId);
$stmt->execute();
$teachers = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt->close();
foreach ($teachers as &$t) {
$stmt = $this->db->prepare("
SELECT Unavailable_From, Unavailable_To
FROM teacher_unavailability WHERE Teacher_ID = ? AND status = 'confirmed'
");
$stmt->bind_param('i', $t['Teacher_ID']);
$stmt->execute();
$t['unavailability'] = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt->close();
$stmt = $this->db->prepare("
SELECT Course_ID FROM teacher_course_preferences
WHERE Teacher_ID = ? AND Priority IN (1,2)
");
$stmt->bind_param('i', $t['Teacher_ID']);
$stmt->execute();
$t['preferred_courses'] = array_column($stmt->get_result()->fetch_all(MYSQLI_ASSOC), 'Course_ID');
$stmt->close();
}
unset($t);
return $teachers;
}
private function buildConstraints(int $programId, int $groupId, string $startDate, array $group): array {
$classDays = explode(',', $group['class_days']);
if (!$group['Weekend_Class'] && in_array('Saturday', $classDays)) {
$classDays = array_filter($classDays, fn($d) => $d !== 'Saturday');
}
return [
'class_days' => array_values($classDays),
'time_slots' => explode(',', $group['Time_Slot']),
'time_from' => $group['Time_From'],
'time_to' => $group['Time_To'],
'holidays' => $this->engine->getHolidays($startDate),
'retake_dates' => $this->engine->getRetakeDates($programId, $groupId),
'exclude_month' => 7,
];
}
private function optimizeWithAI(int $programId, int $groupId, string $startDate, ?string $endDate, array $selectedCourses, array $priorityCourses, array $courseSessions, array $teacherData, array $constraints, array $group): array {
$slotCount = is_array($group['Time_Slot']) ? count($group['Time_Slot']) : count(explode(',', $group['Time_Slot']));
$systemPrompt = <<
"#$id", $t['preferred_courses']);
$teacherList[] = "- Teacher #{$t['Teacher_ID']} ({$t['First_Name']} {$t['Last_Name']}), level: {$level}, load: {$t['Load_Hours']}h, slots: {$t['Availability']}"
. (!empty($unavail) ? ", unavailable: " . implode('; ', $unavail) : "")
. (!empty($prefs) ? ", prefers courses: " . implode(', ', $prefs) : "");
}
$holidayStr = !empty($constraints['holidays']) ? implode(', ', $constraints['holidays']) : 'none';
$retakeStr = !empty($constraints['retake_dates']) ? implode(', ', $constraints['retake_dates']) : 'none';
$endDateStr = $endDate ?? date('Y-m-d', strtotime('+1 year', strtotime($startDate)));
$userPrompt = <<implode($constraints['class_days'])}
Time slots: {$this->implode($constraints['time_slots'])} ({$constraints['time_from']} - {$constraints['time_to']})
Holidays (no classes): {$holidayStr}
Retake dates (no classes): {$retakeStr}
No classes in July.
Courses to schedule:
{$this->implode($courseList, "\n")}
Available teachers:
{$this->implode($teacherList, "\n")}
Respond with JSON exactly in this format:
{
"schedule": [
{
"date": "YYYY-MM-DD",
"slot": "Morning",
"course_id": 101,
"teacher_id": 5,
"is_exam": false
}
],
"summary": {
"total_sessions": 50,
"total_days_used": 25,
"estimated_end_date": "YYYY-MM-DD"
}
}
PROMPT;
$this->lastResponse = $this->provider->sendPrompt($systemPrompt, $userPrompt);
return $this->parseAIResponse($this->lastResponse, $programId, $groupId);
}
private function parseAIResponse(array $response, int $programId, int $groupId): array {
$schedule = $response['schedule'] ?? $response['sessions'] ?? $response['assignments'] ?? [];
if (empty($schedule) || !is_array($schedule)) {
throw new RuntimeException("AI returned no valid schedule entries");
}
$courseNames = $this->engine->getCourseNames();
$parsed = [];
$displayDates = [];
foreach ($schedule as $entry) {
$date = $entry['date'] ?? $entry['Date'] ?? '';
$slot = $entry['slot'] ?? $entry['Slot'] ?? $entry['time_slot'] ?? '';
$courseId = $entry['course_id'] ?? $entry['Course_ID'] ?? $entry['courseId'] ?? 0;
$time = $entry['time'] ?? $entry['Time'] ?? '';
$isExam = !empty($entry['is_exam']) || !empty($entry['Is_Exam']);
if (!$date || !$courseId) continue;
$dayName = (new DateTime($date))->format('l');
$slotLabel = $this->engine->formatTimeSlotLabel($slot, $dayName);
if (!$time) {
$time = $this->engine->formatTimeSlotRange($slot, '08:30:00', '12:30:00', $dayName);
}
$entryData = [
'Date' => $date,
'Slot' => $slotLabel,
'Time' => $time,
'Course_ID' => (int)$courseId,
];
$parsed[] = $entryData;
$displayDates[$date][] = $entryData;
}
ksort($displayDates);
return [
'schedule' => $parsed,
'displayDates' => $displayDates,
'courseNames' => $courseNames,
'lastSessions' => $this->computeLastSessions($parsed),
'ai_reasoning' => $response['summary'] ?? $response['reasoning'] ?? null,
'ai_generated' => true,
];
}
private function computeLastSessions(array $schedule): array {
$last = [];
foreach ($schedule as $i => $entry) {
$last[$entry['Course_ID']] = $i;
}
return $last;
}
private function fallbackGenerate(int $programId, int $groupId, string $startDate, ?string $endDate, array $selectedCourses, array $priorityCourses): array {
$result = $this->generator->generate($programId, $groupId, $startDate, $endDate, $selectedCourses, $priorityCourses);
$result['ai_generated'] = false;
return $result;
}
private function implode(array $items, string $glue = ', '): string {
return implode($glue, $items);
}
}