Initial commit - TPU Survey System with Google Forms & Certificate Generator

This commit is contained in:
pongpon pitisuk
2026-09-12 11:56:34 +07:00
commit 72eb7cd0c8
355 changed files with 117012 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* Authentication API
* POST ?action=login — Login
* POST ?action=logout — Logout
* GET ?action=check — Check session
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? $_POST['action'] ?? '';
switch ($action) {
case 'login':
handleLogin();
break;
case 'logout':
handleLogout();
break;
case 'check':
handleCheck();
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function handleLogin() {
$input = getInput();
$username = trim($input['username'] ?? '');
$password = trim($input['password'] ?? '');
if (empty($username) || empty($password)) {
jsonResponse(['error' => 'กรุณากรอกชื่อผู้ใช้และรหัสผ่าน'], 400);
}
$db = getDB();
// Try login by username
$stmt = $db->prepare("SELECT * FROM users WHERE username = ? OR personnel_code = ? LIMIT 1");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
jsonResponse(['error' => 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง'], 401);
}
// Set session
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['full_name'] = $user['full_name'];
jsonResponse([
'success' => true,
'user' => [
'id' => $user['id'],
'username' => $user['username'],
'role' => $user['role'],
'full_name' => $user['full_name']
]
]);
}
function handleLogout() {
session_destroy();
jsonResponse(['success' => true, 'message' => 'ออกจากระบบสำเร็จ']);
}
function handleCheck() {
if (isLoggedIn()) {
jsonResponse([
'loggedIn' => true,
'user' => getCurrentUser()
]);
} else {
jsonResponse(['loggedIn' => false]);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* Certificate Generator Helper using TCPDF
* TPU Survey System
*/
require_once __DIR__ . '/../vendor/autoload.php';
/**
* Generates a PDF certificate with the recipient's name
*
* @param string $name Name to print on the certificate
* @param string $outputPath Path to save the generated PDF. If empty, returns raw PDF string.
* @param int $yPos Vertical offset (in points) where the name should be drawn
* @return string Path to the file, or raw PDF string
*/
function generateCertificate($name, $outputPath = '', $yPos = 82.5, $certCode = '', $bgImage = '', $codeY = 190.5) {
// สร้างเอกสาร PDF ขนาดมาตรฐาน A4 แนวนอน (297 x 210 มิลลิเมตร)
$pdf = new TCPDF('L', 'mm', 'A4', true, 'UTF-8', false);
// Set document information
$pdf->SetCreator(PDF_CREATOR);
$pdf->SetAuthor('TPU Survey System');
$pdf->SetTitle('เกียรติบัตร - ' . $name);
$pdf->SetSubject('เกียรติบัตรผู้ผ่านการอบรม');
// Remove default header/footer
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
// Set margins to 0
$pdf->SetMargins(0, 0, 0);
$pdf->SetAutoPageBreak(false, 0);
// Add page
$pdf->AddPage();
// Background image path
$imgFile = __DIR__ . '/../pic/AI Antigravity.png';
if (!empty($bgImage)) {
$customImgFile = __DIR__ . '/../' . $bgImage;
if (file_exists($customImgFile)) {
$imgFile = $customImgFile;
}
}
if (!file_exists($imgFile)) {
throw new Exception("ไม่พบไฟล์เทมเพลตเกียรติบัตรที่: " . $imgFile);
}
// วาดภาพพื้นหลังให้พอดีกับกระดาษ A4 แนวนอน (297 x 210 มิลลิเมตร)
$pdf->Image($imgFile, 0, 0, 297, 210, '', '', '', false, 300, '', false, false, 0);
// เรียกใช้ฟอนต์ภาษาไทย Sarabun
$fontRegular = 'sarabun';
$fontBold = 'sarabunb';
// กำหนดฟอนต์ชื่อ: หนา (Bold) ขนาด 26pt (สเกลย่อส่วนสำหรับ A4)
$pdf->SetFont($fontBold, '', 26);
$pdf->SetTextColor(30, 41, 59);
// จัดวางชื่อกึ่งกลางหน้ากระดาษ (ความกว้าง A4 = 297 มม.) (และแก้ปัญหาสระลอย)
$pdf->SetXY(0, $yPos);
$pdf->Cell(297, 15, fixThaiVowels($name), 0, false, 'C', 0, '', 0, false, 'T', 'M');
// วาดรหัสตรวจสอบเกียรติบัตรที่กึ่งกลางล่างสุดของกระดาษ (ขนาด 8pt พิกัด Y = $codeY มม.)
if (!empty($certCode)) {
$pdf->SetFont($fontRegular, '', 8);
$pdf->SetTextColor(100, 116, 139); // Muted Slate color
$pdf->SetXY(0, $codeY);
$pdf->Cell(297, 10, fixThaiVowels('รหัสตรวจสอบเกียรติบัตร: ' . $certCode), 0, false, 'C', 0, '', 0, false, 'T', 'M');
}
// Output
if (!empty($outputPath)) {
$dir = dirname($outputPath);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$pdf->Output($outputPath, 'F');
return $outputPath;
} else {
return $pdf->Output('', 'S');
}
}
/**
* แก้ไขปัญหาสระลอยและวรรณยุกต์ซ้อนทับกันในไฟล์ PDF ภาษาไทย (สำหรับ TCPDF/FPDF)
* โดยการใช้รหัส Unicode ในส่วน Private Use Area (PUA) ของฟอนต์ไทยมาตรฐาน
*
* @param string $text ข้อความภาษาไทยที่ต้องการจัดรูปแบบสระ/วรรณยุกต์
* @return string ข้อความที่จัดตำแหน่งสระ/วรรณยุกต์แล้ว
*/
function fixThaiVowels($text) {
// TCPDF with the Sarabun font natively handles Thai vowel and tone mark positioning correctly.
// The previous PUA mapping caused square box rendering issues.
return $text;
}
+329
View File
@@ -0,0 +1,329 @@
<?php
/**
* Export API
* GET ?action=pdf&survey_id=X — Export to PDF
* GET ?action=word&survey_id=X — Export to Word (DOCX)
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
requireLogin();
$action = $_GET['action'] ?? '';
$surveyId = (int)($_GET['survey_id'] ?? 0);
if (!$surveyId) {
header('Content-Type: application/json; charset=utf-8');
jsonResponse(['error' => 'Missing survey_id'], 400);
}
// Check access
$db = getDB();
if (!isAdmin()) {
$stmt = $db->prepare("SELECT user_id FROM surveys WHERE id = ?");
$stmt->execute([$surveyId]);
$survey = $stmt->fetch();
if (!$survey || $survey['user_id'] != $_SESSION['user_id']) {
header('Content-Type: application/json; charset=utf-8');
jsonResponse(['error' => 'ไม่มีสิทธิ์'], 403);
}
}
// Get statistics data
$statsData = getStatsData($surveyId);
switch ($action) {
case 'pdf':
exportPDF($statsData);
break;
case 'word':
exportWord($statsData);
break;
default:
header('Content-Type: application/json; charset=utf-8');
jsonResponse(['error' => 'Invalid action'], 400);
}
function getStatsData($surveyId) {
$db = getDB();
// Survey info
$stmt = $db->prepare("SELECT s.*, u.full_name as creator_name FROM surveys s LEFT JOIN users u ON s.user_id = u.id WHERE s.id = ?");
$stmt->execute([$surveyId]);
$survey = $stmt->fetch();
// Total responses
$stmt = $db->prepare("SELECT COUNT(*) as total FROM survey_responses WHERE survey_id = ?");
$stmt->execute([$surveyId]);
$totalResponses = (int)$stmt->fetch()['total'];
// Get sections with questions & stats
$stmt = $db->prepare("SELECT * FROM survey_sections WHERE survey_id = ? ORDER BY section_number");
$stmt->execute([$surveyId]);
$sections = $stmt->fetchAll();
foreach ($sections as &$section) {
// Load headings
$hStmt = $db->prepare("SELECT * FROM survey_headings WHERE section_id = ? ORDER BY order_num");
$hStmt->execute([$section['id']]);
$headings = $hStmt->fetchAll();
$qStmt = $db->prepare("SELECT * FROM survey_questions WHERE section_id = ? ORDER BY order_num");
$qStmt->execute([$section['id']]);
$questions = $qStmt->fetchAll();
foreach ($questions as &$q) {
if ($q['question_type'] === 'radio' && empty($q['options'])) {
$aStmt = $db->prepare("
SELECT answer_value, COUNT(*) as cnt
FROM response_answers
WHERE question_id = ? AND answer_value REGEXP '^[0-9]+$'
GROUP BY answer_value
");
$aStmt->execute([$q['id']]);
$freqData = $aStmt->fetchAll();
$values = [];
$freq = [];
for ($i = 1; $i <= 5; $i++) $freq[$i] = 0;
foreach ($freqData as $fd) {
$val = (int)$fd['answer_value'];
if ($val >= 1 && $val <= 5) {
$freq[$val] = (int)$fd['cnt'];
for ($j = 0; $j < (int)$fd['cnt']; $j++) $values[] = $val;
}
}
$mean = count($values) > 0 ? array_sum($values) / count($values) : 0;
$sd = 0;
if (count($values) > 1) {
$sumSq = 0;
foreach ($values as $v) $sumSq += pow($v - $mean, 2);
$sd = sqrt($sumSq / (count($values) - 1));
}
$q['mean'] = round($mean, 2);
$q['sd'] = round($sd, 2);
$q['frequencies'] = $freq;
$q['total_answers'] = count($values);
if ($mean >= 4.51) $q['interpretation'] = 'มากที่สุด';
elseif ($mean >= 3.51) $q['interpretation'] = 'มาก';
elseif ($mean >= 2.51) $q['interpretation'] = 'ปานกลาง';
elseif ($mean >= 1.51) $q['interpretation'] = 'น้อย';
else $q['interpretation'] = 'น้อยที่สุด';
} elseif (in_array($q['question_type'], ['dropdown', 'checkbox', 'radio'])) {
$aStmt = $db->prepare("SELECT answer_value, COUNT(*) as cnt FROM response_answers WHERE question_id = ? GROUP BY answer_value ORDER BY cnt DESC");
$aStmt->execute([$q['id']]);
$q['frequencies'] = $aStmt->fetchAll();
}
if ($q['question_type'] === 'text') {
$aStmt = $db->prepare("SELECT answer_value FROM response_answers WHERE question_id = ? AND answer_value != ''");
$aStmt->execute([$q['id']]);
$q['text_responses'] = array_column($aStmt->fetchAll(), 'answer_value');
}
}
unset($q); // Break the reference to prevent overwriting last element
// Group questions by heading
$looseQuestions = [];
$headingQuestionsMap = [];
foreach ($questions as $q) {
if (!empty($q['heading_id'])) {
$headingQuestionsMap[$q['heading_id']][] = $q;
} else {
$looseQuestions[] = $q;
}
}
foreach ($headings as &$h) {
$h['questions'] = $headingQuestionsMap[$h['id']] ?? [];
}
$section['headings'] = $headings;
$section['questions'] = $looseQuestions;
}
return [
'survey' => $survey,
'total_responses' => $totalResponses,
'sections' => $sections
];
}
function generateHTML($data) {
$survey = $data['survey'];
$totalResponses = $data['total_responses'];
$sections = $data['sections'];
$html = '<!DOCTYPE html><html lang="th"><head><meta charset="UTF-8">';
$html .= '<style>
body { font-family: "TH Sarabun New", "Sarabun", sans-serif; font-size: 16px; line-height: 1.6; }
h1 { text-align: center; font-size: 20px; margin-bottom: 5px; }
h2 { font-size: 18px; margin-top: 20px; border-bottom: 1px solid #333; padding-bottom: 5px; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #333; padding: 6px 10px; text-align: center; font-size: 14px; }
th { background-color: #f0f0f0; font-weight: bold; }
td:first-child { text-align: left; }
.info { margin: 5px 0; }
.text-response { padding: 5px 0; border-bottom: 1px dotted #ccc; }
</style></head><body>';
$html .= '<h1>รายงานผลการประเมิน</h1>';
$html .= '<h1>' . htmlspecialchars($survey['title']) . '</h1>';
$html .= '<p class="info"><strong>ผู้สร้าง:</strong> ' . htmlspecialchars($survey['creator_name'] ?? '') . '</p>';
$html .= '<p class="info"><strong>จำนวนผู้ตอบ:</strong> ' . $totalResponses . ' คน</p>';
$html .= '<p class="info"><strong>วันที่:</strong> ' . date('d/m/Y', strtotime($survey['created_at'])) . '</p>';
foreach ($sections as $section) {
$cleanSectionTitle = preg_replace('/^ส่วนที่\s*[\d๑-๙]+\s*/u', '', $section['title']);
$cleanSectionTitle = preg_replace('/^\s*[\d๑-๙]+[\.\)\s\-:\x{00A0}]*\s*/u', '', $cleanSectionTitle);
$html .= '<h2>' . htmlspecialchars($cleanSectionTitle) . '</h2>';
// Render headings and their questions
if (!empty($section['headings'])) {
foreach ($section['headings'] as $heading) {
if (!empty($heading['questions'])) {
$cleanHeadingTitle = preg_replace('/^\s*[\d๑-๙]+[\.\)\s\-:\x{00A0}]*\s*/u', '', $heading['title']);
$html .= '<h3 style="font-size:16px; margin-top:15px; color:#2563eb; border-left:3px solid #2563eb; padding-left:8px;">' . htmlspecialchars($cleanHeadingTitle) . '</h3>';
if (!empty($heading['description'])) {
$html .= '<p style="font-size:13px; color:#666; margin:0 0 8px 11px;">' . htmlspecialchars($heading['description']) . '</p>';
}
$html .= renderQuestionsHTML($heading['questions']);
}
}
}
// Render loose questions
if (!empty($section['questions'])) {
$html .= renderQuestionsHTML($section['questions']);
}
}
$html .= '</body></html>';
return $html;
}
function renderQuestionsHTML($questions) {
$html = '';
$hasRating = false;
foreach ($questions as $q) {
if ($q['question_type'] === 'radio' && empty($q['options'])) { $hasRating = true; break; }
}
if ($hasRating) {
$html .= '<table>';
$html .= '<tr><th rowspan="2">รายการประเมิน</th><th colspan="5">ระดับความคิดเห็น (ความถี่)</th><th rowspan="2">X̄</th><th rowspan="2">S.D.</th><th rowspan="2">แปลผล</th></tr>';
$html .= '<tr><th>5</th><th>4</th><th>3</th><th>2</th><th>1</th></tr>';
$totalMean = 0;
$totalSD = 0;
$ratingCount = 0;
foreach ($questions as $q) {
if ($q['question_type'] === 'radio' && empty($q['options'])) {
$cleanText = preg_replace('/^\s*[\d๑-๙]+[\.\)\s\-:\x{00A0}]*\s*/u', '', $q['question_text']);
$html .= '<tr>';
$html .= '<td>' . htmlspecialchars($cleanText) . '</td>';
for ($i = 5; $i >= 1; $i--) {
$html .= '<td>' . ($q['frequencies'][$i] ?? 0) . '</td>';
}
$html .= '<td>' . number_format($q['mean'] ?? 0, 2) . '</td>';
$html .= '<td>' . number_format($q['sd'] ?? 0, 2) . '</td>';
$html .= '<td>' . ($q['interpretation'] ?? '-') . '</td>';
$html .= '</tr>';
$totalMean += $q['mean'] ?? 0;
$totalSD += $q['sd'] ?? 0;
$ratingCount++;
}
}
if ($ratingCount > 0) {
$avgMean = $totalMean / $ratingCount;
$avgSD = $totalSD / $ratingCount;
$avgInterp = '';
if ($avgMean >= 4.51) $avgInterp = 'มากที่สุด';
elseif ($avgMean >= 3.51) $avgInterp = 'มาก';
elseif ($avgMean >= 2.51) $avgInterp = 'ปานกลาง';
elseif ($avgMean >= 1.51) $avgInterp = 'น้อย';
else $avgInterp = 'น้อยที่สุด';
$html .= '<tr style="font-weight:bold;background:#f9f9f9;">';
$html .= '<td>รวมเฉลี่ย</td><td colspan="5"></td>';
$html .= '<td>' . number_format($avgMean, 2) . '</td>';
$html .= '<td>' . number_format($avgSD, 2) . '</td>';
$html .= '<td>' . $avgInterp . '</td>';
$html .= '</tr>';
}
$html .= '</table>';
}
// Frequency tables for dropdown/checkbox/custom radio
foreach ($questions as $q) {
if (in_array($q['question_type'], ['dropdown', 'checkbox', 'radio']) && (!empty($q['options']) || $q['question_type'] !== 'radio')) {
if (!empty($q['frequencies'])) {
$cleanText = preg_replace('/^\s*[\d๑-๙]+[\.\)\s\-:\x{00A0}]*\s*/u', '', $q['question_text']);
$html .= '<p><strong>' . htmlspecialchars($cleanText) . '</strong></p>';
$html .= '<table><tr><th>ตัวเลือก</th><th>ความถี่</th><th>ร้อยละ</th></tr>';
$totalQ = array_sum(array_column($q['frequencies'], 'cnt'));
foreach ($q['frequencies'] as $f) {
$percent = $totalQ > 0 ? round(($f['cnt'] / $totalQ) * 100, 1) : 0;
$html .= '<tr><td>' . htmlspecialchars($f['answer_value']) . '</td><td>' . $f['cnt'] . '</td><td>' . $percent . '%</td></tr>';
}
$html .= '</table>';
}
}
}
// Text responses
foreach ($questions as $q) {
if ($q['question_type'] === 'text' && !empty($q['text_responses'])) {
$cleanText = preg_replace('/^\s*[\d๑-๙]+[\.\)\s\-:\x{00A0}]*\s*/u', '', $q['question_text']);
$html .= '<p><strong>' . htmlspecialchars($cleanText) . '</strong></p>';
foreach ($q['text_responses'] as $i => $text) {
$html .= '<div class="text-response">' . ($i+1) . '. ' . htmlspecialchars($text) . '</div>';
}
}
}
return $html;
}
function exportPDF($data) {
$html = generateHTML($data);
$title = $data['survey']['title'] ?? 'survey_report';
$filename = 'report_' . preg_replace('/[^a-zA-Z0-9ก-๙]/u', '_', $title) . '.pdf';
// Use simple HTML-to-PDF approach
header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html><head><meta charset="UTF-8">';
echo '<title>' . htmlspecialchars($title) . '</title>';
echo '<style>@media print { body { -webkit-print-color-adjust: exact; } }</style>';
echo '<script>window.onload = function() { window.print(); }</script>';
echo '</head><body>';
echo $html;
echo '</body></html>';
}
function exportWord($data) {
$html = generateHTML($data);
$title = $data['survey']['title'] ?? 'survey_report';
$filename = 'report_' . preg_replace('/[^a-zA-Z0-9ก-๙]/u', '_', $title) . '.doc';
header('Content-Type: application/vnd.ms-word');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: max-age=0');
echo '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">';
echo '<head><meta charset="UTF-8"><title>' . htmlspecialchars($title) . '</title></head>';
echo '<body>';
echo $html;
echo '</body></html>';
}
+291
View File
@@ -0,0 +1,291 @@
<?php
/**
* Import Google Form API
* POST ?action=importFromUrl - Import survey structure from Google Form URL
* POST ?action=importFromJson - Import survey structure from JSON
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
requireLogin();
$action = $_GET['action'] ?? '';
switch ($action) {
case 'importFromUrl':
importFromUrl();
break;
case 'importFromJson':
importFromJson();
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function importFromUrl() {
$input = json_decode(file_get_contents('php://input'), true);
$url = trim($input['url'] ?? '');
$projectId = !empty($input['project_id']) ? (int)$input['project_id'] : null;
if (empty($url)) {
jsonResponse(['error' => 'กรุณาระบุ URL หรือ Link ของ Google Form'], 400);
}
// Extract Form ID or viewform URL
if (!preg_match('/docs\.google\.com\/forms\/d\/e?\/([^\/]+)/i', $url, $matches) && !preg_match('/docs\.google\.com\/forms\/d\/([^\/]+)/i', $url, $matches)) {
jsonResponse(['error' => 'รูปแบบ URL ของ Google Form ไม่ถูกต้อง'], 400);
}
// Attempt to fetch the public viewform HTML
$fetchUrl = $url;
if (strpos($fetchUrl, '/viewform') === false && strpos($fetchUrl, '/edit') === false) {
$fetchUrl = rtrim($fetchUrl, '/') . '/viewform';
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fetchUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$html = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || empty($html)) {
jsonResponse(['error' => 'ไม่สามารถดึงข้อมูลจาก Google Form URL นี้ได้ โปรดตรวจสอบว่าเปิดเป็นสาธารณะ (Public) หรือไม่'], 400);
}
// Parse FB_PUBLIC_LOAD_DATA_
if (!preg_match('/FB_PUBLIC_LOAD_DATA_\s*=\s*(.+?);<\/script>/s', $html, $dataMatches)) {
jsonResponse(['error' => 'ไม่พบโครงสร้างแบบสอบถามใน Google Form นี้'], 400);
}
$formData = json_decode($dataMatches[1], true);
if (!$formData || !isset($formData[1])) {
jsonResponse(['error' => 'โครงสร้างข้อมูล Google Form ไม่ถูกต้อง'], 400);
}
$parsedSurvey = parseGoogleFormData($formData);
$surveyId = saveParsedSurveyToDB($parsedSurvey, $projectId);
jsonResponse([
'success' => true,
'message' => 'นำเข้าโครงสร้างแบบสอบถามจาก Google Form เรียบร้อยแล้ว',
'survey_id' => $surveyId,
'title' => $parsedSurvey['title']
]);
}
function importFromJson() {
$input = json_decode(file_get_contents('php://input'), true);
$projectId = !empty($input['project_id']) ? (int)$input['project_id'] : null;
$surveyData = $input['survey_data'] ?? null;
if (!$surveyData || empty($surveyData['title'])) {
jsonResponse(['error' => 'ข้อมูล JSON ไม่ถูกต้อง หรือขาดชื่อแบบสอบถาม'], 400);
}
$surveyId = saveParsedSurveyToDB($surveyData, $projectId);
jsonResponse([
'success' => true,
'message' => 'นำเข้าแบบสอบถามเรียบร้อยแล้ว',
'survey_id' => $surveyId,
'title' => $surveyData['title']
]);
}
/**
* Helper: Parse Google Forms FB_PUBLIC_LOAD_DATA_ structure
*/
function parseGoogleFormData($data) {
$title = $data[1][8] ?? $data[1][1] ?? $data[3] ?? 'แบบสอบถามจาก Google Forms';
$description = $data[1][0] ?? '';
$rawItems = $data[1][1] ?? [];
$sections = [];
$currentSection = [
'section_number' => 1,
'title' => 'ส่วนที่ 1 ข้อมูลทั่วไป',
'description' => '',
'category' => 'general',
'questions' => []
];
$sectionCount = 1;
foreach ($rawItems as $item) {
if (!is_array($item)) continue;
$qTitle = $item[1] ?? '';
$qDesc = $item[2] ?? '';
$qTypeNum = $item[3] ?? 0;
$qDetails = $item[4][0] ?? [];
if (empty($qTitle) && $qTypeNum != 8) continue; // Skip empty items except section breaks
// Type 8 = Section Break in Google Forms
if ($qTypeNum == 8) {
if (!empty($currentSection['questions'])) {
$sections[] = $currentSection;
}
$sectionCount++;
$currentSection = [
'section_number' => $sectionCount,
'title' => $qTitle ?: ('ส่วนที่ ' . $sectionCount),
'description' => $qDesc,
'category' => 'rating',
'questions' => []
];
continue;
}
$qType = 'radio';
$options = [];
$scaleMin = 1;
$scaleMax = 5;
// Map Google Forms Item Type numbers:
// 0: Short Text, 1: Paragraph
// 2: Radio (Multiple Choice), 3: Dropdown, 4: Checkbox
// 5: Linear Scale (1-5)
// 7: Grid / Matrix
switch ($qTypeNum) {
case 0:
case 1:
$qType = 'text';
break;
case 2:
$qType = 'radio';
if (isset($qDetails[1]) && is_array($qDetails[1])) {
foreach ($qDetails[1] as $opt) {
if (isset($opt[0])) $options[] = $opt[0];
}
}
break;
case 3:
$qType = 'dropdown';
if (isset($qDetails[1]) && is_array($qDetails[1])) {
foreach ($qDetails[1] as $opt) {
if (isset($opt[0])) $options[] = $opt[0];
}
}
break;
case 4:
$qType = 'checkbox';
if (isset($qDetails[1]) && is_array($qDetails[1])) {
foreach ($qDetails[1] as $opt) {
if (isset($opt[0])) $options[] = $opt[0];
}
}
break;
case 5:
$qType = 'radio'; // Rating Scale
$scaleMin = 1;
$scaleMax = 5;
break;
default:
$qType = 'text';
break;
}
$isRequired = isset($qDetails[2]) && $qDetails[2] == 1 ? 1 : 0;
$questionObj = [
'question_text' => $qTitle,
'question_type' => $qType,
'is_required' => $isRequired,
'description' => $qDesc,
'options' => count($options) > 0 ? $options : null,
'scale_min' => $scaleMin,
'scale_max' => $scaleMax
];
$currentSection['questions'][] = $questionObj;
}
if (!empty($currentSection['questions'])) {
$sections[] = $currentSection;
}
if (empty($sections)) {
$sections[] = [
'section_number' => 1,
'title' => 'ส่วนที่ 1 ข้อมูลและข้อคำถาม',
'description' => 'นำเข้าจาก Google Forms',
'category' => 'general',
'questions' => []
];
}
return [
'title' => $title,
'description' => $description ?: 'แบบสอบถามที่นำเข้าจาก Google Forms',
'sections' => $sections
];
}
/**
* Save parsed survey structure into database
*/
function saveParsedSurveyToDB($parsedSurvey, $projectId = null) {
$db = getDB();
$uniqueLink = bin2hex(random_bytes(16));
$db->beginTransaction();
try {
$stmt = $db->prepare("INSERT INTO surveys (user_id, project_id, title, description, unique_link, status) VALUES (?, ?, ?, ?, ?, 'active')");
$stmt->execute([
$_SESSION['user_id'],
$projectId,
$parsedSurvey['title'],
$parsedSurvey['description'] ?? '',
$uniqueLink
]);
$surveyId = $db->lastInsertId();
$sections = $parsedSurvey['sections'] ?? [];
$secNum = 1;
foreach ($sections as $sec) {
$stmt = $db->prepare("INSERT INTO survey_sections (survey_id, section_number, title, description, category) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$surveyId,
$sec['section_number'] ?? $secNum,
$sec['title'] ?? ('ส่วนที่ ' . $secNum),
$sec['description'] ?? '',
$sec['category'] ?? 'rating'
]);
$sectionId = $db->lastInsertId();
$orderNum = 1;
foreach ($sec['questions'] as $q) {
$optionsJson = !empty($q['options']) ? json_encode($q['options'], JSON_UNESCAPED_UNICODE) : null;
$stmtQ = $db->prepare("INSERT INTO survey_questions (section_id, question_text, question_type, is_required, order_num, options, scale_min, scale_max, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmtQ->execute([
$sectionId,
$q['question_text'],
$q['question_type'] ?? 'radio',
$q['is_required'] ?? 1,
$orderNum++,
$optionsJson,
$q['scale_min'] ?? 1,
$q['scale_max'] ?? 5,
$q['description'] ?? null
]);
}
$secNum++;
}
$db->commit();
return $surveyId;
} catch (Exception $e) {
$db->rollBack();
jsonResponse(['error' => 'เกิดข้อผิดพลาดในการบันทึกแบบสอบถาม: ' . $e->getMessage()], 500);
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
/**
* Import Responses API (from CSV / Excel exported from Google Forms)
* POST ?survey_id=X
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
requireLogin();
$surveyId = (int)($_GET['survey_id'] ?? $_POST['survey_id'] ?? 0);
if (!$surveyId) {
jsonResponse(['error' => 'Missing survey_id'], 400);
}
$db = getDB();
// Verify access
if (!isAdmin()) {
$stmt = $db->prepare("SELECT user_id FROM surveys WHERE id = ?");
$stmt->execute([$surveyId]);
$s = $stmt->fetch();
if (!$s || $s['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์เข้าถึงแบบสอบถามนี้'], 403);
}
}
// Check uploaded file
if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
jsonResponse(['error' => 'กรุณาอัปโหลดไฟล์ CSV คำตอบจาก Google Forms'], 400);
}
$filePath = $_FILES['csv_file']['tmp_name'];
// Read CSV
$handle = fopen($filePath, 'r');
if (!$handle) {
jsonResponse(['error' => 'ไม่สามารถเปิดไฟล์ CSV ได้'], 500);
}
// Check UTF-8 BOM or UTF-16
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
// Get Headers (Row 1)
$headers = fgetcsv($handle, 0, ",");
if (!$headers) {
fclose($handle);
jsonResponse(['error' => 'ไฟล์ CSV ไม่มีข้อมูลหรือรูปแบบไม่ถูกต้อง'], 400);
}
// Clean headers
$cleanHeaders = array_map(function($h) {
return trim(preg_replace('/\s+/', ' ', $h));
}, $headers);
// Fetch all questions for this survey
$stmt = $db->prepare("
SELECT q.id, q.question_text, q.question_type
FROM survey_questions q
JOIN survey_sections s ON q.section_id = s.id
WHERE s.survey_id = ?
ORDER BY s.section_number, q.order_num
");
$stmt->execute([$surveyId]);
$questions = $stmt->fetchAll();
if (empty($questions)) {
fclose($handle);
jsonResponse(['error' => 'ไม่พบข้อคำถามในแบบสอบถามนี้'], 400);
}
// Map CSV Column Index -> question_id
$colToQuestionMap = [];
function normalizeText($str) {
$str = mb_strtolower($str, 'UTF-8');
// Remove leading numbers or prefix like "1. ", "ข้อ 1. "
$str = preg_replace('/^[\d๑-๙]+[\.\)\s\-:\x{00A0}]*/u', '', $str);
return trim($str);
}
foreach ($cleanHeaders as $colIdx => $headerText) {
if (empty($headerText) || strtolower($headerText) === 'timestamp' || strtolower($headerText) === 'ประทับเวลา') {
continue;
}
$normHeader = normalizeText($headerText);
// Try exact or normalized matching against survey questions
foreach ($questions as $q) {
$normQText = normalizeText($q['question_text']);
if ($normHeader === $normQText || strpos($normHeader, $normQText) !== false || strpos($normQText, $normHeader) !== false) {
$colToQuestionMap[$colIdx] = $q['id'];
break;
}
}
}
// Fallback: If column mapping count is low, map columns sequentially to questions
if (count($colToQuestionMap) === 0) {
$qIdx = 0;
foreach ($cleanHeaders as $colIdx => $headerText) {
if (strtolower($headerText) === 'timestamp' || strtolower($headerText) === 'ประทับเวลา') continue;
if (isset($questions[$qIdx])) {
$colToQuestionMap[$colIdx] = $questions[$qIdx]['id'];
$qIdx++;
}
}
}
// Process Rows
$importedCount = 0;
$db->beginTransaction();
try {
while (($row = fgetcsv($handle, 0, ",")) !== false) {
if (empty(array_filter($row))) continue; // Skip empty lines
// Extract timestamp or use now
$submittedAt = date('Y-m-d H:i:s');
if (!empty($row[0])) {
$ts = strtotime($row[0]);
if ($ts !== false) {
$submittedAt = date('Y-m-d H:i:s', $ts);
}
}
// Insert into survey_responses
$stmtResp = $db->prepare("INSERT INTO survey_responses (survey_id, respondent_info, submitted_at) VALUES (?, ?, ?)");
$stmtResp->execute([
$surveyId,
json_encode(['imported' => true, 'source' => 'google_forms_csv'], JSON_UNESCAPED_UNICODE),
$submittedAt
]);
$responseId = $db->lastInsertId();
// Insert answers
foreach ($colToQuestionMap as $colIdx => $qId) {
$val = isset($row[$colIdx]) ? trim($row[$colIdx]) : '';
if ($val !== '') {
$stmtAns = $db->prepare("INSERT INTO response_answers (response_id, question_id, answer_value) VALUES (?, ?, ?)");
$stmtAns->execute([$responseId, $qId, $val]);
}
}
$importedCount++;
}
$db->commit();
fclose($handle);
jsonResponse([
'success' => true,
'message' => "นำเข้าข้อมูลคำตอบสำเร็จจำนวน $importedCount รายการ",
'imported_count' => $importedCount,
'mapped_questions' => count($colToQuestionMap)
]);
} catch (Exception $e) {
$db->rollBack();
fclose($handle);
jsonResponse(['error' => 'เกิดข้อผิดพลาดในการบันทึกคำตอบ: ' . $e->getMessage()], 500);
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Mail Dispatch Helper using PHPMailer
* TPU Survey System
*/
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../config/mail.php';
/**
* Sends an email with the certificate attached
*
* @param string $toEmail Recipient email address
* @param string $toName Recipient name
* @param string $pdfPath Path to the generated PDF certificate
* @param string $surveyTitle Survey title for context
* @return bool True on success
* @throws Exception on mail sending failure
*/
function sendCertificateEmail($toEmail, $toName, $pdfPath, $surveyTitle = '') {
$mail = new PHPMailer(true);
try {
// Server settings
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USER;
$mail->Password = SMTP_PASS;
$mail->SMTPSecure = SMTP_SECURE; // 'tls' or 'ssl'
$mail->Port = SMTP_PORT;
// UTF-8 encoding for Thai support
$mail->CharSet = 'UTF-8';
// Recipients
$mail->setFrom(MAIL_FROM_EMAIL, MAIL_FROM_NAME);
$mail->addAddress($toEmail, $toName);
// Attachments
if (!file_exists($pdfPath)) {
throw new Exception("ไม่พบไฟล์เกียรติบัตรที่จะแนบส่ง: " . $pdfPath);
}
$attachmentName = 'เกียรติบัตร_' . str_replace(' ', '_', $toName) . '.pdf';
$mail->addAttachment($pdfPath, $attachmentName);
// Content
$mail->isHTML(true);
$mail->Subject = 'เกียรติบัตรผ่านการอบรม/แบบประเมิน - ' . $surveyTitle;
$bodyHtml = "
<div style='font-family: \"Sarabun\", sans-serif; line-height: 1.6; color: #333;'>
<h2 style='color: #2563eb;'>สวัสดีคุณ {$toName},</h2>
<p>ขอขอบคุณสำหรับความคิดเห็นและความร่วมมือในการตอบแบบประเมินโครงการ/หลักสูตร <strong>\"{$surveyTitle}\"</strong></p>
<p>ทางผู้จัดงานขอมอบเกียรติบัตรผู้ผ่านการอบรมให้แก่ท่าน โดยท่านสามารถดาวน์โหลดไฟล์เกียรติบัตรที่แนบมากับอีเมลฉบับนี้ได้เลยครับ</p>
<br>
<p>ขอแสดงความนับถือ,</p>
<p><strong>ฝ่ายจัดโครงการ มหาวิทยาลัยตาปี</strong></p>
<hr style='border: 0; border-top: 1px solid #eee; margin: 20px 0;'>
<p style='font-size: 0.8rem; color: #999;'>นี่เป็นอีเมลอัตโนมัติจากระบบ กรุณาอย่าตอบกลับอีเมลฉบับนี้</p>
</div>
";
$mail->Body = $bodyHtml;
$mail->AltBody = "สวัสดีคุณ {$toName},\n\nขอขอบคุณสำหรับการตอบแบบประเมินโครงการ \"{$surveyTitle}\"\n\nทางผู้จัดงานขอมอบเกียรติบัตรผู้ผ่านการอบรมให้แก่ท่าน โดยไฟล์เกียรติบัตรถูกแนบมาพร้อมกับอีเมลนี้เรียบร้อยแล้ว\n\nขอแสดงความนับถือ,\nฝ่ายจัดโครงการ มหาวิทยาลัยตาปี";
$mail->send();
return true;
} catch (Exception $e) {
throw new Exception("ระบบส่งอีเมลขัดข้อง: " . $mail->ErrorInfo);
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
/**
* Profile API — any logged-in user can view/update their own profile
* GET ?action=get — Get current user's profile
* POST ?action=update — Update current user's profile (except personnel_code)
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
requireLogin();
$action = $_GET['action'] ?? '';
switch ($action) {
case 'get':
getProfile();
break;
case 'update':
updateProfile();
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function getProfile() {
$userId = $_SESSION['user_id'];
$db = getDB();
$stmt = $db->prepare("SELECT id, username, role, full_name, personnel_code, subject_area, branch_code, email, created_at FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user) {
jsonResponse(['error' => 'ไม่พบข้อมูลผู้ใช้'], 404);
}
jsonResponse(['user' => $user]);
}
function updateProfile() {
$userId = $_SESSION['user_id'];
$input = getInput();
$fullName = trim($input['full_name'] ?? '');
$subjectArea = trim($input['subject_area'] ?? '');
$branchCode = trim($input['branch_code'] ?? '');
$email = trim($input['email'] ?? '');
$db = getDB();
$fields = [];
$params = [];
if (!empty($fullName)) { $fields[] = "full_name = ?"; $params[] = $fullName; }
if ($subjectArea !== '') { $fields[] = "subject_area = ?"; $params[] = $subjectArea; }
if ($branchCode !== '') { $fields[] = "branch_code = ?"; $params[] = $branchCode; }
if (isset($input['email'])) { $fields[] = "email = ?"; $params[] = trim($input['email']); }
// Update password if provided
if (!empty($input['password'])) {
if (strlen($input['password']) < 4) {
jsonResponse(['error' => 'รหัสผ่านต้องมีอย่างน้อย 4 ตัวอักษร'], 400);
}
$fields[] = "password_hash = ?";
$params[] = password_hash($input['password'], PASSWORD_DEFAULT);
}
if (empty($fields)) {
jsonResponse(['error' => 'ไม่มีข้อมูลที่ต้องอัปเดต'], 400);
}
$params[] = $userId;
$sql = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->execute($params);
// Update session name if changed
if (!empty($fullName)) {
$_SESSION['full_name'] = $fullName;
}
jsonResponse(['success' => true, 'message' => 'อัปเดตข้อมูลส่วนตัวสำเร็จ']);
}
+254
View File
@@ -0,0 +1,254 @@
<?php
/**
* Survey Responses API
* POST ?action=submit — Submit response (public)
* GET ?action=list&survey_id=X — List responses for a survey
* GET ?action=get&id=X — Get single response detail
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
require_once __DIR__ . '/../config/mail.php';
require_once __DIR__ . '/certificate_helper.php';
require_once __DIR__ . '/mail_helper.php';
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? '';
// Public endpoint
if ($action === 'submit') {
submitResponse();
exit;
}
requireLogin();
switch ($action) {
case 'list':
listResponses();
break;
case 'get':
getResponse();
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function submitResponse() {
$input = getInput();
$surveyLink = $input['survey_link'] ?? '';
$answers = $input['answers'] ?? [];
$respondentInfo = $input['respondent_info'] ?? [];
if (empty($surveyLink)) {
jsonResponse(['error' => 'Missing survey link'], 400);
}
$db = getDB();
// Find survey
$stmt = $db->prepare("SELECT id, status, title, has_certificate, certificate_template, cert_name_y, cert_code_y FROM surveys WHERE unique_link = ?");
$stmt->execute([$surveyLink]);
$survey = $stmt->fetch();
if (!$survey) jsonResponse(['error' => 'ไม่พบแบบสอบถาม'], 404);
if ($survey['status'] !== 'active') jsonResponse(['error' => 'แบบสอบถามนี้ปิดรับคำตอบแล้ว'], 400);
// ตรวจสอบข้อมูลเพื่อรับเกียรติบัตรล่วงหน้า (และทำ Fallback ตรวจจับจากคำตอบ)
if (!empty($survey['has_certificate'])) {
$email = trim($respondentInfo['email'] ?? '');
$name = trim($respondentInfo['name'] ?? '');
// Fallback: หากส่งค่าจาก JS เป็นค่าว่าง (เช่น กรณีเบราว์เซอร์แคชไฟล์เก่า) ค้นหาอีเมลจากคำตอบ
if (empty($email) || empty($name)) {
foreach ($answers as $ans) {
$val = trim($ans['value'] ?? '');
if (filter_var($val, FILTER_VALIDATE_EMAIL) &&
(str_ends_with($val, '@tapee.ac.th') || str_ends_with($val, '@student.tapee.ac.th') || str_ends_with($val, '@gmail.com') || str_ends_with($val, '@gmil.com'))) {
$email = $val;
}
}
if (!empty($email) && empty($name)) {
// ค้นหาช่องคำตอบชื่อจริง
$stmtQ = $db->prepare("
SELECT q.id
FROM survey_questions q
JOIN survey_sections s ON q.section_id = s.id
WHERE s.survey_id = ? AND (q.question_text LIKE '%ชื่อ%' OR q.question_text LIKE '%นามสกุล%')
LIMIT 1
");
$stmtQ->execute([$survey['id']]);
$qRow = $stmtQ->fetch();
if ($qRow) {
$nameQuestionId = $qRow['id'];
foreach ($answers as $ans) {
if (($ans['question_id'] ?? 0) == $nameQuestionId) {
$name = trim($ans['value'] ?? '');
break;
}
}
}
}
// อัปเดตกลับเข้าตัวแปร respondentInfo เพื่อเตรียมบันทึกลง JSON
$respondentInfo['email'] = $email;
$respondentInfo['name'] = $name;
}
// ตรวจสอบการกรอกอีเมลซ้ำสำหรับแบบสอบถามที่แจกเกียรติบัตร (1 อีเมล ต่อ 1 สิทธิ์)
if (!empty($email)) {
$stmtCheck = $db->prepare("
SELECT COUNT(*) FROM survey_responses
WHERE survey_id = ? AND LOWER(JSON_UNQUOTE(JSON_EXTRACT(respondent_info, '$.email'))) = ?
");
$stmtCheck->execute([$survey['id'], strtolower($email)]);
if ($stmtCheck->fetchColumn() > 0) {
jsonResponse(['error' => 'อีเมลนี้เคยทำแบบประเมินและขอรับเกียรติบัตรไปแล้ว (สามารถลงทะเบียนได้เพียง 1 ครั้งต่อ 1 อีเมลเท่านั้น)'], 400);
}
}
}
$db->beginTransaction();
try {
$stmt = $db->prepare("INSERT INTO survey_responses (survey_id, respondent_info, ip_address) VALUES (?, ?, ?)");
$stmt->execute([
$survey['id'],
json_encode($respondentInfo, JSON_UNESCAPED_UNICODE),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
$responseId = $db->lastInsertId();
foreach ($answers as $answer) {
$questionId = (int)($answer['question_id'] ?? 0);
$value = $answer['value'] ?? '';
if ($questionId) {
$stmt2 = $db->prepare("INSERT INTO response_answers (response_id, question_id, answer_value) VALUES (?, ?, ?)");
$stmt2->execute([$responseId, $questionId, is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value]);
}
}
$db->commit();
// ดำเนินการสร้างและจัดส่งเกียรติบัตรอัตโนมัติหากเปิดใช้งานเกียรติบัตรสำหรับแบบสอบถามนี้
if (!empty($survey['has_certificate'])) {
$email = $respondentInfo['email'] ?? '';
$name = $respondentInfo['name'] ?? '';
if (!empty($email) && !empty($name)) {
// ดึงข้อมูลหัวข้อแบบสอบถามเพื่อใช้เป็นชื่อหัวข้ออีเมล
$surveyTitle = $survey['title'] ?? 'โครงการอบรม';
// สร้างรหัสตรวจสอบเกียรติบัตรที่ไม่ซ้ำกัน (เช่น TPU-2569-0016)
$thaiYear = date('Y') + 543;
$certCode = 'TPU-' . $thaiYear . '-' . str_pad($responseId, 4, '0', STR_PAD_LEFT);
// อัปเดตรหัสตรวจสอบลงในฐานข้อมูลตาราง survey_responses
$stmtUpdateCode = $db->prepare("UPDATE survey_responses SET cert_code = ? WHERE id = ?");
$stmtUpdateCode->execute([$certCode, $responseId]);
// สร้างโฟลเดอร์ชั่วคราวสำหรับเกียรติบัตร (ถ้ายังไม่มี)
$tempDir = __DIR__ . '/../temp_certificates';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0777, true);
}
chmod($tempDir, 0777);
$pdfFileName = 'cert_' . $responseId . '_' . time() . '.pdf';
$pdfPath = $tempDir . '/' . $pdfFileName;
try {
// 1. สร้างเกียรติบัตร PDF โดยใช้พิกัดและเทมเพลตที่ตั้งค่าเฉพาะของแบบสอบถามนี้
$yPos = isset($survey['cert_name_y']) ? (double)$survey['cert_name_y'] : 82.5;
$codeY = isset($survey['cert_code_y']) ? (double)$survey['cert_code_y'] : 190.5;
$bgImage = $survey['certificate_template'] ?? '';
generateCertificate($name, $pdfPath, $yPos, $certCode, $bgImage, $codeY);
// 2. ส่งอีเมลพร้อมแนบเกียรติบัตร
sendCertificateEmail($email, $name, $pdfPath, $surveyTitle);
// 3. ลบไฟล์ PDF ชั่วคราวหลังจากส่งสำเร็จ
if (file_exists($pdfPath)) {
unlink($pdfPath);
}
} catch (Exception $mailEx) {
// บันทึกข้อผิดพลาดกรณีส่งเมลไม่สำเร็จ แต่ไม่ทำให้การส่งแบบสอบถามทั้งหมดล้มเหลว
// เพื่อให้ผู้ใช้ตอบแบบสอบถามเสร็จสมบูรณ์
error_log("Certificate dispatch failed for response ID " . $responseId . ": " . $mailEx->getMessage());
}
}
}
jsonResponse(['success' => true, 'message' => 'ส่งแบบสอบถามสำเร็จ ขอบคุณค่ะ/ครับ'], 201);
} catch (Exception $e) {
$db->rollBack();
jsonResponse(['error' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()], 500);
}
}
function listResponses() {
$surveyId = (int)($_GET['survey_id'] ?? 0);
if (!$surveyId) jsonResponse(['error' => 'Missing survey_id'], 400);
$db = getDB();
// Check access
if (!isAdmin()) {
$stmt = $db->prepare("SELECT user_id FROM surveys WHERE id = ?");
$stmt->execute([$surveyId]);
$survey = $stmt->fetch();
if (!$survey || $survey['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์'], 403);
}
}
$stmt = $db->prepare("
SELECT id, respondent_info, ip_address, submitted_at
FROM survey_responses
WHERE survey_id = ?
ORDER BY submitted_at DESC
");
$stmt->execute([$surveyId]);
$responses = $stmt->fetchAll();
foreach ($responses as &$r) {
$r['respondent_info'] = json_decode($r['respondent_info'], true);
}
jsonResponse(['responses' => $responses, 'total' => count($responses)]);
}
function getResponse() {
$id = (int)($_GET['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
$stmt = $db->prepare("
SELECT sr.*, s.title as survey_title, s.user_id
FROM survey_responses sr
JOIN surveys s ON sr.survey_id = s.id
WHERE sr.id = ?
");
$stmt->execute([$id]);
$response = $stmt->fetch();
if (!$response) jsonResponse(['error' => 'ไม่พบคำตอบ'], 404);
if (!isAdmin() && $response['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์'], 403);
}
$response['respondent_info'] = json_decode($response['respondent_info'], true);
// Get answers
$stmt = $db->prepare("
SELECT ra.*, sq.question_text, sq.question_type
FROM response_answers ra
JOIN survey_questions sq ON ra.question_id = sq.id
WHERE ra.response_id = ?
ORDER BY sq.order_num
");
$stmt->execute([$id]);
$response['answers'] = $stmt->fetchAll();
jsonResponse(['response' => $response]);
}
+247
View File
@@ -0,0 +1,247 @@
<?php
/**
* Statistics API
* GET ?action=summary&survey_id=X — Summary statistics
* GET ?action=detail&survey_id=X — Detailed per-question stats
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
requireLogin();
$action = $_GET['action'] ?? 'summary';
$surveyId = (int)($_GET['survey_id'] ?? 0);
if (!$surveyId) jsonResponse(['error' => 'Missing survey_id'], 400);
// Check access
$db = getDB();
if (!isAdmin()) {
$stmt = $db->prepare("SELECT user_id FROM surveys WHERE id = ?");
$stmt->execute([$surveyId]);
$survey = $stmt->fetch();
if (!$survey || $survey['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์'], 403);
}
}
switch ($action) {
case 'summary':
getSummary($surveyId);
break;
case 'detail':
getDetail($surveyId);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function getSummary($surveyId) {
$db = getDB();
// Total responses
$stmt = $db->prepare("SELECT COUNT(*) as total FROM survey_responses WHERE survey_id = ?");
$stmt->execute([$surveyId]);
$total = $stmt->fetch()['total'];
// Get survey info
$stmt = $db->prepare("SELECT title, status, created_at FROM surveys WHERE id = ?");
$stmt->execute([$surveyId]);
$surveyInfo = $stmt->fetch();
// Get overall mean and SD for radio/rating questions
$stmt = $db->prepare("
SELECT
AVG(CAST(ra.answer_value AS DECIMAL(10,2))) as overall_mean,
STDDEV(CAST(ra.answer_value AS DECIMAL(10,2))) as overall_sd
FROM response_answers ra
JOIN survey_questions sq ON ra.question_id = sq.id
JOIN survey_sections ss ON sq.section_id = ss.id
WHERE ss.survey_id = ? AND sq.question_type = 'radio' AND sq.options IS NULL AND ra.answer_value REGEXP '^[0-9]+$'
");
$stmt->execute([$surveyId]);
$overall = $stmt->fetch();
jsonResponse([
'survey' => $surveyInfo,
'total_responses' => (int)$total,
'overall_mean' => $overall['overall_mean'] ? round((float)$overall['overall_mean'], 2) : 0,
'overall_sd' => $overall['overall_sd'] ? round((float)$overall['overall_sd'], 2) : 0
]);
}
function getDetail($surveyId) {
$db = getDB();
// Total responses
$stmt = $db->prepare("SELECT COUNT(*) as total FROM survey_responses WHERE survey_id = ?");
$stmt->execute([$surveyId]);
$totalResponses = (int)$stmt->fetch()['total'];
// Get sections with questions
$stmt = $db->prepare("SELECT * FROM survey_sections WHERE survey_id = ? ORDER BY section_number");
$stmt->execute([$surveyId]);
$sections = $stmt->fetchAll();
$result = [];
foreach ($sections as $section) {
$qStmt = $db->prepare("SELECT * FROM survey_questions WHERE section_id = ? ORDER BY order_num");
$qStmt->execute([$section['id']]);
$questions = $qStmt->fetchAll();
$sectionStats = [
'section_number' => $section['section_number'],
'title' => $section['title'],
'headings' => [],
'questions' => []
];
// Load headings for this section
$headingStmt = $db->prepare("SELECT * FROM survey_headings WHERE section_id = ? ORDER BY order_num");
$headingStmt->execute([$section['id']]);
$headings = $headingStmt->fetchAll();
foreach ($questions as $q) {
$qStat = [
'id' => $q['id'],
'heading_id' => $q['heading_id'] ?? null,
'question_text' => $q['question_text'],
'question_type' => $q['question_type']
];
if ($q['question_type'] === 'radio' && empty($q['options'])) {
// Calculate mean, SD, frequency for rating questions (1-5)
$aStmt = $db->prepare("
SELECT answer_value, COUNT(*) as cnt
FROM response_answers
WHERE question_id = ? AND answer_value REGEXP '^[0-9]+$'
GROUP BY answer_value
ORDER BY answer_value
");
$aStmt->execute([$q['id']]);
$freqData = $aStmt->fetchAll();
$frequencies = [];
$values = [];
$totalAnswers = 0;
for ($i = 1; $i <= 5; $i++) {
$frequencies[$i] = ['value' => $i, 'count' => 0, 'percentage' => 0];
}
foreach ($freqData as $fd) {
$val = (int)$fd['answer_value'];
if ($val >= 1 && $val <= 5) {
$frequencies[$val]['count'] = (int)$fd['cnt'];
$totalAnswers += (int)$fd['cnt'];
for ($j = 0; $j < (int)$fd['cnt']; $j++) {
$values[] = $val;
}
}
}
// Calculate percentages
foreach ($frequencies as &$f) {
$f['percentage'] = $totalAnswers > 0 ? round(($f['count'] / $totalAnswers) * 100, 1) : 0;
}
unset($f);
// Calculate mean
$mean = count($values) > 0 ? array_sum($values) / count($values) : 0;
// Calculate SD
$sd = 0;
if (count($values) > 1) {
$sumSquaredDiff = 0;
foreach ($values as $v) {
$sumSquaredDiff += pow($v - $mean, 2);
}
$sd = sqrt($sumSquaredDiff / (count($values) - 1));
}
// Interpretation
$interpretation = '';
if ($mean >= 4.51) $interpretation = 'มากที่สุด';
elseif ($mean >= 3.51) $interpretation = 'มาก';
elseif ($mean >= 2.51) $interpretation = 'ปานกลาง';
elseif ($mean >= 1.51) $interpretation = 'น้อย';
else $interpretation = 'น้อยที่สุด';
$qStat['mean'] = round($mean, 2);
$qStat['sd'] = round($sd, 2);
$qStat['interpretation'] = $interpretation;
$qStat['frequencies'] = array_values($frequencies);
$qStat['total_answers'] = $totalAnswers;
} elseif ($q['question_type'] === 'text') {
// Collect text responses
$aStmt = $db->prepare("
SELECT answer_value FROM response_answers
WHERE question_id = ? AND answer_value != ''
ORDER BY id DESC
");
$aStmt->execute([$q['id']]);
$qStat['text_responses'] = array_column($aStmt->fetchAll(), 'answer_value');
$qStat['total_answers'] = count($qStat['text_responses']);
} elseif (in_array($q['question_type'], ['dropdown', 'checkbox', 'radio'])) {
// Frequency for dropdown/checkbox/custom radio
$aStmt = $db->prepare("
SELECT answer_value, COUNT(*) as cnt
FROM response_answers
WHERE question_id = ?
GROUP BY answer_value
ORDER BY cnt DESC
");
$aStmt->execute([$q['id']]);
$freqData = $aStmt->fetchAll();
$totalAnswers = array_sum(array_column($freqData, 'cnt'));
$qStat['frequencies'] = array_map(function($fd) use ($totalAnswers) {
return [
'value' => $fd['answer_value'],
'count' => (int)$fd['cnt'],
'percentage' => $totalAnswers > 0 ? round(((int)$fd['cnt'] / $totalAnswers) * 100, 1) : 0
];
}, $freqData);
$qStat['total_answers'] = $totalAnswers;
}
$sectionStats['questions'][] = $qStat;
}
// Build heading stats groups
$headingStats = [];
foreach ($headings as $h) {
$hStat = [
'id' => $h['id'],
'title' => $h['title'],
'description' => $h['description'],
'questions' => []
];
foreach ($sectionStats['questions'] as $qs) {
if (($qs['heading_id'] ?? null) == $h['id']) {
$hStat['questions'][] = $qs;
}
}
$headingStats[] = $hStat;
}
$sectionStats['headings'] = $headingStats;
// Filter loose questions (no heading)
$sectionStats['questions'] = array_values(array_filter($sectionStats['questions'], function($qs) {
return empty($qs['heading_id']);
}));
$result[] = $sectionStats;
}
jsonResponse([
'total_responses' => $totalResponses,
'sections' => $result
]);
}
+533
View File
@@ -0,0 +1,533 @@
<?php
/**
* Surveys CRUD API
* GET ?action=list — List surveys (filtered by role)
* GET ?action=get&id=X — Get survey with sections & questions
* GET ?action=getByLink&link=X — Get survey by unique link (public)
* POST ?action=create — Create survey with sections & questions
* POST ?action=update — Update survey
* POST ?action=delete — Delete survey
* POST ?action=generateLink — Generate new unique link
* POST ?action=updateStatus — Update survey status
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? '';
// Public endpoint for survey respondents
if ($action === 'getByLink') {
getByLink();
exit;
}
requireLogin();
switch ($action) {
case 'list':
listSurveys();
break;
case 'get':
getSurvey();
break;
case 'create':
createSurvey();
break;
case 'update':
updateSurvey();
break;
case 'delete':
deleteSurvey();
break;
case 'generateLink':
generateLink();
break;
case 'updateStatus':
updateStatus();
break;
case 'duplicate':
duplicateSurvey();
break;
case 'uploadTemplate':
uploadTemplate();
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function listSurveys() {
$db = getDB();
if (isAdmin()) {
$stmt = $db->query("
SELECT s.*, u.full_name as creator_name,
(SELECT COUNT(*) FROM survey_responses WHERE survey_id = s.id) as response_count
FROM surveys s
LEFT JOIN users u ON s.user_id = u.id
ORDER BY s.created_at DESC
");
} else {
$stmt = $db->prepare("
SELECT s.*, u.full_name as creator_name,
(SELECT COUNT(*) FROM survey_responses WHERE survey_id = s.id) as response_count
FROM surveys s
LEFT JOIN users u ON s.user_id = u.id
WHERE s.user_id = ?
ORDER BY s.created_at DESC
");
$stmt->execute([$_SESSION['user_id']]);
}
jsonResponse(['surveys' => $stmt->fetchAll()]);
}
function getSurvey() {
$id = (int)($_GET['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
$stmt = $db->prepare("SELECT * FROM surveys WHERE id = ?");
$stmt->execute([$id]);
$survey = $stmt->fetch();
if (!$survey) jsonResponse(['error' => 'ไม่พบแบบสอบถาม'], 404);
// Check access
if (!isAdmin() && $survey['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์เข้าถึง'], 403);
}
// Get sections with questions
$survey['sections'] = getSections($db, $id);
jsonResponse(['survey' => $survey]);
}
function getByLink() {
$link = $_GET['link'] ?? '';
if (empty($link)) jsonResponse(['error' => 'Missing link'], 400);
$db = getDB();
$stmt = $db->prepare("SELECT * FROM surveys WHERE unique_link = ? AND status = 'active'");
$stmt->execute([$link]);
$survey = $stmt->fetch();
if (!$survey) jsonResponse(['error' => 'ไม่พบแบบสอบถามหรือแบบสอบถามถูกปิด'], 404);
$survey['sections'] = getSections($db, $survey['id']);
// Remove sensitive data
unset($survey['user_id']);
jsonResponse(['survey' => $survey]);
}
function getSections($db, $surveyId) {
$stmt = $db->prepare("SELECT * FROM survey_sections WHERE survey_id = ? ORDER BY section_number");
$stmt->execute([$surveyId]);
$sections = $stmt->fetchAll();
foreach ($sections as &$section) {
// Load headings for this section
$hStmt = $db->prepare("SELECT * FROM survey_headings WHERE section_id = ? ORDER BY order_num");
$hStmt->execute([$section['id']]);
$headings = $hStmt->fetchAll();
// Load ALL questions for this section
$qStmt = $db->prepare("SELECT * FROM survey_questions WHERE section_id = ? ORDER BY order_num");
$qStmt->execute([$section['id']]);
$allQuestions = $qStmt->fetchAll();
// Separate questions: those with heading_id vs those without
$looseQuestions = [];
$headingQuestionsMap = [];
foreach ($allQuestions as &$q) {
$q['is_required'] = (int)$q['is_required'];
if ($q['options']) {
$q['options'] = json_decode($q['options'], true);
}
if ($q['heading_id']) {
$headingQuestionsMap[$q['heading_id']][] = $q;
} else {
$looseQuestions[] = $q;
}
}
// Attach questions to their headings
foreach ($headings as &$h) {
$h['questions'] = $headingQuestionsMap[$h['id']] ?? [];
}
$section['headings'] = $headings;
$section['questions'] = $looseQuestions; // questions not in any heading
}
return $sections;
}
function insertQuestion($db, $sectionId, $headingId, $q, $orderNum) {
$options = isset($q['options']) ? json_encode($q['options'], JSON_UNESCAPED_UNICODE) : null;
$stmt = $db->prepare("INSERT INTO survey_questions (section_id, heading_id, question_text, question_type, is_required, order_num, options, scale_min, scale_max, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$sectionId,
$headingId,
$q['question_text'],
$q['question_type'] ?? 'radio',
$q['is_required'] ?? 1,
$q['order_num'] ?? $orderNum,
$options,
$q['scale_min'] ?? 1,
$q['scale_max'] ?? 5,
$q['description'] ?? null
]);
return $db->lastInsertId();
}
function createSurvey() {
$input = getInput();
$title = trim($input['title'] ?? '');
$description = trim($input['description'] ?? '');
$projectId = $input['project_id'] ?? null;
if (empty($title)) {
jsonResponse(['error' => 'กรุณากรอกชื่อแบบสอบถาม'], 400);
}
$hasCertificate = (int)($input['has_certificate'] ?? 0);
$certificateTemplate = $input['certificate_template'] ?? null;
$certNameY = isset($input['cert_name_y']) ? (double)$input['cert_name_y'] : 82.5;
$certCodeY = isset($input['cert_code_y']) ? (double)$input['cert_code_y'] : 190.5;
$db = getDB();
$uniqueLink = bin2hex(random_bytes(16));
$db->beginTransaction();
try {
$stmt = $db->prepare("INSERT INTO surveys (user_id, project_id, title, description, unique_link, has_certificate, certificate_template, cert_name_y, cert_code_y) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$_SESSION['user_id'], $projectId, $title, $description, $uniqueLink, $hasCertificate, $certificateTemplate, $certNameY, $certCodeY]);
$surveyId = $db->lastInsertId();
// Create default sections with pre-defined questions based on user requirements
$sections = $input['sections'] ?? [
[
'section_number' => 1,
'title' => 'ส่วนที่ ข้อมูลทั่วไป',
'description' => 'ข้อมูลพื้นฐานของผู้ตอบ (ต้องตอบทุกข้อ)',
'questions' => [
['question_text' => 'เพศ', 'question_type' => 'radio', 'options' => ['ชาย', 'หญิง'], 'is_required' => 1],
['question_text' => 'ช่วงอายุ', 'question_type' => 'dropdown', 'options' => ['18-25 ปี', '26-35 ปี', '36-45 ปี', '46-55 ปี', '56 ปีขึ้นไป'], 'is_required' => 1],
['question_text' => 'คณะ / หน่วยงาน', 'question_type' => 'text', 'is_required' => 1]
]
],
[
'section_number' => 2,
'title' => 'ส่วนที่ การประเมินความพึงพอใจ',
'description' => 'ประเมินระดับความพึงพอใจ (5 = มากที่สุด, 1 = น้อยที่สุด)',
'questions' => [
['question_text' => 'ด้านเนื้อหาของการจัดโครงการ', 'question_type' => 'radio', 'is_required' => 1],
['question_text' => 'ด้านวิทยากร / ผู้บรรยาย', 'question_type' => 'radio', 'is_required' => 1],
['question_text' => 'ด้านสถานที่และความสะดวก', 'question_type' => 'radio', 'is_required' => 1]
]
],
[
'section_number' => 3,
'title' => 'ส่วนที่ ข้อเสนอแนะ',
'description' => 'ข้อเสนอแนะเพิ่มเติม',
'questions' => [
['question_text' => 'ข้อเสนอแนะอื่นๆ', 'question_type' => 'text', 'is_required' => 0]
]
]
];
foreach ($sections as $section) {
$stmt = $db->prepare("INSERT INTO survey_sections (survey_id, section_number, title, description, category) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$surveyId, $section['section_number'], $section['title'], $section['description'] ?? '', $section['category'] ?? 'rating']);
$sectionId = $db->lastInsertId();
// Insert headings and their questions
if (isset($section['headings'])) {
foreach ($section['headings'] as $hi => $heading) {
$hStmt = $db->prepare("INSERT INTO survey_headings (section_id, title, description, order_num) VALUES (?, ?, ?, ?)");
$hStmt->execute([$sectionId, $heading['title'], $heading['description'] ?? null, $heading['order_num'] ?? $hi]);
$headingId = $db->lastInsertId();
if (isset($heading['questions'])) {
foreach ($heading['questions'] as $i => $q) {
insertQuestion($db, $sectionId, $headingId, $q, $i);
}
}
}
}
// Insert loose questions (not in any heading)
if (isset($section['questions'])) {
foreach ($section['questions'] as $i => $q) {
insertQuestion($db, $sectionId, null, $q, $i);
}
}
}
$db->commit();
jsonResponse(['success' => true, 'id' => $surveyId, 'unique_link' => $uniqueLink, 'message' => 'สร้างแบบสอบถามสำเร็จ'], 201);
} catch (Exception $e) {
$db->rollBack();
jsonResponse(['error' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()], 500);
}
}
function updateSurvey() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
// Check ownership
$stmt = $db->prepare("SELECT user_id FROM surveys WHERE id = ?");
$stmt->execute([$id]);
$survey = $stmt->fetch();
if (!$survey) jsonResponse(['error' => 'ไม่พบแบบสอบถาม'], 404);
if (!isAdmin() && $survey['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์'], 403);
}
$db->beginTransaction();
try {
// Update survey info
if (isset($input['title'])) {
$hasCertificate = (int)($input['has_certificate'] ?? 0);
$certificateTemplate = $input['certificate_template'] ?? null;
$certNameY = isset($input['cert_name_y']) ? (double)$input['cert_name_y'] : 82.5;
$certCodeY = isset($input['cert_code_y']) ? (double)$input['cert_code_y'] : 190.5;
$stmt = $db->prepare("UPDATE surveys SET title = ?, description = ?, has_certificate = ?, certificate_template = ?, cert_name_y = ?, cert_code_y = ? WHERE id = ?");
$stmt->execute([$input['title'], $input['description'] ?? '', $hasCertificate, $certificateTemplate, $certNameY, $certCodeY, $id]);
}
// Update sections and questions
if (isset($input['sections'])) {
// Delete old headings first (they reference sections)
$db->prepare("DELETE h FROM survey_headings h JOIN survey_sections s ON h.section_id = s.id WHERE s.survey_id = ?")->execute([$id]);
// Delete old sections (cascades to questions)
$db->prepare("DELETE FROM survey_sections WHERE survey_id = ?")->execute([$id]);
foreach ($input['sections'] as $section) {
$stmt = $db->prepare("INSERT INTO survey_sections (survey_id, section_number, title, description, category) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$id, $section['section_number'], $section['title'], $section['description'] ?? '', $section['category'] ?? 'rating']);
$sectionId = $db->lastInsertId();
// Insert headings and their questions
if (isset($section['headings'])) {
foreach ($section['headings'] as $hi => $heading) {
$hStmt = $db->prepare("INSERT INTO survey_headings (section_id, title, description, order_num) VALUES (?, ?, ?, ?)");
$hStmt->execute([$sectionId, $heading['title'], $heading['description'] ?? null, $heading['order_num'] ?? $hi]);
$headingId = $db->lastInsertId();
if (isset($heading['questions'])) {
foreach ($heading['questions'] as $i => $q) {
insertQuestion($db, $sectionId, $headingId, $q, $i);
}
}
}
}
// Insert loose questions (not in any heading)
if (isset($section['questions'])) {
foreach ($section['questions'] as $i => $q) {
insertQuestion($db, $sectionId, null, $q, $i);
}
}
}
}
$db->commit();
jsonResponse(['success' => true, 'message' => 'อัปเดตแบบสอบถามสำเร็จ']);
} catch (Exception $e) {
$db->rollBack();
jsonResponse(['error' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()], 500);
}
}
function deleteSurvey() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
$stmt = $db->prepare("SELECT user_id FROM surveys WHERE id = ?");
$stmt->execute([$id]);
$survey = $stmt->fetch();
if (!$survey) jsonResponse(['error' => 'ไม่พบแบบสอบถาม'], 404);
if (!isAdmin() && $survey['user_id'] != $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่มีสิทธิ์'], 403);
}
$db->prepare("DELETE FROM surveys WHERE id = ?")->execute([$id]);
jsonResponse(['success' => true, 'message' => 'ลบแบบสอบถามสำเร็จ']);
}
function generateLink() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
$newLink = bin2hex(random_bytes(16));
$stmt = $db->prepare("UPDATE surveys SET unique_link = ?, status = 'active' WHERE id = ?");
$stmt->execute([$newLink, $id]);
$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
$fullLink = $baseUrl . '/survey/respond.php?link=' . $newLink;
jsonResponse(['success' => true, 'unique_link' => $newLink, 'full_url' => $fullLink]);
}
function updateStatus() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
$status = $input['status'] ?? '';
if (!$id || !in_array($status, ['draft', 'active', 'closed'])) {
jsonResponse(['error' => 'Invalid input'], 400);
}
$db = getDB();
$stmt = $db->prepare("UPDATE surveys SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
jsonResponse(['success' => true, 'message' => 'อัปเดตสถานะสำเร็จ']);
}
function duplicateSurvey() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
$stmt = $db->prepare("SELECT * FROM surveys WHERE id = ?");
$stmt->execute([$id]);
$survey = $stmt->fetch();
if (!$survey) jsonResponse(['error' => 'ไม่พบแบบสอบถาม'], 404);
$db->beginTransaction();
try {
$uniqueLink = bin2hex(random_bytes(16));
$stmt = $db->prepare("INSERT INTO surveys (user_id, project_id, title, description, unique_link, status, has_certificate, certificate_template, cert_name_y, cert_code_y) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$_SESSION['user_id'],
$survey['project_id'],
"สำเนาของ " . $survey['title'],
$survey['description'],
$uniqueLink,
'draft',
$survey['has_certificate'],
$survey['certificate_template'],
$survey['cert_name_y'],
$survey['cert_code_y']
]);
$newSurveyId = $db->lastInsertId();
// Copy sections
$stmt = $db->prepare("SELECT * FROM survey_sections WHERE survey_id = ? ORDER BY section_number");
$stmt->execute([$id]);
$sections = $stmt->fetchAll();
foreach ($sections as $section) {
$stmtInsert = $db->prepare("INSERT INTO survey_sections (survey_id, section_number, title, description, category) VALUES (?, ?, ?, ?, ?)");
$stmtInsert->execute([$newSurveyId, $section['section_number'], $section['title'], $section['description'], $section['category']]);
$newSectionId = $db->lastInsertId();
// Copy headings
$stmtH = $db->prepare("SELECT * FROM survey_headings WHERE section_id = ? ORDER BY order_num");
$stmtH->execute([$section['id']]);
$headings = $stmtH->fetchAll();
$headingIdMap = []; // old heading id -> new heading id
foreach ($headings as $h) {
$stmtInsertH = $db->prepare("INSERT INTO survey_headings (section_id, title, description, order_num) VALUES (?, ?, ?, ?)");
$stmtInsertH->execute([$newSectionId, $h['title'], $h['description'], $h['order_num']]);
$headingIdMap[$h['id']] = $db->lastInsertId();
}
// Copy questions
$stmtQ = $db->prepare("SELECT * FROM survey_questions WHERE section_id = ? ORDER BY order_num");
$stmtQ->execute([$section['id']]);
$questions = $stmtQ->fetchAll();
foreach ($questions as $q) {
$newHeadingId = $q['heading_id'] ? ($headingIdMap[$q['heading_id']] ?? null) : null;
$stmtInsertQ = $db->prepare("INSERT INTO survey_questions (section_id, heading_id, question_text, question_type, is_required, order_num, options, scale_min, scale_max, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmtInsertQ->execute([
$newSectionId,
$newHeadingId,
$q['question_text'],
$q['question_type'],
$q['is_required'],
$q['order_num'],
$q['options'],
$q['scale_min'],
$q['scale_max'],
$q['description']
]);
}
}
$db->commit();
jsonResponse(['success' => true, 'id' => $newSurveyId, 'message' => 'คัดลอกแบบสอบถามสำเร็จ']);
} catch (Exception $e) {
$db->rollBack();
jsonResponse(['error' => 'เกิดข้อผิดพลาด: ' . $e->getMessage()], 500);
}
}
function uploadTemplate() {
if (!isset($_FILES['template_image'])) {
jsonResponse(['error' => 'ไม่พบไฟล์ที่อัปโหลด'], 400);
}
$file = $_FILES['template_image'];
if ($file['error'] !== UPLOAD_ERR_OK) {
jsonResponse(['error' => 'การอัปโหลดไฟล์ล้มเหลว: รหัสข้อผิดพลาด ' . $file['error']], 400);
}
// ตรวจสอบขนาดไฟล์ (สูงสุด 5MB)
if ($file['size'] > 5 * 1024 * 1024) {
jsonResponse(['error' => 'ขนาดไฟล์รูปภาพต้องไม่เกิน 5MB'], 400);
}
// ตรวจสอบนามสกุล/ประเภทไฟล์ (ต้องเป็น PNG หรือ JPEG เท่านั้น)
$imageInfo = getimagesize($file['tmp_name']);
if ($imageInfo === false) {
jsonResponse(['error' => 'ไฟล์ที่อัปโหลดไม่ใช่รูปภาพที่ถูกต้อง'], 400);
}
$mimeType = $imageInfo['mime'];
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/jpg'];
if (!in_array($mimeType, $allowedMimeTypes)) {
jsonResponse(['error' => 'อนุญาตให้อัปโหลดเฉพาะไฟล์รูปภาพ PNG หรือ JPG/JPEG เท่านั้น'], 400);
}
// สร้างโฟลเดอร์สำหรับเก็บไฟล์รูปภาพ (ถ้ายังไม่มี)
$uploadDir = __DIR__ . '/../uploads/templates';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
chmod($uploadDir, 0777);
// สร้างชื่อไฟล์ใหม่เพื่อป้องกันไฟล์ซ้ำและแฮกเกอร์
$ext = ($mimeType === 'image/png') ? 'png' : 'jpg';
$fileName = 'tpl_' . time() . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$destPath = $uploadDir . '/' . $fileName;
if (move_uploaded_file($file['tmp_name'], $destPath)) {
chmod($destPath, 0666);
$relativePath = 'uploads/templates/' . $fileName;
jsonResponse(['success' => true, 'template_path' => $relativePath, 'message' => 'อัปโหลดเทมเพลตสำเร็จ']);
} else {
jsonResponse(['error' => 'ไม่สามารถย้ายไฟล์ไปยังโฟลเดอร์ปลายทางได้'], 500);
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
/**
* Users CRUD API (Admin only)
* GET ?action=list — List all users
* GET ?action=get&id=X — Get user by ID
* POST ?action=create — Create user
* POST ?action=update — Update user
* POST ?action=delete — Delete user
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
header('Content-Type: application/json; charset=utf-8');
requireAdmin();
$action = $_GET['action'] ?? '';
switch ($action) {
case 'list':
listUsers();
break;
case 'get':
getUser();
break;
case 'create':
createUser();
break;
case 'update':
updateUser();
break;
case 'delete':
deleteUser();
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
function listUsers() {
$db = getDB();
$stmt = $db->query("SELECT id, username, role, full_name, personnel_code, subject_area, branch_code, email, created_at FROM users ORDER BY created_at DESC");
jsonResponse(['users' => $stmt->fetchAll()]);
}
function getUser() {
$id = (int)($_GET['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$db = getDB();
$stmt = $db->prepare("SELECT id, username, role, full_name, personnel_code, subject_area, branch_code, email, created_at FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch();
if (!$user) jsonResponse(['error' => 'ไม่พบผู้ใช้'], 404);
jsonResponse(['user' => $user]);
}
function createUser() {
$input = getInput();
$username = trim($input['username'] ?? '');
$password = trim($input['password'] ?? '');
$role = $input['role'] ?? 'teacher';
$fullName = trim($input['full_name'] ?? '');
$personnelCode = trim($input['personnel_code'] ?? '');
$subjectArea = trim($input['subject_area'] ?? '');
$branchCode = trim($input['branch_code'] ?? '');
$email = trim($input['email'] ?? '');
if (empty($username) || empty($password) || empty($fullName)) {
jsonResponse(['error' => 'กรุณากรอกข้อมูลให้ครบถ้วน'], 400);
}
$db = getDB();
// Check duplicate username
$stmt = $db->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
jsonResponse(['error' => 'ชื่อผู้ใช้นี้มีอยู่แล้ว'], 409);
}
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (username, password_hash, role, full_name, personnel_code, subject_area, branch_code, email) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$username, $passwordHash, $role, $fullName, $personnelCode, $subjectArea, $branchCode, $email]);
jsonResponse(['success' => true, 'id' => $db->lastInsertId(), 'message' => 'สร้างผู้ใช้สำเร็จ'], 201);
}
function updateUser() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
$fullName = trim($input['full_name'] ?? '');
$role = $input['role'] ?? '';
$personnelCode = trim($input['personnel_code'] ?? '');
$subjectArea = trim($input['subject_area'] ?? '');
$branchCode = trim($input['branch_code'] ?? '');
$db = getDB();
$fields = [];
$params = [];
if (!empty($fullName)) { $fields[] = "full_name = ?"; $params[] = $fullName; }
if (!empty($role)) { $fields[] = "role = ?"; $params[] = $role; }
if ($personnelCode !== '') { $fields[] = "personnel_code = ?"; $params[] = $personnelCode; }
if ($subjectArea !== '') { $fields[] = "subject_area = ?"; $params[] = $subjectArea; }
if ($branchCode !== '') { $fields[] = "branch_code = ?"; $params[] = $branchCode; }
if (isset($input['email'])) { $fields[] = "email = ?"; $params[] = trim($input['email']); }
// Update password if provided
if (!empty($input['password'])) {
$fields[] = "password_hash = ?";
$params[] = password_hash($input['password'], PASSWORD_DEFAULT);
}
if (empty($fields)) {
jsonResponse(['error' => 'ไม่มีข้อมูลที่ต้องอัปเดต'], 400);
}
$params[] = $id;
$sql = "UPDATE users SET " . implode(', ', $fields) . " WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->execute($params);
jsonResponse(['success' => true, 'message' => 'อัปเดตผู้ใช้สำเร็จ']);
}
function deleteUser() {
$input = getInput();
$id = (int)($input['id'] ?? 0);
if (!$id) jsonResponse(['error' => 'Missing ID'], 400);
// Don't delete yourself
if ($id == $_SESSION['user_id']) {
jsonResponse(['error' => 'ไม่สามารถลบบัญชีตัวเองได้'], 400);
}
$db = getDB();
$stmt = $db->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
jsonResponse(['success' => true, 'message' => 'ลบผู้ใช้สำเร็จ']);
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* Certificate Verification API
* GET ?code=TPU-2569-0016 — Verify a certificate by its unique code
*/
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json; charset=utf-8');
$code = trim($_GET['code'] ?? '');
if (empty($code)) {
jsonResponse(['success' => false, 'error' => 'กรุณาระบุรหัสตรวจสอบเกียรติบัตร'], 400);
}
$db = getDB();
// ค้นหารหัสเกียรติบัตรในฐานข้อมูล
$stmt = $db->prepare("
SELECT sr.id, sr.respondent_info, sr.submitted_at, s.title as survey_title
FROM survey_responses sr
JOIN surveys s ON sr.survey_id = s.id
WHERE sr.cert_code = ?
");
$stmt->execute([$code]);
$record = $stmt->fetch();
if (!$record) {
jsonResponse(['success' => false, 'error' => 'ไม่พบรหัสเกียรติบัตรนี้ในระบบ หรือรหัสอาจไม่ถูกต้อง'], 404);
}
// ถอดรหัสชื่อผู้เข้าร่วม
$respondentInfo = json_decode($record['respondent_info'], true) ?: [];
$name = $respondentInfo['name'] ?? '';
// ถ้ารายละเอียดของชื่อว่าง (เช่น กรณีข้อมูลเก่าที่ใช้ Fallback)
// ให้พยายามดึงข้อมูลจากตารางคำตอบจริงที่มีคำถามเกี่ยวกับชื่อ
if (empty($name)) {
$stmtQ = $db->prepare("
SELECT q.id
FROM survey_questions q
JOIN survey_sections s ON q.section_id = s.id
WHERE s.survey_id = (SELECT survey_id FROM survey_responses WHERE id = ?)
AND (q.question_text LIKE '%ชื่อ%' OR q.question_text LIKE '%นามสกุล%')
LIMIT 1
");
$stmtQ->execute([$record['id']]);
$qRow = $stmtQ->fetch();
if ($qRow) {
$nameQuestionId = $qRow['id'];
$stmtA = $db->prepare("
SELECT answer_value FROM response_answers
WHERE response_id = ? AND question_id = ?
");
$stmtA->execute([$record['id'], $nameQuestionId]);
$aRow = $stmtA->fetch();
if ($aRow) {
$name = trim($aRow['answer_value']);
}
}
}
if (empty($name)) {
$name = 'ผู้เข้าร่วมโครงการ';
}
// ส่งรายละเอียดกลับไปหาผู้ขอตรวจสอบ
jsonResponse([
'success' => true,
'code' => $code,
'recipient_name' => $name,
'course_title' => $record['survey_title'],
'issue_date' => $record['submitted_at']
]);