Fix Google Form Grid parser, survey display, and instant copy link button
This commit is contained in:
+94
-14
@@ -9,11 +9,13 @@ require_once __DIR__ . '/../config/database.php';
|
|||||||
require_once __DIR__ . '/../config/auth.php';
|
require_once __DIR__ . '/../config/auth.php';
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
requireLogin();
|
if (php_sapi_name() !== 'cli') {
|
||||||
|
requireLogin();
|
||||||
|
}
|
||||||
|
|
||||||
$action = $_GET['action'] ?? '';
|
if (isset($_GET['action'])) {
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
case 'importFromUrl':
|
case 'importFromUrl':
|
||||||
importFromUrl();
|
importFromUrl();
|
||||||
break;
|
break;
|
||||||
@@ -22,6 +24,7 @@ switch ($action) {
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
jsonResponse(['error' => 'Invalid action'], 400);
|
jsonResponse(['error' => 'Invalid action'], 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function importFromUrl() {
|
function importFromUrl() {
|
||||||
@@ -111,9 +114,10 @@ function parseGoogleFormData($data) {
|
|||||||
$sections = [];
|
$sections = [];
|
||||||
$currentSection = [
|
$currentSection = [
|
||||||
'section_number' => 1,
|
'section_number' => 1,
|
||||||
'title' => 'ส่วนที่ 1 ข้อมูลทั่วไป',
|
'title' => 'ตอนที่ 1 ข้อมูลทั่วไป',
|
||||||
'description' => '',
|
'description' => '',
|
||||||
'category' => 'general',
|
'category' => 'general',
|
||||||
|
'headings' => [],
|
||||||
'questions' => []
|
'questions' => []
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -127,24 +131,64 @@ function parseGoogleFormData($data) {
|
|||||||
$qTypeNum = $item[3] ?? 0;
|
$qTypeNum = $item[3] ?? 0;
|
||||||
$qDetails = $item[4][0] ?? [];
|
$qDetails = $item[4][0] ?? [];
|
||||||
|
|
||||||
if (empty($qTitle) && $qTypeNum != 8) continue; // Skip empty items except section breaks
|
if (empty($qTitle) && $qTypeNum != 8 && $qTypeNum != 7) continue;
|
||||||
|
|
||||||
// Type 8 = Section Break in Google Forms
|
// Type 8 = Section Break in Google Forms
|
||||||
if ($qTypeNum == 8) {
|
if ($qTypeNum == 8) {
|
||||||
if (!empty($currentSection['questions'])) {
|
if (!empty($currentSection['questions']) || !empty($currentSection['headings'])) {
|
||||||
$sections[] = $currentSection;
|
$sections[] = $currentSection;
|
||||||
}
|
}
|
||||||
$sectionCount++;
|
$sectionCount++;
|
||||||
$currentSection = [
|
$currentSection = [
|
||||||
'section_number' => $sectionCount,
|
'section_number' => $sectionCount,
|
||||||
'title' => $qTitle ?: ('ส่วนที่ ' . $sectionCount),
|
'title' => $qTitle ?: ('ตอนที่ ' . $sectionCount),
|
||||||
'description' => $qDesc,
|
'description' => $qDesc ?: '',
|
||||||
'category' => 'rating',
|
'category' => 'rating',
|
||||||
|
'headings' => [],
|
||||||
'questions' => []
|
'questions' => []
|
||||||
];
|
];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type 7 = Grid / Matrix (ตารางประเมินระดับความพึงพอใจ 5 ระดับ)
|
||||||
|
if ($qTypeNum == 7) {
|
||||||
|
$gridTitle = $qTitle ?: 'ด้านที่ประเมิน';
|
||||||
|
$gridDesc = $qDesc ?: '';
|
||||||
|
$gridQuestions = [];
|
||||||
|
|
||||||
|
if (isset($item[4]) && is_array($item[4])) {
|
||||||
|
foreach ($item[4] as $sub) {
|
||||||
|
if (!is_array($sub)) continue;
|
||||||
|
$rowTitle = '';
|
||||||
|
if (isset($sub[3]) && is_array($sub[3]) && !empty($sub[3])) {
|
||||||
|
$rowTitle = $sub[3][0];
|
||||||
|
} elseif (isset($sub[3]) && is_string($sub[3])) {
|
||||||
|
$rowTitle = $sub[3];
|
||||||
|
}
|
||||||
|
if (empty($rowTitle)) continue;
|
||||||
|
|
||||||
|
$gridQuestions[] = [
|
||||||
|
'question_text' => $rowTitle,
|
||||||
|
'question_type' => 'radio',
|
||||||
|
'is_required' => 1,
|
||||||
|
'description' => '',
|
||||||
|
'options' => null, // 5-point Rating scale
|
||||||
|
'scale_min' => 1,
|
||||||
|
'scale_max' => 5
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($gridQuestions)) {
|
||||||
|
$currentSection['headings'][] = [
|
||||||
|
'title' => $gridTitle,
|
||||||
|
'description' => $gridDesc,
|
||||||
|
'questions' => $gridQuestions
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$qType = 'radio';
|
$qType = 'radio';
|
||||||
$options = [];
|
$options = [];
|
||||||
$scaleMin = 1;
|
$scaleMin = 1;
|
||||||
@@ -154,7 +198,6 @@ function parseGoogleFormData($data) {
|
|||||||
// 0: Short Text, 1: Paragraph
|
// 0: Short Text, 1: Paragraph
|
||||||
// 2: Radio (Multiple Choice), 3: Dropdown, 4: Checkbox
|
// 2: Radio (Multiple Choice), 3: Dropdown, 4: Checkbox
|
||||||
// 5: Linear Scale (1-5)
|
// 5: Linear Scale (1-5)
|
||||||
// 7: Grid / Matrix
|
|
||||||
switch ($qTypeNum) {
|
switch ($qTypeNum) {
|
||||||
case 0:
|
case 0:
|
||||||
case 1:
|
case 1:
|
||||||
@@ -209,7 +252,7 @@ function parseGoogleFormData($data) {
|
|||||||
$currentSection['questions'][] = $questionObj;
|
$currentSection['questions'][] = $questionObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($currentSection['questions'])) {
|
if (!empty($currentSection['questions']) || !empty($currentSection['headings'])) {
|
||||||
$sections[] = $currentSection;
|
$sections[] = $currentSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +262,7 @@ function parseGoogleFormData($data) {
|
|||||||
'title' => 'ส่วนที่ 1 ข้อมูลและข้อคำถาม',
|
'title' => 'ส่วนที่ 1 ข้อมูลและข้อคำถาม',
|
||||||
'description' => 'นำเข้าจาก Google Forms',
|
'description' => 'นำเข้าจาก Google Forms',
|
||||||
'category' => 'general',
|
'category' => 'general',
|
||||||
|
'headings' => [],
|
||||||
'questions' => []
|
'questions' => []
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -257,18 +301,32 @@ function saveParsedSurveyToDB($parsedSurvey, $projectId = null) {
|
|||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$surveyId,
|
$surveyId,
|
||||||
$sec['section_number'] ?? $secNum,
|
$sec['section_number'] ?? $secNum,
|
||||||
$sec['title'] ?? ('ส่วนที่ ' . $secNum),
|
$sec['title'] ?? ('ตอนที่ ' . $secNum),
|
||||||
$sec['description'] ?? '',
|
$sec['description'] ?? '',
|
||||||
$sec['category'] ?? 'rating'
|
$sec['category'] ?? 'rating'
|
||||||
]);
|
]);
|
||||||
$sectionId = $db->lastInsertId();
|
$sectionId = $db->lastInsertId();
|
||||||
|
|
||||||
$orderNum = 1;
|
$orderNum = 1;
|
||||||
foreach ($sec['questions'] as $q) {
|
|
||||||
|
// 1. Insert Headings & their questions (e.g. Grids)
|
||||||
|
if (!empty($sec['headings'])) {
|
||||||
|
foreach ($sec['headings'] as $hIdx => $h) {
|
||||||
|
$stmtH = $db->prepare("INSERT INTO survey_headings (section_id, title, description, order_num) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmtH->execute([
|
||||||
|
$sectionId,
|
||||||
|
$h['title'] ?? '',
|
||||||
|
$h['description'] ?? '',
|
||||||
|
$hIdx + 1
|
||||||
|
]);
|
||||||
|
$headingId = $db->lastInsertId();
|
||||||
|
|
||||||
|
foreach ($h['questions'] as $q) {
|
||||||
$optionsJson = !empty($q['options']) ? json_encode($q['options'], JSON_UNESCAPED_UNICODE) : null;
|
$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 = $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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmtQ->execute([
|
$stmtQ->execute([
|
||||||
$sectionId,
|
$sectionId,
|
||||||
|
$headingId,
|
||||||
$q['question_text'],
|
$q['question_text'],
|
||||||
$q['question_type'] ?? 'radio',
|
$q['question_type'] ?? 'radio',
|
||||||
$q['is_required'] ?? 1,
|
$q['is_required'] ?? 1,
|
||||||
@@ -279,6 +337,28 @@ function saveParsedSurveyToDB($parsedSurvey, $projectId = null) {
|
|||||||
$q['description'] ?? null
|
$q['description'] ?? null
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Insert Loose questions
|
||||||
|
if (!empty($sec['questions'])) {
|
||||||
|
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, heading_id, question_text, question_type, is_required, order_num, options, scale_min, scale_max, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmtQ->execute([
|
||||||
|
$sectionId,
|
||||||
|
null,
|
||||||
|
$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++;
|
$secNum++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-16
@@ -809,9 +809,9 @@ async function generateLink(id) {
|
|||||||
<div class="qr-link-container">
|
<div class="qr-link-container">
|
||||||
<div class="qr-link-section">
|
<div class="qr-link-section">
|
||||||
<div class="qr-link-label">🔗 ลิงก์แบบสอบถาม</div>
|
<div class="qr-link-label">🔗 ลิงก์แบบสอบถาม</div>
|
||||||
<div class="link-display">
|
<div class="link-display" onclick="copyLink(this.querySelector('.copy-btn'))" style="cursor:pointer;" title="คลิกเพื่อคัดลอกลิงก์">
|
||||||
<span class="flex-1" id="surveyLinkText">${fullUrl}</span>
|
<span class="flex-1 select-all" id="surveyLinkText">${fullUrl}</span>
|
||||||
<button class="copy-btn" onclick="copyLink()">📋 คัดลอก</button>
|
<button class="copy-btn" onclick="event.stopPropagation(); copyLink(this)">📋 คัดลอก</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-code-section">
|
<div class="qr-code-section">
|
||||||
@@ -849,20 +849,11 @@ async function generateLink(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyLink() {
|
function copyLink(btnEl) {
|
||||||
const text = document.getElementById('surveyLinkText')?.textContent;
|
const text = document.getElementById('surveyLinkText')?.textContent?.trim();
|
||||||
if (text) {
|
if (text) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
const btn = btnEl || document.querySelector('.link-display .copy-btn');
|
||||||
Toast.success('คัดลอกลิงก์แล้ว');
|
copyTextToClipboard(text, btn);
|
||||||
}).catch(() => {
|
|
||||||
const ta = document.createElement('textarea');
|
|
||||||
ta.value = text;
|
|
||||||
document.body.appendChild(ta);
|
|
||||||
ta.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
ta.remove();
|
|
||||||
Toast.success('คัดลอกลิงก์แล้ว');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,63 @@ const API = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Universal Clipboard Helper (HTTP & HTTPS compatible)
|
||||||
|
// ============================================================
|
||||||
|
function copyTextToClipboard(text, btnElement) {
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
const showSuccess = () => {
|
||||||
|
if (typeof Toast !== 'undefined') Toast.success('คัดลอกลิงก์สำเร็จแล้ว!');
|
||||||
|
if (btnElement) {
|
||||||
|
const origHtml = btnElement.innerHTML;
|
||||||
|
btnElement.innerHTML = '✅ คัดลอกแล้ว!';
|
||||||
|
btnElement.style.backgroundColor = '#16a34a';
|
||||||
|
btnElement.style.color = '#ffffff';
|
||||||
|
setTimeout(() => {
|
||||||
|
btnElement.innerHTML = origHtml;
|
||||||
|
btnElement.style.backgroundColor = '';
|
||||||
|
btnElement.style.color = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(text).then(showSuccess).catch(() => fallbackCopy(text, showSuccess));
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text, showSuccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text, callback) {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.top = '0';
|
||||||
|
textArea.style.left = '0';
|
||||||
|
textArea.style.width = '2em';
|
||||||
|
textArea.style.height = '2em';
|
||||||
|
textArea.style.padding = '0';
|
||||||
|
textArea.style.border = 'none';
|
||||||
|
textArea.style.outline = 'none';
|
||||||
|
textArea.style.boxShadow = 'none';
|
||||||
|
textArea.style.background = 'transparent';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
if (successful && callback) {
|
||||||
|
callback();
|
||||||
|
} else if (!successful && typeof Toast !== 'undefined') {
|
||||||
|
Toast.error('กรุณากดคัดลอกด้วยตนเอง');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof Toast !== 'undefined') Toast.error('ไม่สามารถคัดลอกได้');
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Auth State
|
// Auth State
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
+3
-6
@@ -83,21 +83,19 @@ function renderSurvey() {
|
|||||||
|
|
||||||
|
|
||||||
(currentSurvey.sections || []).forEach(section => {
|
(currentSurvey.sections || []).forEach(section => {
|
||||||
const cleanSectionTitle = section.title.replace(/^ส่วนที่\s*[\d๑-๙]+\s*/, '').replace(/^\s*[\d๑-๙]+[\.\)\s\-:\u00A0]*\s*/, '');
|
|
||||||
html += `
|
html += `
|
||||||
<div class="survey-section">
|
<div class="survey-section">
|
||||||
<div class="survey-section-header">
|
<div class="survey-section-header">
|
||||||
<h2>${escapeHtml(cleanSectionTitle)}</h2>
|
<h2>${escapeHtml(section.title)}</h2>
|
||||||
${section.description ? `<p>${escapeHtml(section.description)}</p>` : ''}
|
${section.description ? `<p>${escapeHtml(section.description)}</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Render headings and their questions
|
// Render headings and their questions
|
||||||
(section.headings || []).forEach(heading => {
|
(section.headings || []).forEach(heading => {
|
||||||
const cleanHeadingTitle = heading.title.replace(/^\s*[\d๑-๙]+[\.\)\s\-:\u00A0]*\s*/, '');
|
|
||||||
html += `
|
html += `
|
||||||
<div class="survey-heading" style="margin:24px 0 16px; padding:16px 20px; background:linear-gradient(135deg, rgba(37,99,235,0.06), rgba(37,99,235,0.10)); border-left:4px solid var(--primary-color, #2563eb); border-radius:0 12px 12px 0;">
|
<div class="survey-heading" style="margin:24px 0 16px; padding:16px 20px; background:linear-gradient(135deg, rgba(37,99,235,0.06), rgba(37,99,235,0.10)); border-left:4px solid var(--primary-color, #2563eb); border-radius:0 12px 12px 0;">
|
||||||
<h3 style="margin:0; font-size:1.1rem; font-weight:700; color:#1e3a8a;">📑 ${escapeHtml(cleanHeadingTitle)}</h3>
|
<h3 style="margin:0; font-size:1.1rem; font-weight:700; color:#1e3a8a;">📑 ${escapeHtml(heading.title)}</h3>
|
||||||
${heading.description ? `<p style="margin:4px 0 0; font-size:0.9rem; color:#475569;">${escapeHtml(heading.description)}</p>` : ''}
|
${heading.description ? `<p style="margin:4px 0 0; font-size:0.9rem; color:#475569;">${escapeHtml(heading.description)}</p>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -127,11 +125,10 @@ function renderSurvey() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderQuestion(q) {
|
function renderQuestion(q) {
|
||||||
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s\-:\u00A0]*\s*/, '');
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="survey-question">
|
<div class="survey-question">
|
||||||
<div class="q-label">
|
<div class="q-label">
|
||||||
<span>${escapeHtml(cleanText)}</span>
|
<span>${escapeHtml(q.question_text)}</span>
|
||||||
${q.is_required ? '<span class="required">*</span>' : ''}
|
${q.is_required ? '<span class="required">*</span>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
+7
-9
@@ -385,9 +385,9 @@ async function teacherGenerateLink(id) {
|
|||||||
<div class="qr-link-container">
|
<div class="qr-link-container">
|
||||||
<div class="qr-link-section">
|
<div class="qr-link-section">
|
||||||
<div class="qr-link-label">🔗 ลิงก์แบบสอบถาม</div>
|
<div class="qr-link-label">🔗 ลิงก์แบบสอบถาม</div>
|
||||||
<div class="link-display">
|
<div class="link-display" onclick="copyTeacherLink(this.querySelector('.copy-btn'))" style="cursor:pointer;" title="คลิกเพื่อคัดลอกลิงก์">
|
||||||
<span class="flex-1" id="surveyLinkText">${fullUrl}</span>
|
<span class="flex-1 select-all" id="surveyLinkText">${fullUrl}</span>
|
||||||
<button class="copy-btn" onclick="copyTeacherLink()">📋 คัดลอก</button>
|
<button class="copy-btn" onclick="event.stopPropagation(); copyTeacherLink(this)">📋 คัดลอก</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-code-section">
|
<div class="qr-code-section">
|
||||||
@@ -425,13 +425,11 @@ async function teacherGenerateLink(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyTeacherLink() {
|
function copyTeacherLink(btnEl) {
|
||||||
const text = document.getElementById('surveyLinkText')?.textContent;
|
const text = document.getElementById('surveyLinkText')?.textContent?.trim();
|
||||||
if (text) {
|
if (text) {
|
||||||
navigator.clipboard.writeText(text).then(() => Toast.success('คัดลอกแล้ว')).catch(() => {
|
const btn = btnEl || document.querySelector('.link-display .copy-btn');
|
||||||
const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
|
copyTextToClipboard(text, btn);
|
||||||
Toast.success('คัดลอกแล้ว');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user