Files
transfer.tapee.ac.th/includes/functions.php
T
pongpon pitisuk 72701ad58c Initial import
2026-06-25 16:49:13 +07:00

742 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../db.php';
function getCurricula() {
return getDB()->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 .= '<option value="' . $g['id'] . '"' . $sel . '>' . $prefix . htmlspecialchars($g['name_th']) . '</option>';
$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 .= '<option value="' . $g . '"' . $sel . '>' . $g . '</option>';
}
return $html;
}
function getCourseTypeOptions($selected = null) {
$types = [
'เรียนปกติ' => 'เรียนปกติ',
'เทียบโอนรายวิชา' => 'เทียบโอนรายวิชา',
'เทียบโอนกลุ่มวิชา' => 'เทียบโอนกลุ่มวิชา',
'กิจกรรม/สหกิจศึกษา' => 'กิจกรรม/สหกิจศึกษา',
'ฝึกอบรม' => 'ฝึกอบรม',
];
$html = '';
foreach ($types as $val => $label) {
$sel = $val === $selected ? ' selected' : '';
$html .= '<option value="' . $val . '"' . $sel . '>' . $label . '</option>';
}
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
];
}