'Invalid action'], 400); } } /** * Handle PDF upload and extraction */ function parsePdf() { if (!isset($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) { $errorMsg = 'กรุณาเลือกไฟล์ PDF'; if (isset($_FILES['pdf_file']['error'])) { switch ($_FILES['pdf_file']['error']) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $errorMsg = 'ขนาดไฟล์ใหญ่เกินขีดจำกัดของเซิร์ฟเวอร์'; break; case UPLOAD_ERR_NO_FILE: $errorMsg = 'ไม่มีไฟล์ถูกอัปโหลด'; break; } } jsonResponse(['error' => $errorMsg], 400); } $file = $_FILES['pdf_file']; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if ($ext !== 'pdf') { jsonResponse(['error' => 'รองรับเฉพาะไฟล์เอกสาร PDF เท่านั้น'], 400); } $tmpPath = $file['tmp_name']; try { $parser = new Parser(); $pdf = $parser->parseFile($tmpPath); $rawText = $pdf->getText(); } catch (\Throwable $e) { jsonResponse(['error' => 'ไม่สามารถอ่านไฟล์ PDF ได้: ' . $e->getMessage()], 500); } $rawText = trim($rawText); if (empty($rawText)) { jsonResponse(['error' => 'ไม่พบข้อความในไฟล์ PDF (อาจเป็นไฟล์สแกนรูปภาพล้วนที่ไม่มี Text Layer)'], 400); } // Parse structured survey from raw text $parsedSurvey = parseThaiSurveyText($rawText, pathinfo($file['name'], PATHINFO_FILENAME)); jsonResponse([ 'success' => true, 'message' => 'อ่านและวิเคราะห์โครงสร้างแบบประเมินจาก PDF สำเร็จ', 'raw_text_preview' => mb_substr($rawText, 0, 500, 'UTF-8') . '...', 'survey' => $parsedSurvey ]); } /** * Save finalized survey to DB */ function confirmImport() { $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' => 'ข้อมูลแบบประเมินไม่ถูกต้อง หรือไม่มีชื่อแบบประเมิน'], 400); } $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'] ?? 1, $projectId, $surveyData['title'], $surveyData['description'] ?? '', $uniqueLink ]); $surveyId = $db->lastInsertId(); $sections = $surveyData['sections'] ?? []; $secNum = 1; foreach ($sections as $sec) { $stmt = $db->prepare("INSERT INTO survey_sections (survey_id, section_number, title, description, category) VALUES (?, ?, ?, ?, ?)"); $secCategory = $sec['category'] ?? 'rating'; $stmt->execute([ $surveyId, $secNum, $sec['title'] ?? ('ตอนที่ ' . $secNum), $sec['description'] ?? '', $secCategory ]); $sectionId = $db->lastInsertId(); $orderNum = 1; // Direct questions in this section if (!empty($sec['questions'])) { foreach ($sec['questions'] as $q) { insertQuestionRecord($db, $sectionId, null, $q, $orderNum++); } } // Headings with sub-questions (e.g. ด้านวิทยากร, ด้านสถานที่) if (!empty($sec['headings'])) { $headingOrder = 1; foreach ($sec['headings'] as $h) { $stmtH = $db->prepare("INSERT INTO survey_headings (section_id, title, description, order_num) VALUES (?, ?, ?, ?)"); $stmtH->execute([ $sectionId, $h['title'] ?? ('ด้านที่ ' . $headingOrder), $h['description'] ?? '', $headingOrder++ ]); $headingId = $db->lastInsertId(); if (!empty($h['questions'])) { foreach ($h['questions'] as $hq) { insertQuestionRecord($db, $sectionId, $headingId, $hq, $orderNum++); } } } } $secNum++; } $db->commit(); jsonResponse([ 'success' => true, 'message' => 'สร้างแบบประเมินจาก PDF เรียบร้อยแล้ว', 'survey_id' => $surveyId, 'unique_link' => $uniqueLink, 'title' => $surveyData['title'] ]); } catch (\Throwable $e) { $db->rollBack(); jsonResponse(['error' => 'บันทึกข้อมูลไม่สำเร็จ: ' . $e->getMessage()], 500); } } function insertQuestionRecord($db, $sectionId, $headingId, $q, $orderNum) { $qText = trim($q['question_text'] ?? ''); if (empty($qText)) return; $qType = $q['question_type'] ?? 'radio'; $isRequired = isset($q['is_required']) ? (int)$q['is_required'] : 1; $options = !empty($q['options']) && is_array($q['options']) ? json_encode(array_values($q['options']), JSON_UNESCAPED_UNICODE) : null; $scaleMin = $q['scale_min'] ?? 1; $scaleMax = $q['scale_max'] ?? 5; $desc = $q['description'] ?? ''; $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, $qText, $qType, $isRequired, $orderNum, $options, $scaleMin, $scaleMax, $desc ]); } /** * Intelligent Thai Survey Pattern Parser */ function parseThaiSurveyText($text, $defaultTitle = 'แบบประเมินความพึงพอใจ') { // 1. Normalize line endings and whitespace $text = str_replace(["\r\n", "\r"], "\n", $text); // Replace special unicode bullets/boxes with standard marks $text = preg_replace('/[\x{25A0}\x{25A1}\x{25CB}\x{25CF}\x{2610}\x{2611}\x{274F}\x{25EF}]/u', '[ ]', $text); $rawLines = explode("\n", $text); $lines = []; foreach ($rawLines as $line) { $trimmed = trim($line); if ($trimmed !== '') { $lines[] = $trimmed; } } // 2. Extract Document Title and Description $title = ''; $description = ''; $startIndex = 0; $titleLines = []; // Filter out standard scale headers/garbage lines like "5 4 3 2 1", "มากที่สุด มาก ปานกลาง..." $ignoredPatterns = [ '/^ระดับความพึงพอใจ/u', '/^ระดับความคิดเห็น/u', '/^เกณฑ์การประเมิน/u', '/^5\s+4\s+3\s+2\s+1/u', '/^หมายเหตุ\s*:/u', '/^หน้า\s*\d+/u', '/^page\s*\d+/i' ]; for ($i = 0; $i < min(8, count($lines)); $i++) { $line = $lines[$i]; // If we hit "ตอนที่" or "ส่วนที่" or "คำชี้แจง", stop title accumulation if (preg_match('/^(ตอนที่|ส่วนที่|คำชี้แจง|คำอธิบาย|ข้อชี้แจง)/u', $line)) { $startIndex = $i; break; } if (preg_match('/(แบบประเมิน|แบบสอบถาม|โครงการ|การอบรม|รายงานการประเมิน|แบบสำรวจ)/u', $line)) { $titleLines[] = $line; $startIndex = $i + 1; } elseif (empty($titleLines) && $i === 0) { $titleLines[] = $line; $startIndex = 1; } } $title = !empty($titleLines) ? implode(' ', $titleLines) : ($defaultTitle ?: 'แบบประเมินความพึงพอใจ'); // 3. Scan for "คำชี้แจง" (Description) for ($i = $startIndex; $i < min($startIndex + 6, count($lines)); $i++) { $line = $lines[$i]; if (preg_match('/^(คำชี้แจง|คำอธิบาย|ข้อชี้แจง)[\s\:\.\-–]*(.*)/u', $line, $m)) { $description = trim($m[2]); if (empty($description) && isset($lines[$i + 1])) { $description = $lines[$i + 1]; $startIndex = $i + 2; } else { $startIndex = $i + 1; } break; } } // 4. Partition lines into Sections $sections = []; $currentSection = null; for ($i = $startIndex; $i < count($lines); $i++) { $line = $lines[$i]; // Check if line is a Section Header: "ตอนที่ 1...", "ส่วนที่ 1...", "ตอนที่ 2...", "ตอนที่ 3..." if (preg_match('/^(ตอนที่|ส่วนที่)\s*([0-9๑-๙]+|หนึ่ง|สอง|สาม|สี่|ห้า)[\s\:\.\-–]*(.*)/u', $line, $matches)) { if ($currentSection !== null) { $sections[] = $currentSection; } $rawSecNum = $matches[2]; $secNumMap = ['หนึ่ง' => 1, 'สอง' => 2, 'สาม' => 3, 'สี่' => 4, '๑' => 1, '๒' => 2, '๓' => 3, '๔' => 4]; $secNum = $secNumMap[$rawSecNum] ?? (int)$rawSecNum; if ($secNum <= 0) $secNum = count($sections) + 1; $secTitle = trim($matches[0]); $secCategory = 'rating'; if (preg_match('/(ข้อมูลทั่วไป|สถานภาพ|ข้อมูลส่วนตัว|สถานะ)/u', $secTitle)) { $secCategory = 'general'; } elseif (preg_match('/(ข้อเสนอแนะ|ความคิดเห็น|ความเห็นเพิ่มเติม|ข้อคิดเห็น)/u', $secTitle)) { $secCategory = 'suggestion'; } elseif (preg_match('/(ความพึงพอใจ|ระดับความพึงพอใจ|การประเมินผล|เกณฑ์)/u', $secTitle)) { $secCategory = 'rating'; } $currentSection = [ 'section_number' => $secNum, 'title' => $secTitle, 'description' => '', 'category' => $secCategory, 'raw_lines' => [] ]; continue; } if ($currentSection === null) { // Default section 1 if not explicitly declared $currentSection = [ 'section_number' => 1, 'title' => 'ตอนที่ 1 ข้อมูลทั่วไปและข้อประเมิน', 'description' => '', 'category' => 'general', 'raw_lines' => [] ]; } $currentSection['raw_lines'][] = $line; } if ($currentSection !== null) { $sections[] = $currentSection; } // 5. Parse each section's contents (Headings, Questions, Options, Rating Scales) $parsedSections = []; $secCounter = 1; foreach ($sections as $sec) { $secCategory = $sec['category']; $rawSecLines = $sec['raw_lines']; $headings = []; $questions = []; // Check if section contains sub-headings (e.g. ด้านที่ 1..., 1. ด้าน..., 2. ด้าน...) $currentHeading = null; $currentQuestion = null; for ($j = 0; $j < count($rawSecLines); $j++) { $line = $rawSecLines[$j]; // Ignore table scale legends / headers $shouldSkip = false; foreach ($ignoredPatterns as $pat) { if (preg_match($pat, $line)) { $shouldSkip = true; break; } } if ($shouldSkip) continue; // Check if line is a Heading / Dimension: // "ด้านที่ 1 ด้านวิทยากร", "1. ด้านวิทยากร", "ด้านกระบวนการขั้นตอน", "1. ด้านเนื้อหา" if (preg_match('/^(ด้านที่\s*[0-9๑-๙]+|[0-9๑-๙]+\.\s*ด้าน|ด้าน[ก-๙]+)[\s\:\.\-–]*(.*)/u', $line) && !preg_match('/\[\s*\]/u', $line)) { // Finalize previous question if ($currentQuestion !== null) { if ($currentHeading !== null) { $currentHeading['questions'][] = $currentQuestion; } else { $questions[] = $currentQuestion; } $currentQuestion = null; } if ($currentHeading !== null) { $headings[] = $currentHeading; } $currentHeading = [ 'title' => $line, 'description' => '', 'questions' => [] ]; continue; } // Check if line is a question // e.g. "1. เพศ", "1.1 วิทยากรมีความรู้", "2. สถานภาพ", "• ความเหมาะสมของสถานที่" // or starting with number: "1.1", "1.", "2)", "(1)" $isNumberedQuestion = preg_match('/^([0-9๑-๙]+\.[0-9๑-๙]+[\.\)]?\s*|[0-9๑-๙]+[\.\)]\s*|[\-\•\*]\s+)(.*)/u', $line, $qMatch); // Or general question prompt: "เพศ :", "ระดับการศึกษา :", "ชั้นปี :" $isPromptQuestion = preg_match('/^(เพศ|สถานะ|สถานภาพ|อายุ|ระดับการศึกษา|ระดับชั้น|สาขาวิชา|แผนกวิชา|สังกัด|ตำแหน่ง|อาชีพ)[\s\:\.\-–]*/u', $line); if ($isNumberedQuestion || ($isPromptQuestion && !preg_match('/\[\s*\]/u', $line))) { // Save previous question if ($currentQuestion !== null) { if ($currentHeading !== null) { $currentHeading['questions'][] = $currentQuestion; } else { $questions[] = $currentQuestion; } $currentQuestion = null; } $qText = $isNumberedQuestion ? trim($qMatch[2]) : $line; // Remove numbering from question text for clean display if desired, or keep clear text if (empty($qText)) $qText = $line; // Determine default question type based on section or content $qType = 'radio'; $scaleMin = 1; $scaleMax = 5; $options = null; if ($secCategory === 'suggestion') { $qType = 'text'; } elseif ($secCategory === 'rating' || $currentHeading !== null || preg_match('/^[0-9๑-๙]+\.[0-9๑-๙]+/u', $line)) { $qType = 'radio'; $options = null; // Rating scale 1-5 } elseif ($secCategory === 'general') { $qType = 'radio'; $options = []; } // Check if options are inline on the same line (e.g. "[ ] ชาย [ ] หญิง") $inlineOptions = extractOptionsFromLine($line); if (!empty($inlineOptions)) { $options = $inlineOptions; // Clean question text of options $qText = preg_replace('/(\[\s*\]|\(\s*\)|[1-4]\)).*/u', '', $qText); $qText = trim($qText, " :.-–"); if (empty($qText)) $qText = $line; } $currentQuestion = [ 'question_text' => $qText, 'question_type' => $qType, 'is_required' => 1, 'description' => '', 'options' => $options, 'scale_min' => $scaleMin, 'scale_max' => $scaleMax ]; continue; } // Check if line contains options for the current question // e.g. "[ ] ชาย [ ] หญิง", "( ) 1. นักเรียน ( ) 2. ครู", "1) ปวช. 2) ปวส." if ($currentQuestion !== null) { $extractedOptions = extractOptionsFromLine($line); if (!empty($extractedOptions)) { if (!is_array($currentQuestion['options'])) { $currentQuestion['options'] = []; } foreach ($extractedOptions as $opt) { if (!in_array($opt, $currentQuestion['options'])) { $currentQuestion['options'][] = $opt; } } $currentQuestion['question_type'] = 'radio'; continue; } // If this is a continuation of question text if (empty($currentQuestion['options']) && $currentQuestion['question_type'] === 'radio' && $secCategory !== 'general') { // Could be multi-line question text if (mb_strlen($line, 'UTF-8') < 100 && !preg_match('/^[0-9]/', $line)) { $currentQuestion['question_text'] .= ' ' . $line; continue; } } } } // Add last question if ($currentQuestion !== null) { if ($currentHeading !== null) { $currentHeading['questions'][] = $currentQuestion; } else { $questions[] = $currentQuestion; } } // Add last heading if ($currentHeading !== null) { $headings[] = $currentHeading; } // If section has neither questions nor headings, check if raw lines can be converted to text questions if (empty($questions) && empty($headings) && !empty($rawSecLines)) { foreach ($rawSecLines as $raw) { if (mb_strlen($raw, 'UTF-8') > 3) { $questions[] = [ 'question_text' => $raw, 'question_type' => ($secCategory === 'suggestion') ? 'text' : 'radio', 'is_required' => 1, 'description' => '', 'options' => null, 'scale_min' => 1, 'scale_max' => 5 ]; } } } $parsedSections[] = [ 'section_number' => $secCounter++, 'title' => $sec['title'], 'description' => $sec['description'], 'category' => $secCategory, 'headings' => $headings, 'questions' => $questions ]; } // Default safety fallback if empty if (empty($parsedSections)) { $parsedSections[] = [ 'section_number' => 1, 'title' => 'ตอนที่ 1 ข้อมูลและข้อคำถาม', 'description' => '', 'category' => 'general', 'headings' => [], 'questions' => [] ]; } return [ 'title' => $title, 'description' => $description ?: 'แบบประเมินความพึงพอใจ นำเข้าจากไฟล์ PDF', 'sections' => $parsedSections ]; } /** * Extract multiple choice options from a line */ function extractOptionsFromLine($line) { $options = []; // Pattern 1: [ ] ชาย [ ] หญิง or ( ) 1. นักเรียน ( ) 2. ครู if (preg_match_all('/(?:\[\s*\]|\(\s*\)|□|○)\s*([^\(\)\[\]□○\t\n]+)/u', $line, $matches)) { foreach ($matches[1] as $opt) { $cleanOpt = trim(preg_replace('/^[0-9๑-๙]+[\.\)]\s*/u', '', trim($opt))); if (!empty($cleanOpt)) { $options[] = $cleanOpt; } } } // Pattern 2: 1) ชาย 2) หญิง 3) อื่นๆ if (empty($options) && preg_match_all('/(?:^|\s+)[0-9๑-๙]+[\)]\s*([^\d\)]+)/u', $line, $matches)) { foreach ($matches[1] as $opt) { $cleanOpt = trim($opt); if (!empty($cleanOpt) && mb_strlen($cleanOpt, 'UTF-8') < 60) { $options[] = $cleanOpt; } } } return $options; }