query("SELECT * FROM curricula ORDER BY code")->fetchAll(); } function getCurriculum($id) { $stmt = getDB()->prepare("SELECT * FROM curricula WHERE id = ?"); $stmt->execute([$id]); return $stmt->fetch(); } function getCourseGroups($curriculum_id, $parent_id = null) { if ($parent_id === null) { $stmt = getDB()->prepare("SELECT * FROM course_groups WHERE curriculum_id = ? AND parent_id IS NULL ORDER BY sort_order"); $stmt->execute([$curriculum_id]); } else { $stmt = getDB()->prepare("SELECT * FROM course_groups WHERE parent_id = ? ORDER BY sort_order"); $stmt->execute([$parent_id]); } return $stmt->fetchAll(); } function getCoursesByGroup($group_id) { $stmt = getDB()->prepare("SELECT * FROM courses WHERE group_id = ? ORDER BY sort_order"); $stmt->execute([$group_id]); return $stmt->fetchAll(); } function getStudents($curriculum_id = null) { if ($curriculum_id === null && isStudent()) { $stmt = getDB()->prepare(" SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM students s JOIN curricula c ON s.curriculum_id = c.id WHERE s.id = ? ORDER BY s.student_code "); $stmt->execute([getCurrentUserId()]); return $stmt->fetchAll(); } if ($curriculum_id === null && isAdvisor()) { $stmt = getDB()->prepare(" SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM students s JOIN curricula c ON s.curriculum_id = c.id WHERE s.advisor_id = ? ORDER BY s.student_code "); $stmt->execute([getCurrentUserId()]); return $stmt->fetchAll(); } if ($curriculum_id) { $stmt = getDB()->prepare(" SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM students s JOIN curricula c ON s.curriculum_id = c.id WHERE s.curriculum_id = ? ORDER BY s.student_code "); $stmt->execute([$curriculum_id]); return $stmt->fetchAll(); } return getDB()->query(" SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM students s JOIN curricula c ON s.curriculum_id = c.id ORDER BY s.student_code ")->fetchAll(); } function getStudent($id) { $stmt = getDB()->prepare(" SELECT s.*, c.code AS curriculum_code, c.name_th AS curriculum_name, c.total_credits FROM students s JOIN curricula c ON s.curriculum_id = c.id WHERE s.id = ? "); $stmt->execute([$id]); return $stmt->fetch(); } function getFacultyList() { return [ '12' => 'คณะนวัตกรรมดิจิทัลเทคโนโลยี', '11' => 'คณะบริหารธุรกิจ', '13' => 'คณะศึกษาศาสตร์และศิลปศาสตร์', '14' => 'คณะบัญชี', '15' => 'คณะนิติศาสตร์และรัฐศาสตร์', ]; } function getTransferCreditsTotal($student_id) { $stmt = getDB()->prepare("SELECT COALESCE(SUM(credits), 0) FROM student_courses WHERE student_id = ? AND source_type = 'transfer'"); $stmt->execute([$student_id]); return floatval($stmt->fetchColumn()); } function getCompletedCreditsTotal($student_id) { $stmt = getDB()->prepare("SELECT COALESCE(SUM(credits), 0) FROM student_courses WHERE student_id = ?"); $stmt->execute([$student_id]); return floatval($stmt->fetchColumn()); } function getStudentCourses($student_id) { $stmt = getDB()->prepare(" SELECT sc.*, c.code AS curriculum_course_code FROM student_courses sc LEFT JOIN courses c ON sc.course_id = c.id WHERE sc.student_id = ? ORDER BY sc.course_code "); $stmt->execute([$student_id]); return $stmt->fetchAll(); } function getStudentCoursesByGroup($student_id, $group_id) { $stmt = getDB()->prepare(" SELECT sc.* FROM student_courses sc JOIN courses c ON sc.course_id = c.id WHERE sc.student_id = ? AND c.group_id = ? "); $stmt->execute([$student_id, $group_id]); return $stmt->fetchAll(); } /** * Build recursive tree of course groups for a curriculum */ function buildGroupTree($curriculum_id, $parent_id = null) { $groups = getCourseGroups($curriculum_id, $parent_id); $tree = []; foreach ($groups as $g) { $children = buildGroupTree($curriculum_id, $g['id']); $courses = getCoursesByGroup($g['id']); $node = [ 'group' => $g, 'children' => $children, 'courses' => $courses, 'total_credits' => 0, 'completed_credits' => 0, ]; // Calculate total credits from courses foreach ($courses as $c) { $node['total_credits'] += $c['credits']; } // Add children totals foreach ($children as $ch) { $node['total_credits'] += $ch['total_credits']; } $tree[] = $node; } return $tree; } function getStudentSummary($student_id) { $student = getStudent($student_id); if (!$student) return null; $tree = buildGroupTree($student['curriculum_id']); $student_courses = getStudentCourses($student_id); $completed_map = []; foreach ($student_courses as $sc) { $completed_map[$sc['course_code']] = $sc; } // Mark completed courses recursively $total_completed = 0; foreach ($student_courses as $sc) { $total_completed += $sc['credits']; } $markCompleted = function(&$nodes) use (&$markCompleted, $completed_map) { foreach ($nodes as &$node) { foreach ($node['courses'] as &$course) { if (isset($completed_map[$course['code']])) { $course['_completed'] = $completed_map[$course['code']]; $node['completed_credits'] += $course['credits']; } } if (!empty($node['children'])) { $markCompleted($node['children']); // Sum children completed credits foreach ($node['children'] as $ch) { $node['completed_credits'] += $ch['completed_credits']; } } } }; $markCompleted($tree); return [ 'student' => $student, 'groups' => $tree, 'total_completed' => $total_completed, 'student_courses' => $student_courses, 'completed_map' => $completed_map ]; } function findCourseInCurriculum($curriculum_id, $code) { $stmt = getDB()->prepare(" SELECT c.* FROM courses c JOIN course_groups cg ON c.group_id = cg.id WHERE cg.curriculum_id = ? AND c.code = ? "); $stmt->execute([$curriculum_id, $code]); return $stmt->fetch(); } function getGroupSelectOptions($curriculum_id, $selected_id = null, $parent_id = null, $prefix = '') { $groups = getCourseGroups($curriculum_id, $parent_id); $html = ''; foreach ($groups as $g) { $sel = $g['id'] == $selected_id ? ' selected' : ''; $html .= ''; $html .= getGroupSelectOptions($curriculum_id, $selected_id, $g['id'], $prefix . '— '); } return $html; } function getGradeOptions($selected = null) { $grades = ['A', 'B+', 'B', 'C+', 'C', 'D+', 'D', 'F', 'S', 'U', 'กำลังเรียน']; $html = ''; foreach ($grades as $g) { $sel = $g === $selected ? ' selected' : ''; $html .= ''; } return $html; } function getCourseTypeOptions($selected = null) { $types = [ 'เรียนปกติ' => 'เรียนปกติ', 'เทียบโอนรายวิชา' => 'เทียบโอนรายวิชา', 'เทียบโอนกลุ่มวิชา' => 'เทียบโอนกลุ่มวิชา', 'กิจกรรม/สหกิจศึกษา' => 'กิจกรรม/สหกิจศึกษา', 'ฝึกอบรม' => 'ฝึกอบรม', ]; $html = ''; foreach ($types as $val => $label) { $sel = $val === $selected ? ' selected' : ''; $html .= ''; } return $html; } function getTotalCurriculumCredits($curriculum_id) { $stmt = getDB()->prepare("SELECT total_credits FROM curricula WHERE id = ?"); $stmt->execute([$curriculum_id]); $r = $stmt->fetch(); return $r ? $r['total_credits'] : 0; } // ============================================================ // Access Control Helpers // ============================================================ function requireStudentAccess($student_id) { if (isAdmin()) return; if (isStudent() && getCurrentUserId() == $student_id) return; $db = getDB(); $stmt = $db->prepare("SELECT advisor_id, curriculum_id FROM students WHERE id = ?"); $stmt->execute([$student_id]); $student = $stmt->fetch(); if (!$student || $student['advisor_id'] != getCurrentUserId()) { header('Location: ' . BASE_URL . '/index.php'); exit; } } // ============================================================ // User Management Functions // ============================================================ function getUsers() { return getDB()->query("SELECT u.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id ORDER BY u.staff_code")->fetchAll(); } function getAdvisors() { return getDB()->query("SELECT u.id, u.name, u.staff_code, c.code AS curriculum_code FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id WHERE u.role = 'advisor' ORDER BY u.name")->fetchAll(); } function getUser($id) { $stmt = getDB()->prepare("SELECT u.*, c.code AS curriculum_code, c.name_th AS curriculum_name FROM users u LEFT JOIN curricula c ON u.curriculum_id = c.id WHERE u.id = ?"); $stmt->execute([$id]); return $stmt->fetch(); } function authenticateUser($staff_code, $password) { $stmt = getDB()->prepare("SELECT * FROM users WHERE staff_code = ?"); $stmt->execute([$staff_code]); $user = $stmt->fetch(); if ($user && password_verify($password, $user['password'])) { return $user; } return false; } // ============================================================ // PDF Parsing Functions // ============================================================ function extractTextFromPDF($filepath) { $text = trySmalotParser($filepath); if (!empty(trim($text))) return $text; $text = tryPdftotext($filepath); if (!empty(trim($text))) return $text; $text = tryBasicPdfParse($filepath); if (!empty(trim($text))) return $text; return tryRawPdfRead($filepath); } function trySmalotParser($filepath) { if (!class_exists('Smalot\\PdfParser\\Parser')) { spl_autoload_register(function ($class) { $prefix = 'Smalot\\PdfParser\\'; $base_dir = __DIR__ . '/../vendor/smalot/pdfparser/src/'; if (strncmp($prefix, $class, strlen($prefix)) === 0) { $file = $base_dir . str_replace('\\', '/', $class) . '.php'; if (file_exists($file)) { require $file; } } }); } try { $parser = new Smalot\PdfParser\Parser(); $pdf = $parser->parseFile($filepath); return $pdf->getText(); } catch (Throwable $e) { return ''; } } function tryPdftotext($filepath) { if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $exe_paths = [ 'C:\xampp\htdocs\Transfer\vendor\pdftotext.exe', 'pdftotext', 'C:\Program Files\pdftotext\pdftotext.exe', ]; } else { $exe_paths = [ 'pdftotext', '/usr/bin/pdftotext' ]; } foreach ($exe_paths as $exe) { $output = []; $cmd = escapeshellcmd($exe) . ' ' . escapeshellarg($filepath) . ' -'; @exec($cmd, $output, $return_var); if ($return_var === 0 && !empty(implode('', $output))) { return implode("\n", $output); } } return ''; } function tryBasicPdfParse($filepath) { $content = file_get_contents($filepath); if ($content === false) return ''; $text = ''; $offset = 0; while (($start = strpos($content, 'stream', $offset)) !== false) { $stream_start = $start + 6; // Skip leading whitespace (usually CR/LF or LF) after 'stream' while ($stream_start < strlen($content) && ctype_space($content[$stream_start])) { $stream_start++; } $end = strpos($content, 'endstream', $stream_start); if ($end === false) { break; } $stream = substr($content, $stream_start, $end - $stream_start); $stream = rtrim($stream); $decoded = @gzuncompress($stream); if ($decoded === false) { $decoded = @gzuncompress(trim($stream)); } if ($decoded !== false) { if (strlen($decoded) > 100000) continue; preg_match_all('/\((.*?)\)\s*Tj/s', $decoded, $text_matches); $text .= implode(' ', $text_matches[1]) . "\n"; preg_match_all('/\[(.*?)\]\s*TJ/s', $decoded, $tj_matches); foreach ($tj_matches[1] as $tj) { preg_match_all('/\((.*?)\)/', $tj, $tj_text); $text .= implode('', $tj_text[1]) . ' '; } } $offset = $end + 9; } return $text; } function tryRawPdfRead($filepath) { $content = file_get_contents($filepath); if ($content === false) return ''; // Cap binary parsing to prevent CPU exhaustion on very large or corrupt files if (strlen($content) > 500000) { $content = substr($content, 0, 500000); } $text = ''; $len = strlen($content); $in_paren = false; $buffer = ''; for ($i = 0; $i < $len; $i++) { $char = $content[$i]; if ($char === '(') { $in_paren = true; $buffer = ''; } else if ($char === ')') { if ($in_paren) { if (strlen($buffer) > 0 && strlen($buffer) <= 200) { $text .= $buffer . "\n"; } $in_paren = false; } } else if ($in_paren) { $buffer .= $char; if (strlen($buffer) > 200) { $in_paren = false; } } } return $text; } function parseCoursesFromText($text) { $courses = []; // Try Pattern A: 9-digit code (transfer PDF format with multi-line text) // Strip first 2 digits for 7-digit curriculum code $courses = parseTransferPdfCourses($text); // Fallback: Standard 7-digit code format if (empty($courses)) { $courses = parseStandardCourses($text); } return $courses; } function parseTransferPdfCourses($text) { $courses = []; $lines = explode("\n", $text); $lines = array_map(function($l) { return str_replace("\t", " ", trim($l)); }, $lines); $lines = array_values(array_filter($lines, function($l) { return $l !== ''; })); $current_code = ''; $current_name_parts = []; $flushCourse = function() use (&$current_code, &$current_name_parts, &$courses) { if ($current_code && !empty($current_name_parts)) { $courses[] = finalizeTransferCourse($current_code, $current_name_parts); $current_code = ''; $current_name_parts = []; } }; for ($i = 0; $i < count($lines); $i++) { $line = $lines[$i]; // Single regex: optional seq# (1-2 digits with optional trailing space) + 9-digit code + rest $m = []; if (preg_match('/^(\d{1,2})?\s*(\d{9})(.*)$/', $line, $m)) { $pdf_code = $m[2]; $after_code = trim($m[3]); if ($current_code) { $courses[] = finalizeTransferCourse($current_code, $current_name_parts); $current_code = ''; $current_name_parts = []; } $current_code = $pdf_code; $current_name_parts = []; if ($after_code !== '') { if (preg_match('/^(.+?)\s+(\d+\.\d)\s*$/', $after_code, $nm)) { $current_name_parts[] = trim($nm[1]); $courses[] = finalizeTransferCourse($current_code, $current_name_parts, floatval($nm[2])); $current_code = ''; $current_name_parts = []; } else { $current_name_parts[] = $after_code; } } continue; } if ($current_code) { if (preg_match('/^(\d+\.\d)\s*$/', $line, $cm)) { $courses[] = finalizeTransferCourse($current_code, $current_name_parts, floatval($cm[1])); $current_code = ''; $current_name_parts = []; continue; } if (preg_match('/^(.+?)\s+(\d+\.\d)\s*$/', $line, $nm)) { $current_name_parts[] = trim($nm[1]); $courses[] = finalizeTransferCourse($current_code, $current_name_parts, floatval($nm[2])); $current_code = ''; $current_name_parts = []; continue; } $current_name_parts[] = $line; } } if ($current_code && !empty($current_name_parts)) { $courses[] = finalizeTransferCourse($current_code, $current_name_parts); } return $courses; } function finalizeTransferCourse($pdf_code, $name_parts, $credits = null) { // Clean the name: remove extra whitespace, collapsed newlines $name = preg_replace('/\s+/', ' ', implode(' ', $name_parts)); $name = trim($name); // Remove trailing orphan characters like "-" $name = rtrim($name, ' -'); // Strip first 2 digits from 9-digit code to get 7-digit curriculum code $curriculum_code = substr($pdf_code, 2); // If credits not specified, try to find a number in the name if ($credits === null) { if (preg_match('/\b(\d+\.\d)\b/', $name, $cm)) { $credits = floatval($cm[1]); $name = trim(str_replace($cm[0], '', $name)); } else { $credits = 3.0; // default } } return [ 'code' => $curriculum_code, 'name_th' => $name, 'credits' => $credits, 'lecture' => 0, 'practice' => 0, 'self_study' => 0, ]; } function parseStandardCourses($text) { $courses = []; // Pattern: Standard format "0001101 จิตวิทยาทั่วไป 3(3-0-6)" preg_match_all('/(\d{7})\s+([^\d]+?)\s+(\d+(?:\.\d)?)\s*\(\s*(\d+)\s*[-–]\s*(\d+)\s*[-–]\s*(\d+)\s*\)/', $text, $matches, PREG_SET_ORDER); foreach ($matches as $m) { $courses[] = [ 'code' => trim($m[1]), 'name_th' => trim($m[2]), 'credits' => floatval($m[3]), 'lecture' => intval($m[4]), 'practice' => intval($m[5]), 'self_study' => intval($m[6]), ]; } // Pattern: Simple format "0001101 จิตวิทยาทั่วไป 3" if (empty($courses)) { preg_match_all('/(\d{7})\s+([^\d]{2,80}?)\s+(\d+(?:\.\d)?)(?:\s|$)/', $text, $matches, PREG_SET_ORDER); foreach ($matches as $m) { $courses[] = [ 'code' => trim($m[1]), 'name_th' => trim($m[2]), 'credits' => floatval($m[3]), 'lecture' => 0, 'practice' => 0, 'self_study' => 0, ]; } } return $courses; } function getStudentTransferPdfPath($student_id) { $stmt = getDB()->prepare("SELECT DISTINCT source_file FROM student_courses WHERE student_id = ? AND source_type = 'transfer' AND source_file IS NOT NULL LIMIT 1"); $stmt->execute([$student_id]); $filename = $stmt->fetchColumn(); if ($filename) { return __DIR__ . '/../uploads/' . $filename; } return null; } function parseTransferReportFromPdf($filepath) { if (!$filepath || !file_exists($filepath)) return null; // Register autoloader for Smalot Parser just in case it is not registered yet spl_autoload_register(function ($class) { $prefix = 'Smalot\\PdfParser\\'; $base_dir = __DIR__ . '/../vendor/smalot/pdfparser/src/'; if (strncmp($prefix, $class, strlen($prefix)) === 0) { $file = $base_dir . str_replace('\\', '/', $class) . '.php'; if (file_exists($file)) { require $file; } } }); $text = extractTextFromPDF($filepath); $lines = explode("\n", $text); $pattern = '/^(?:(\d+)\s+)?(\d*)(\d{9})\s*(.+?)\s+(\d+\.\d)\s+(\d{5}-\d{4})\s*(.+?)\s+(\d+(?:\.\d)?)\s+([A-F][+-]?|[0-4](?:\.[0-9])?|S|U)\s*(\d+(?:\.\d+)?%)/u'; $source_pattern = '/^(\d{5}-\d{4})\s*(.+?)\s+(\d+(?:\.\d)?)\s+([A-F][+-]?|[0-4](?:\.[0-9])?|S|U)\s*(\d+(?:\.\d+)?%)/u'; $target_only_pattern = '/^(?:(\d+)\s+)?(\d*)(\d{9})\s*(.+?)\s+(\d+\.\d)\s*$/u'; $untransferable_pattern = '/^(?:(\d+)\s+)?(\d*)(\d{5}-\d{4})\s*(.+?)\s+(\d+(?:\.\d)?)\s+([A-F][+-]?|[0-4](?:\.[0-9])?|[ก-๙]\.?|S|U|W|I)\s*(.+)$/u'; $results = []; $untransferable = []; $start_transfer = false; $start_untransfer = false; foreach ($lines as $line) { $line = trim($line); if ($line === '') continue; if (mb_strpos($line, 'รายละเอียดวิชาที่เทียบโอนได้') !== false) { $start_transfer = true; $start_untransfer = false; continue; } if (mb_strpos($line, 'วิชาต้นทางที่ไม่สามารถเทียบโอนได้') !== false) { $start_transfer = false; $start_untransfer = true; continue; } if (mb_strpos($line, 'ผู้ประเมิน') !== false || mb_strpos($line, '(............................................)') !== false) { $start_transfer = false; $start_untransfer = false; break; } if ($start_transfer) { if (preg_match($pattern, $line, $m)) { $seq = !empty($m[1]) ? $m[1] : (!empty($m[2]) ? $m[2] : (count($results) + 1)); $target_code = $m[3]; $target_name = trim($m[4]); $target_credits = floatval($m[5]); $source_code = $m[6]; $source_name = trim($m[7]); $source_credits = floatval($m[8]); $source_grade = $m[9]; $similarity = $m[10]; $results[] = [ 'seq' => $seq, 'target_code' => $target_code, 'target_name' => $target_name, 'target_credits' => $target_credits, 'sources' => [ [ 'code' => $source_code, 'name' => $source_name, 'credits' => $source_credits, 'grade' => $source_grade, 'similarity' => $similarity ] ] ]; } else if (preg_match($target_only_pattern, $line, $m)) { $seq = !empty($m[1]) ? $m[1] : (!empty($m[2]) ? $m[2] : (count($results) + 1)); $target_code = $m[3]; $target_name = trim($m[4]); $target_credits = floatval($m[5]); $results[] = [ 'seq' => $seq, 'target_code' => $target_code, 'target_name' => $target_name, 'target_credits' => $target_credits, 'sources' => [] ]; } else if (preg_match($source_pattern, $line, $m) && !empty($results)) { $source_code = $m[1]; $source_name = trim($m[2]); $source_credits = floatval($m[3]); $source_grade = $m[4]; $similarity = $m[5]; $results[count($results) - 1]['sources'][] = [ 'code' => $source_code, 'name' => $source_name, 'credits' => $source_credits, 'grade' => $source_grade, 'similarity' => $similarity ]; } } else if ($start_untransfer) { if (preg_match($untransferable_pattern, $line, $m)) { $seq = !empty($m[1]) ? $m[1] : (!empty($m[2]) ? $m[2] : (count($untransferable) + 1)); $source_code = $m[3]; $source_name = trim($m[4]); $source_credits = floatval($m[5]); $source_grade = $m[6]; $reason = trim($m[7]); $untransferable[] = [ 'seq' => $seq, 'code' => $source_code, 'name' => $source_name, 'credits' => $source_credits, 'grade' => $source_grade, 'reason' => $reason ]; } } } return [ 'transfers' => $results, 'untransferable' => $untransferable ]; }