diff --git a/api/export.php b/api/export.php index cc5815a..680af54 100644 --- a/api/export.php +++ b/api/export.php @@ -45,6 +45,41 @@ switch ($action) { jsonResponse(['error' => 'Invalid action'], 400); } +function parseRatingScore($str) { + $trimmed = trim((string)$str); + if ($trimmed === '') return null; + + if (is_numeric($trimmed)) { + $num = (int)$trimmed; + return ($num >= 1 && $num <= 5) ? $num : null; + } + + if (preg_match('/^([1-5])\s*[\.\:\-\(\s]/u', $trimmed, $m)) { + return (int)$m[1]; + } + + if (mb_strpos($trimmed, 'มากที่สุด') !== false || mb_strpos($trimmed, 'ดีมาก') !== false || mb_strpos($trimmed, 'ยอดเยี่ยม') !== false || mb_strpos($trimmed, 'ดีเยี่ยม') !== false || mb_strpos($trimmed, 'เห็นด้วยอย่างยิ่ง') !== false) { + return 5; + } + if (mb_strpos($trimmed, 'น้อยที่สุด') !== false || mb_strpos($trimmed, 'ไม่เห็นด้วยอย่างยิ่ง') !== false) { + return 1; + } + if (mb_strpos($trimmed, 'มาก') !== false || mb_strpos($trimmed, 'ดี') !== false || mb_strpos($trimmed, 'เห็นด้วย') !== false) { + return 4; + } + if (mb_strpos($trimmed, 'ปานกลาง') !== false || mb_strpos($trimmed, 'พอใช้') !== false || mb_strpos($trimmed, 'ไม่แน่ใจ') !== false) { + return 3; + } + if (mb_strpos($trimmed, 'น้อย') !== false || mb_strpos($trimmed, 'ควรปรับปรุง') !== false || mb_strpos($trimmed, 'ต้องปรับปรุง') !== false || mb_strpos($trimmed, 'ไม่เห็นด้วย') !== false) { + return 2; + } + if (mb_strpos($trimmed, 'ปรับปรุง') !== false) { + return 1; + } + + return null; +} + function getStatsData($surveyId) { $db = getDB(); @@ -78,7 +113,7 @@ function getStatsData($surveyId) { $aStmt = $db->prepare(" SELECT answer_value, COUNT(*) as cnt FROM response_answers - WHERE question_id = ? AND answer_value REGEXP '^[0-9]+$' + WHERE question_id = ? AND answer_value != '' GROUP BY answer_value "); $aStmt->execute([$q['id']]); @@ -89,10 +124,13 @@ function getStatsData($surveyId) { for ($i = 1; $i <= 5; $i++) $freq[$i] = 0; foreach ($freqData as $fd) { - $val = (int)$fd['answer_value']; + $rawVal = trim($fd['answer_value']); + $cnt = (int)$fd['cnt']; + $val = parseRatingScore($rawVal); + if ($val >= 1 && $val <= 5) { - $freq[$val] = (int)$fd['cnt']; - for ($j = 0; $j < (int)$fd['cnt']; $j++) $values[] = $val; + $freq[$val] += $cnt; + for ($j = 0; $j < $cnt; $j++) $values[] = $val; } } @@ -116,7 +154,7 @@ function getStatsData($surveyId) { 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 = $db->prepare("SELECT answer_value, COUNT(*) as cnt FROM response_answers WHERE question_id = ? AND answer_value != '' GROUP BY answer_value ORDER BY cnt DESC"); $aStmt->execute([$q['id']]); $q['frequencies'] = $aStmt->fetchAll(); } diff --git a/api/import_responses.php b/api/import_responses.php index 7c0fc28..90e6676 100644 --- a/api/import_responses.php +++ b/api/import_responses.php @@ -41,7 +41,7 @@ if (!$handle) { jsonResponse(['error' => 'ไม่สามารถเปิดไฟล์ CSV ได้'], 500); } -// Check UTF-8 BOM or UTF-16 +// Check UTF-8 BOM $bom = fread($handle, 3); if ($bom !== "\xEF\xBB\xBF") { rewind($handle); @@ -61,7 +61,7 @@ $cleanHeaders = array_map(function($h) { // Fetch all questions for this survey $stmt = $db->prepare(" - SELECT q.id, q.question_text, q.question_type + SELECT q.id, q.question_text, q.question_type, q.options FROM survey_questions q JOIN survey_sections s ON q.section_id = s.id WHERE s.survey_id = ? @@ -75,16 +75,57 @@ if (empty($questions)) { jsonResponse(['error' => 'ไม่พบข้อคำถามในแบบสอบถามนี้'], 400); } -// Map CSV Column Index -> question_id -$colToQuestionMap = []; - +// Helper: normalize text for comparison 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); + $str = preg_replace('/^[\d๑-๙\.]*[\.\)\s\-:\x{00A0}]*/u', '', $str); return trim($str); } +// Helper to parse Likert rating values from numbers or Thai text +function parseRatingScore($str) { + $trimmed = trim((string)$str); + if ($trimmed === '') return null; + + if (is_numeric($trimmed)) { + $num = (int)$trimmed; + return ($num >= 1 && $num <= 5) ? $num : null; + } + + if (preg_match('/^([1-5])\s*[\.\:\-\(\s]/u', $trimmed, $m)) { + return (int)$m[1]; + } + + if (mb_strpos($trimmed, 'มากที่สุด') !== false || mb_strpos($trimmed, 'ดีมาก') !== false || mb_strpos($trimmed, 'ยอดเยี่ยม') !== false || mb_strpos($trimmed, 'ดีเยี่ยม') !== false || mb_strpos($trimmed, 'เห็นด้วยอย่างยิ่ง') !== false) { + return 5; + } + if (mb_strpos($trimmed, 'น้อยที่สุด') !== false || mb_strpos($trimmed, 'ไม่เห็นด้วยอย่างยิ่ง') !== false) { + return 1; + } + if (mb_strpos($trimmed, 'มาก') !== false || mb_strpos($trimmed, 'ดี') !== false || mb_strpos($trimmed, 'เห็นด้วย') !== false) { + return 4; + } + if (mb_strpos($trimmed, 'ปานกลาง') !== false || mb_strpos($trimmed, 'พอใช้') !== false || mb_strpos($trimmed, 'ไม่แน่ใจ') !== false) { + return 3; + } + if (mb_strpos($trimmed, 'น้อย') !== false || mb_strpos($trimmed, 'ควรปรับปรุง') !== false || mb_strpos($trimmed, 'ต้องปรับปรุง') !== false || mb_strpos($trimmed, 'ไม่เห็นด้วย') !== false) { + return 2; + } + if (mb_strpos($trimmed, 'ปรับปรุง') !== false) { + return 1; + } + + return null; +} + +// Map CSV Column Index -> question_id +$colToQuestionMap = []; +$questionsById = []; +foreach ($questions as $q) { + $questionsById[$q['id']] = $q; +} + foreach ($cleanHeaders as $colIdx => $headerText) { if (empty($headerText) || strtolower($headerText) === 'timestamp' || strtolower($headerText) === 'ประทับเวลา') { continue; @@ -92,11 +133,22 @@ foreach ($cleanHeaders as $colIdx => $headerText) { $normHeader = normalizeText($headerText); + // Extract inside bracket if Google Forms grid header e.g. "Heading [Question]" + $bracketText = ''; + if (preg_match('/\[(.*?)\]/', $headerText, $m)) { + $bracketText = normalizeText($m[1]); + } + // 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) { + if ( + $normHeader === $normQText || + ($bracketText !== '' && ($bracketText === $normQText || strpos($bracketText, $normQText) !== false || strpos($normQText, $bracketText) !== false)) || + strpos($normHeader, $normQText) !== false || + strpos($normQText, $normHeader) !== false + ) { $colToQuestionMap[$colIdx] = $q['id']; break; } @@ -104,7 +156,7 @@ foreach ($cleanHeaders as $colIdx => $headerText) { } // Fallback: If column mapping count is low, map columns sequentially to questions -if (count($colToQuestionMap) === 0) { +if (count($colToQuestionMap) < count($questions) / 2) { $qIdx = 0; foreach ($cleanHeaders as $colIdx => $headerText) { if (strtolower($headerText) === 'timestamp' || strtolower($headerText) === 'ประทับเวลา') continue; @@ -145,6 +197,15 @@ try { foreach ($colToQuestionMap as $colIdx => $qId) { $val = isset($row[$colIdx]) ? trim($row[$colIdx]) : ''; if ($val !== '') { + $targetQ = $questionsById[$qId] ?? null; + // Normalize rating score if it's a 5-point rating scale question + if ($targetQ && $targetQ['question_type'] === 'radio' && empty($targetQ['options'])) { + $score = parseRatingScore($val); + if ($score !== null) { + $val = (string)$score; + } + } + $stmtAns = $db->prepare("INSERT INTO response_answers (response_id, question_id, answer_value) VALUES (?, ?, ?)"); $stmtAns->execute([$responseId, $qId, $val]); } diff --git a/api/statistics.php b/api/statistics.php index 9f2c8f6..bdfecef 100644 --- a/api/statistics.php +++ b/api/statistics.php @@ -28,6 +28,44 @@ if (!isAdmin()) { } } +/** + * Helper to parse Likert rating values from numbers or Thai text + */ +function parseRatingScore($str) { + $trimmed = trim((string)$str); + if ($trimmed === '') return null; + + if (is_numeric($trimmed)) { + $num = (int)$trimmed; + return ($num >= 1 && $num <= 5) ? $num : null; + } + + if (preg_match('/^([1-5])\s*[\.\:\-\(\s]/u', $trimmed, $m)) { + return (int)$m[1]; + } + + if (mb_strpos($trimmed, 'มากที่สุด') !== false || mb_strpos($trimmed, 'ดีมาก') !== false || mb_strpos($trimmed, 'ยอดเยี่ยม') !== false || mb_strpos($trimmed, 'ดีเยี่ยม') !== false || mb_strpos($trimmed, 'เห็นด้วยอย่างยิ่ง') !== false) { + return 5; + } + if (mb_strpos($trimmed, 'น้อยที่สุด') !== false || mb_strpos($trimmed, 'ไม่เห็นด้วยอย่างยิ่ง') !== false) { + return 1; + } + if (mb_strpos($trimmed, 'มาก') !== false || mb_strpos($trimmed, 'ดี') !== false || mb_strpos($trimmed, 'เห็นด้วย') !== false) { + return 4; + } + if (mb_strpos($trimmed, 'ปานกลาง') !== false || mb_strpos($trimmed, 'พอใช้') !== false || mb_strpos($trimmed, 'ไม่แน่ใจ') !== false) { + return 3; + } + if (mb_strpos($trimmed, 'น้อย') !== false || mb_strpos($trimmed, 'ควรปรับปรุง') !== false || mb_strpos($trimmed, 'ต้องปรับปรุง') !== false || mb_strpos($trimmed, 'ไม่เห็นด้วย') !== false) { + return 2; + } + if (mb_strpos($trimmed, 'ปรับปรุง') !== false) { + return 1; + } + + return null; +} + switch ($action) { case 'summary': getSummary($surveyId); @@ -52,24 +90,44 @@ function getSummary($surveyId) { $stmt->execute([$surveyId]); $surveyInfo = $stmt->fetch(); - // Get overall mean and SD for radio/rating questions + // Fetch all answers for rating questions in this survey and compute mean/sd $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 + SELECT ra.answer_value 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]+$' + WHERE ss.survey_id = ? AND sq.question_type = 'radio' AND sq.options IS NULL "); $stmt->execute([$surveyId]); - $overall = $stmt->fetch(); + $rows = $stmt->fetchAll(PDO::FETCH_COLUMN); + + $scores = []; + foreach ($rows as $ans) { + $sc = parseRatingScore($ans); + if ($sc !== null) { + $scores[] = $sc; + } + } + + $overallMean = 0; + $overallSd = 0; + + if (count($scores) > 0) { + $overallMean = array_sum($scores) / count($scores); + if (count($scores) > 1) { + $sumSq = 0; + foreach ($scores as $s) { + $sumSq += pow($s - $overallMean, 2); + } + $overallSd = sqrt($sumSq / (count($scores) - 1)); + } + } 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 + 'overall_mean' => round($overallMean, 2), + 'overall_sd' => round($overallSd, 2) ]); } @@ -114,11 +172,11 @@ function getDetail($surveyId) { ]; if ($q['question_type'] === 'radio' && empty($q['options'])) { - // Calculate mean, SD, frequency for rating questions (1-5) + // Rating scale question (1-5) $aStmt = $db->prepare(" SELECT answer_value, COUNT(*) as cnt FROM response_answers - WHERE question_id = ? AND answer_value REGEXP '^[0-9]+$' + WHERE question_id = ? AND answer_value != '' GROUP BY answer_value ORDER BY answer_value "); @@ -134,11 +192,14 @@ function getDetail($surveyId) { } foreach ($freqData as $fd) { - $val = (int)$fd['answer_value']; + $rawVal = trim($fd['answer_value']); + $cnt = (int)$fd['cnt']; + $val = parseRatingScore($rawVal); + if ($val >= 1 && $val <= 5) { - $frequencies[$val]['count'] = (int)$fd['cnt']; - $totalAnswers += (int)$fd['cnt']; - for ($j = 0; $j < (int)$fd['cnt']; $j++) { + $frequencies[$val]['count'] += $cnt; + $totalAnswers += $cnt; + for ($j = 0; $j < $cnt; $j++) { $values[] = $val; } } @@ -193,7 +254,7 @@ function getDetail($surveyId) { $aStmt = $db->prepare(" SELECT answer_value, COUNT(*) as cnt FROM response_answers - WHERE question_id = ? + WHERE question_id = ? AND answer_value != '' GROUP BY answer_value ORDER BY cnt DESC "); diff --git a/stats.html b/stats.html index b4bffba..386dab6 100644 --- a/stats.html +++ b/stats.html @@ -24,44 +24,62 @@ } } - +
-
- +
+
กำลังโหลดผลสถิติ...
-
+
@@ -75,7 +93,7 @@ - - + +