Files
survey/api/import_pdf.php
T

559 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Import Survey from PDF API
* POST ?action=parsePdf - Upload and parse PDF file to survey JSON
* POST ?action=confirmImport - Save parsed survey JSON into database
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../config/auth.php';
require_once __DIR__ . '/../vendor/autoload.php';
use Smalot\PdfParser\Parser;
header('Content-Type: application/json; charset=utf-8');
if (php_sapi_name() !== 'cli') {
requireLogin();
}
if (isset($_GET['action'])) {
$action = $_GET['action'] ?? '';
switch ($action) {
case 'parsePdf':
parsePdf();
break;
case 'confirmImport':
confirmImport();
break;
default:
jsonResponse(['error' => '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;
}