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
];
}