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
+1266
View File
File diff suppressed because it is too large Load Diff
+284
View File
@@ -0,0 +1,284 @@
/**
* TPU Survey - Core Application Logic
* API communication, Auth state, Toast notifications, Theme
*/
const API = {
base: '/survey/api',
async request(endpoint, options = {}) {
const url = `${this.base}/${endpoint}`;
const config = {
headers: { 'Content-Type': 'application/json' },
...options
};
if (options.body && typeof options.body === 'object') {
config.body = JSON.stringify(options.body);
}
try {
const res = await fetch(url, config);
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'เกิดข้อผิดพลาด');
return data;
} catch (err) {
if (err.message === 'Failed to fetch') throw new Error('ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้');
throw err;
}
},
get(endpoint) { return this.request(endpoint); },
post(endpoint, body) { return this.request(endpoint, { method: 'POST', body }); },
upload(endpoint, formData) {
const url = `${this.base}/${endpoint}`;
return fetch(url, { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if (data.error) throw new Error(data.error);
return data;
});
}
};
// ============================================================
// Auth State
// ============================================================
const Auth = {
user: null,
async check() {
try {
const data = await API.get('auth.php?action=check');
if (data.loggedIn) {
this.user = data.user;
return true;
}
} catch (e) {}
this.user = null;
return false;
},
async login(username, password) {
const data = await API.post('auth.php?action=login', { username, password });
this.user = data.user;
return data;
},
async logout() {
await API.post('auth.php?action=logout', {});
this.user = null;
window.location.href = '/survey/index.html';
},
isAdmin() { return this.user?.role === 'admin'; },
isTeacher() { return this.user?.role === 'teacher'; }
};
// ============================================================
// Sound Effects (Web Audio API)
// ============================================================
const SoundFX = {
ctx: null,
getCtx() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
return this.ctx;
},
// เสียง OK — 2 โน้ตสั้น ไพเราะ
playSuccess() {
try {
const ctx = this.getCtx();
const now = ctx.currentTime;
// Note 1: C5 (523 Hz)
const osc1 = ctx.createOscillator();
const gain1 = ctx.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(523, now);
gain1.gain.setValueAtTime(0.3, now);
gain1.gain.exponentialRampToValueAtTime(0.01, now + 0.15);
osc1.connect(gain1);
gain1.connect(ctx.destination);
osc1.start(now);
osc1.stop(now + 0.15);
// Note 2: E5 (659 Hz) — ช้ากว่าเล็กน้อย
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(659, now + 0.1);
gain2.gain.setValueAtTime(0, now);
gain2.gain.setValueAtTime(0.3, now + 0.1);
gain2.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.start(now + 0.1);
osc2.stop(now + 0.3);
// Note 3: G5 (784 Hz) — จบด้วยโน้ตสูง
const osc3 = ctx.createOscillator();
const gain3 = ctx.createGain();
osc3.type = 'sine';
osc3.frequency.setValueAtTime(784, now + 0.2);
gain3.gain.setValueAtTime(0, now);
gain3.gain.setValueAtTime(0.25, now + 0.2);
gain3.gain.exponentialRampToValueAtTime(0.01, now + 0.45);
osc3.connect(gain3);
gain3.connect(ctx.destination);
osc3.start(now + 0.2);
osc3.stop(now + 0.45);
} catch (e) {
// AudioContext not supported
}
},
// เสียง Error — โทนต่ำ
playError() {
try {
const ctx = this.getCtx();
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(150, now + 0.3);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.3);
} catch (e) {}
}
};
// ============================================================
// Toast Notifications
// ============================================================
const Toast = {
container: null,
init() {
if (!this.container) {
this.container = document.createElement('div');
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
},
show(message, type = 'info', duration = 4000) {
this.init();
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
toast.innerHTML = `<span>${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
this.container.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, duration);
},
success(msg) { this.show(msg, 'success'); SoundFX.playSuccess(); },
error(msg) { this.show(msg, 'error'); SoundFX.playError(); },
warning(msg) { this.show(msg, 'warning'); },
info(msg) { this.show(msg, 'info'); }
};
// ============================================================
// Theme Management (Light Mode Only — ม.ตาปี)
// ============================================================
const Theme = {
init() { /* Light mode only */ },
toggle() { /* Disabled */ }
};
// ============================================================
// Modal Helper
// ============================================================
const Modal = {
show(id) {
const el = document.getElementById(id);
if (el) el.classList.add('active');
},
hide(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('active');
},
create(title, bodyHTML, footerHTML = '', size = '') {
const id = 'modal-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.className = 'modal-overlay';
div.innerHTML = `
<div class="modal ${size}">
<div class="modal-header">
<h3>${title}</h3>
<button class="modal-close" onclick="Modal.hide('${id}');this.closest('.modal-overlay').remove();">✕</button>
</div>
<div class="modal-body">${bodyHTML}</div>
${footerHTML ? `<div class="modal-footer">${footerHTML}</div>` : ''}
</div>
`;
document.body.appendChild(div);
div.classList.add('active');
div.addEventListener('click', (e) => {
if (e.target === div) { div.classList.remove('active'); div.remove(); }
});
return id;
}
};
// ============================================================
// Utility Functions
// ============================================================
function formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleDateString('th-TH', { year: 'numeric', month: 'short', day: 'numeric' });
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// ============================================================
// Sidebar Toggle (Mobile)
// ============================================================
function toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay');
if (sidebar) sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('active');
}
// Close sidebar when clicking overlay
document.addEventListener('click', (e) => {
if (e.target.classList.contains('sidebar-overlay')) {
toggleSidebar();
}
});
// Init
Theme.init();
+534
View File
@@ -0,0 +1,534 @@
/**
* TPU Survey - Standalone Editor Logic
* รองรับ Section -> Heading -> Question
*/
let currentSurvey = null;
let isDirty = false;
async function initEditor() {
const urlParams = new URLSearchParams(window.location.search);
const id = urlParams.get('id');
if (!id) {
alert('ไม่ระบุ ID แบบสอบถาม');
window.close();
return;
}
try {
const data = await API.get(`surveys.php?action=get&id=${id}`);
currentSurvey = data.survey;
renderEditor();
} catch (err) {
Toast.error(err.message);
document.getElementById('editorContent').innerHTML = `
<div class="empty-state">
<div class="empty-icon">❌</div>
<h3>เกิดข้อผิดพลาด</h3>
<p>${err.message}</p>
<button class="btn btn-primary" onclick="window.close()">ปิดหน้านี้</button>
</div>
`;
}
}
function renderEditor() {
if (!currentSurvey) return;
document.title = `แก้ไข: ${currentSurvey.title} - TPU Survey`;
document.getElementById('surveyTitleDisplay').textContent = currentSurvey.title;
let html = `
<div class="card mb-32" style="border:none; box-shadow:var(--shadow-md); background:var(--bg-card); border-radius:var(--radius-xl);">
<div class="card-body" style="padding:40px;">
<div class="form-group mb-24">
<label class="form-label label-lg" style="font-size:1.2rem; font-weight:800;">ชื่อแบบสอบถาม</label>
<input type="text" id="edit-survey-title" class="form-input" style="font-size:1.6rem; font-weight:800; height:70px;"
value="${escapeHtml(currentSurvey.title)}" oninput="markDirty()">
</div>
<div class="form-group mb-24">
<label class="form-label">รายละเอียด / คำชี้แจง</label>
<textarea id="edit-survey-desc" class="form-input" rows="3" oninput="markDirty()">${escapeHtml(currentSurvey.description || '')}</textarea>
</div>
<div class="form-group mt-16" style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" id="edit-survey-cert" style="width:22px; height:22px; cursor:pointer;"
${currentSurvey.has_certificate == 1 ? 'checked' : ''} onchange="toggleCertPanel(); markDirty();">
<label for="edit-survey-cert" style="font-weight:700; cursor:pointer; user-select:none; font-size:0.95rem; color:var(--primary-dark);">🎓 เปิดใช้งานและจัดส่งเกียรติบัตรทางอีเมลอัตโนมัติสำหรับแบบสอบถามนี้</label>
</div>
<!-- พาเนลตั้งค่าเกียรติบัตร -->
<div id="cert-config-panel" style="margin-top:20px; padding:24px; border:1px dashed var(--primary-color, #2563eb); border-radius:12px; background:rgba(37,99,235,0.02); display:${currentSurvey.has_certificate == 1 ? 'block' : 'none'};">
<h4 style="font-weight:700; color:var(--primary-dark); margin-bottom:16px; font-size:1.05rem;">⚙️ ตั้งค่าเกียรติบัตรเฉพาะแบบประเมิน</h4>
<div class="form-group mb-20">
<label class="form-label" style="font-weight:700;">รูปภาพเทมเพลตเกียรติบัตร (กระดาษ A4 แนวนอน แนะนำไฟล์ PNG ขนาดประมาณ 2970x2100px)</label>
<div style="display:flex; gap:16px; align-items:center; margin-top:8px;">
<input type="file" id="edit-survey-cert-file" accept="image/png, image/jpeg" style="display:none;" onchange="uploadCertTemplate(this)">
<button type="button" class="btn btn-secondary btn-sm" onclick="document.getElementById('edit-survey-cert-file').click()">
📤 อัปโหลดรูปภาพเทมเพลต
</button>
<span id="cert-upload-status" style="font-size:0.85rem; color:#6b7280;">
${currentSurvey.certificate_template ? 'ใช้รูปภาพเทมเพลตที่อัปโหลดไว้แล้ว' : 'ใช้เทมเพลตเริ่มต้นของระบบ'}
</span>
${currentSurvey.certificate_template ? `<button type="button" id="cert-delete-btn" class="btn btn-ghost btn-sm text-red-500" onclick="resetCertTemplate()" style="color:#ef4444; border:1px solid #ef4444; padding:2px 8px; margin-left:10px;">ลบรูปภาพ</button>` : ''}
</div>
<input type="hidden" id="edit-survey-cert-template-path" value="${escapeHtml(currentSurvey.certificate_template || '')}">
<div id="cert-preview-wrapper" style="margin-top:12px; display:${currentSurvey.certificate_template ? 'block' : 'none'};">
<img id="cert-template-preview" src="${currentSurvey.certificate_template ? '/survey/' + currentSurvey.certificate_template : ''}" style="max-height:140px; width:auto; border-radius:8px; border:1px solid var(--border-color); box-shadow:var(--shadow-sm);">
</div>
</div>
<div style="display:flex; gap:24px; flex-wrap:wrap; margin-top:16px;">
<div class="form-group" style="flex:1; min-width:200px;">
<label class="form-label" style="font-weight:700;">ตำแหน่งชื่อแนวตั้ง (Name Y-Position)</label>
<div style="display:flex; align-items:center; gap:8px; margin-top:8px;">
<input type="number" step="0.1" id="edit-survey-cert-name-y" class="form-input" style="width:110px;" value="${currentSurvey.cert_name_y || 82.5}" oninput="markDirty()">
<span style="font-size:0.9rem; color:#4b5563;">มิลลิเมตร (mm)</span>
</div>
<small style="color:#6b7280; display:block; margin-top:4px;">(ค่าปกติคือ 82.5 มม.) วัดจากขอบบนสุดของกระดาษ</small>
</div>
<div class="form-group" style="flex:1; min-width:200px;">
<label class="form-label" style="font-weight:700;">ตำแหน่งรหัสตรวจสอบแนวตั้ง (Code Y-Position)</label>
<div style="display:flex; align-items:center; gap:8px; margin-top:8px;">
<input type="number" step="0.1" id="edit-survey-cert-code-y" class="form-input" style="width:110px;" value="${currentSurvey.cert_code_y || 190.5}" oninput="markDirty()">
<span style="font-size:0.9rem; color:#4b5563;">มิลลิเมตร (mm)</span>
</div>
<small style="color:#6b7280; display:block; margin-top:4px;">(ค่าปกติคือ 190.5 มม.) วัดจากขอบบนสุดของกระดาษ</small>
</div>
</div>
</div>
</div>
</div>
<div id="sectionsContainer"></div>
`;
document.getElementById('editorContent').innerHTML = html;
const container = document.getElementById('sectionsContainer');
(currentSurvey.sections || []).forEach((section, si) => {
container.insertAdjacentHTML('beforeend', buildSectionUI(si, section));
});
}
/* ============================================================
SECTION
============================================================ */
function buildSectionUI(si, section) {
// Build headings HTML
let headingsHTML = '';
(section.headings || []).forEach((heading, hi) => {
headingsHTML += buildHeadingUI(si, hi, heading);
});
// Build loose questions (not in any heading) HTML
let looseQuestionsHTML = '';
(section.questions || []).forEach((q, qi) => {
looseQuestionsHTML += buildQuestionUI(si, null, qi, q);
});
return `
<div class="card mb-40" data-section="${si}" style="border:none; box-shadow:var(--shadow-md); overflow:visible;">
<div class="card-header" style="background:var(--bg-secondary); border-radius:var(--radius-xl) var(--radius-xl) 0 0; padding:24px 32px;">
<div class="d-flex items-center gap-16 flex-1">
<div style="flex:1;">
<input type="text" class="form-input section-title-input" style="font-weight:800;font-size:24px;border:none;padding:4px 0;background:transparent; color:var(--primary-dark);"
value="${escapeHtml(section.title)}" oninput="markDirty()">
</div>
<select class="form-input section-category-select" style="width:auto;font-size:14px;padding:6px 12px;border:1px solid var(--border-color); border-radius:var(--radius-md); background:var(--bg-card);"
onchange="markDirty()">
<option value="general" ${section.category==='general'?'selected':''}>📊 ข้อมูลทั่วไป (General)</option>
<option value="rating" ${section.category==='rating'?'selected':''}>⭐ การประเมิน (Rating)</option>
<option value="suggestions" ${section.category==='suggestions'?'selected':''}>📝 ข้อเสนอแนะ (Suggestions)</option>
</select>
</div>
<button type="button" class="btn btn-ghost btn-sm text-danger" onclick="removeSection(this)" style="margin-left:16px;">🗑️ ลบส่วนนี้</button>
</div>
<div class="card-body" style="padding:32px;">
<!-- Headings Container -->
<div class="headings-container" data-section-headings="${si}">${headingsHTML}</div>
<!-- Loose Questions Container (questions not in any heading) -->
<div class="loose-questions-container" data-section-loose="${si}">
${looseQuestionsHTML ? `<div class="loose-questions-label" style="font-size:13px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin:16px 0 12px;padding-bottom:8px;border-bottom:1px dashed var(--border-color);">📋 คำถามทั่วไป (ไม่อยู่ในหัวข้อย่อย)</div>` : ''}
${looseQuestionsHTML}
</div>
<div class="section-actions-bar" style="display:flex;gap:12px;margin-top:24px;">
<button type="button" class="btn btn-ghost" style="border:2px dashed var(--primary-color); flex:1; border-radius:var(--radius-lg); color:var(--primary-color); font-weight:700;"
onclick="addHeading(this)">📑 เพิ่มหัวข้อย่อย</button>
<button type="button" class="btn btn-ghost" style="border:2px dashed var(--border-color); flex:1; border-radius:var(--radius-lg);"
onclick="addLooseQuestion(this)">+ เพิ่มคำถามทั่วไป</button>
</div>
</div>
</div>
`;
}
function addSection() {
const si = document.querySelectorAll('[data-section]').length;
const html = buildSectionUI(si, { title: `ส่วนที่ ${si+1}`, category: 'rating', headings: [], questions: [] });
document.getElementById('sectionsContainer').insertAdjacentHTML('beforeend', html);
markDirty();
}
function removeSection(btn) {
if (!confirm('ยืนยันลบส่วนนี้พร้อมหัวข้อย่อยและคำถามทั้งหมด?')) return;
btn.closest('.card').remove();
markDirty();
}
/* ============================================================
HEADING (หัวข้อย่อย)
============================================================ */
function buildHeadingUI(si, hi, heading = {}) {
let questionsHTML = '';
(heading.questions || []).forEach((q, qi) => {
questionsHTML += buildQuestionUI(si, hi, qi, q);
});
return `
<div class="heading-block" data-heading="${hi}" style="margin-bottom:28px; border:2px solid var(--primary-color); border-radius:var(--radius-lg); background:linear-gradient(135deg, rgba(37,99,235,0.03), rgba(37,99,235,0.06)); overflow:visible;">
<div class="heading-header" style="padding:16px 20px; background:linear-gradient(135deg, rgba(37,99,235,0.08), rgba(37,99,235,0.12)); border-bottom:1px solid rgba(37,99,235,0.15); display:flex; align-items:center; gap:12px;">
<span style="font-size:20px;">📑</span>
<div style="flex:1;">
<input type="text" class="form-input heading-title-input" style="font-weight:700;font-size:18px;border:none;padding:4px 0;background:transparent; color:var(--primary-dark);"
value="${escapeHtml(heading.title || '')}" placeholder="ชื่อหัวข้อย่อย..." oninput="markDirty()">
<input type="text" class="form-input heading-desc-input" style="font-size:13px;border:none;padding:2px 0;background:transparent; color:var(--text-muted);"
value="${escapeHtml(heading.description || '')}" placeholder="คำอธิบายหัวข้อ (ไม่บังคับ)" oninput="markDirty()">
</div>
<button type="button" class="btn btn-ghost btn-sm text-danger" onclick="removeHeading(this)" title="ลบหัวข้อย่อย">🗑️</button>
</div>
<div class="heading-questions" style="padding:16px 20px;">
<div class="heading-questions-list">${questionsHTML}</div>
<button type="button" class="btn btn-ghost btn-sm" style="border:1px dashed var(--border-color); width:100%; border-radius:var(--radius-md); margin-top:12px;"
onclick="addQuestionToHeading(this)">+ เพิ่มคำถามในหัวข้อนี้</button>
</div>
</div>
`;
}
function addHeading(btn) {
const sectionEl = btn.closest('[data-section]');
const headingsContainer = sectionEl.querySelector('.headings-container');
const hi = headingsContainer.querySelectorAll('.heading-block').length;
const si = parseInt(sectionEl.dataset.section);
const html = buildHeadingUI(si, hi, { title: '', description: '', questions: [] });
headingsContainer.insertAdjacentHTML('beforeend', html);
// Focus on the title input
const newHeading = headingsContainer.lastElementChild;
const titleInput = newHeading.querySelector('.heading-title-input');
if (titleInput) titleInput.focus();
markDirty();
}
function removeHeading(btn) {
if (!confirm('ยืนยันลบหัวข้อย่อยนี้พร้อมคำถามทั้งหมด?')) return;
btn.closest('.heading-block').remove();
markDirty();
}
/* ============================================================
QUESTION
============================================================ */
function buildQuestionUI(si, hi, qi, q = {}) {
const qType = q.question_type || 'radio';
const qText = q.question_text || '';
const qDescription = q.description || '';
const qOptions = Array.isArray(q.options) ? q.options.join('\n') : '';
const showOptions = ['radio', 'checkbox', 'dropdown'].includes(qType);
// Context label: which heading or "ทั่วไป"
const contextLabel = hi !== null ? `หัวข้อ ${hi+1}` : '';
return `
<div class="question-builder mb-24" style="border-left:4px solid var(--primary-color);">
<div class="qb-header">
<span class="qb-handle">⋮⋮</span>
<span class="text-md fw-800 q-number-label" style="color:var(--primary-dark); font-size:1.1rem;">คำถาม</span>
${contextLabel ? `<span style="font-size:11px;color:var(--text-muted);margin-left:8px;">(${contextLabel})</span>` : ''}
<div class="qb-actions">
<button type="button" class="btn btn-ghost btn-sm text-danger" onclick="removeQuestion(this)" title="ลบคำถาม">🗑️ ลบ</button>
</div>
</div>
<div class="form-group mb-16">
<label class="form-label">คำถาม / หัวข้อ</label>
<input type="text" class="form-input q-text-input" value="${escapeHtml(qText)}" placeholder="พิมพ์คำถามที่นี่..." oninput="markDirty()">
</div>
<div class="form-group mb-16">
<label class="form-label">รายละเอียดเพิ่มเติม (คำชี้แจง/คำใบ้ - ไม่บังคับ)</label>
<input type="text" class="form-input q-desc-input" value="${escapeHtml(qDescription)}" placeholder="เช่น กรุณาระบุรหัสนักศึกษา..." oninput="markDirty()">
</div>
<div class="grid grid-2 gap-24 items-end">
<div class="form-group">
<label class="form-label">ประเภทการตอบ</label>
<select class="form-input q-type-select" onchange="toggleOptions(this);markDirty();">
<option value="radio" ${qType==='radio'?'selected':''}>📊 ระดับ 1-5 (Radio)</option>
<option value="text" ${qType==='text'?'selected':''}>📝 พิมพ์ตอบ (Short Text)</option>
<option value="dropdown" ${qType==='dropdown'?'selected':''}>📋 รายการเลือก (Dropdown)</option>
<option value="checkbox" ${qType==='checkbox'?'selected':''}>☑️ เลือกได้หลายข้อ (Checkbox)</option>
</select>
</div>
<div class="form-group q-options-group" style="${showOptions ? '' : 'display:none;'}">
<label class="form-label">ตัวเลือก (1 บรรทัดต่อ 1 ข้อ)</label>
<textarea class="form-input q-options-input" placeholder="ตัวเลือก 1\nตัวเลือก 2" rows="2" oninput="markDirty()">${escapeHtml(qOptions)}</textarea>
</div>
</div>
</div>
`;
}
function addQuestionToHeading(btn) {
const headingEl = btn.closest('.heading-block');
const listEl = headingEl.querySelector('.heading-questions-list');
const qi = listEl.querySelectorAll('.question-builder').length;
const si = parseInt(headingEl.closest('[data-section]').dataset.section);
const hi = parseInt(headingEl.dataset.heading);
const html = buildQuestionUI(si, hi, qi);
listEl.insertAdjacentHTML('beforeend', html);
reindexQuestions(listEl);
markDirty();
}
function addLooseQuestion(btn) {
const sectionEl = btn.closest('[data-section]');
const looseContainer = sectionEl.querySelector('.loose-questions-container');
const si = parseInt(sectionEl.dataset.section);
const qi = looseContainer.querySelectorAll('.question-builder').length;
// Add label if first loose question
if (qi === 0) {
looseContainer.insertAdjacentHTML('afterbegin',
`<div class="loose-questions-label" style="font-size:13px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin:16px 0 12px;padding-bottom:8px;border-bottom:1px dashed var(--border-color);">📋 คำถามทั่วไป (ไม่อยู่ในหัวข้อย่อย)</div>`
);
}
const html = buildQuestionUI(si, null, qi);
looseContainer.insertAdjacentHTML('beforeend', html);
reindexQuestions(looseContainer);
markDirty();
}
function removeQuestion(btn) {
if (!confirm('ยืนยันลบคำถามนี้?')) return;
const container = btn.closest('.heading-questions-list') || btn.closest('.loose-questions-container');
btn.closest('.question-builder').remove();
if (container) reindexQuestions(container);
markDirty();
}
function reindexQuestions(container) {
if (!container) return;
container.querySelectorAll('.question-builder').forEach((qEl, idx) => {
const label = qEl.querySelector('.q-number-label');
if (label) label.textContent = `คำถาม`;
});
}
function toggleOptions(select) {
const group = select.closest('.question-builder').querySelector('.q-options-group');
if (['radio', 'checkbox', 'dropdown'].includes(select.value)) {
group.style.display = 'block';
} else {
group.style.display = 'none';
}
}
/* ============================================================
SAVE — อ่าน DOM แปลงเป็น JSON แล้วส่ง API
============================================================ */
async function saveSurvey() {
if (!currentSurvey) return;
const title = document.getElementById('edit-survey-title').value;
const description = document.getElementById('edit-survey-desc').value;
const hasCertificate = document.getElementById('edit-survey-cert').checked ? 1 : 0;
const sections = [];
document.querySelectorAll('[data-section]').forEach((sectionEl, si) => {
const sTitle = sectionEl.querySelector('.section-title-input')?.value || `ส่วนที่ ${si+1}`;
const sCategory = sectionEl.querySelector('.section-category-select')?.value || 'rating';
// Collect headings and their questions
const headings = [];
sectionEl.querySelectorAll('.heading-block').forEach((headingEl, hi) => {
const hTitle = headingEl.querySelector('.heading-title-input')?.value || '';
const hDesc = headingEl.querySelector('.heading-desc-input')?.value || '';
const hQuestions = collectQuestions(headingEl.querySelector('.heading-questions-list'));
if (hTitle.trim()) {
headings.push({
title: hTitle.trim(),
description: hDesc.trim() || null,
order_num: hi,
questions: hQuestions
});
}
});
// Collect loose questions (not in any heading)
const looseQuestions = collectQuestions(sectionEl.querySelector('.loose-questions-container'));
sections.push({
section_number: si + 1,
title: sTitle,
category: sCategory,
headings: headings,
questions: looseQuestions
});
});
try {
const btn = document.querySelector('.btn-primary');
btn.disabled = true;
btn.textContent = '⌛ กำลังบันทึก...';
const certificateTemplate = document.getElementById('edit-survey-cert-template-path').value || null;
const certNameY = parseFloat(document.getElementById('edit-survey-cert-name-y').value || 82.5);
const certCodeY = parseFloat(document.getElementById('edit-survey-cert-code-y').value || 190.5);
await API.post('surveys.php?action=update', {
id: currentSurvey.id,
title,
description,
has_certificate: hasCertificate,
certificate_template: certificateTemplate,
cert_name_y: certNameY,
cert_code_y: certCodeY,
sections
});
Toast.success('บันทึกสำเร็จ');
isDirty = false;
} catch (err) {
Toast.error(err.message);
} finally {
const btn = document.querySelector('.btn-primary');
btn.disabled = false;
btn.textContent = '💾 บันทึกการเปลี่ยนแปลง';
}
}
function collectQuestions(container) {
if (!container) return [];
const questions = [];
container.querySelectorAll(':scope > .question-builder').forEach((qEl, qi) => {
const qText = qEl.querySelector('.q-text-input')?.value || '';
const qDescription = qEl.querySelector('.q-desc-input')?.value || '';
const qType = qEl.querySelector('.q-type-select')?.value || 'radio';
const qOptionsRaw = qEl.querySelector('.q-options-input')?.value || '';
const options = qOptionsRaw.split('\n').map(o => o.trim()).filter(o => o !== '');
if (qText.trim()) {
questions.push({
question_text: qText.trim(),
description: qDescription.trim() || null,
question_type: qType,
order_num: qi,
is_required: 1,
options: options.length > 0 ? options : null
});
}
});
return questions;
}
/* ============================================================
UTILITY FUNCTIONS
============================================================ */
function markDirty() {
isDirty = true;
}
function closeEditor() {
if (isDirty) {
if (!confirm('มีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก ยืนยันปิดหรือไม่?')) return;
}
window.close();
}
function previewSurvey() {
if (!currentSurvey) return;
window.open(`respond.php?link=${currentSurvey.unique_link}`, '_blank');
}
// Warn before leave if dirty
window.addEventListener('beforeunload', (e) => {
if (isDirty) {
e.preventDefault();
e.returnValue = '';
}
});
function toggleCertPanel() {
const isChecked = document.getElementById('edit-survey-cert').checked;
const panel = document.getElementById('cert-config-panel');
if (panel) {
panel.style.display = isChecked ? 'block' : 'none';
}
}
async function uploadCertTemplate(input) {
if (!input.files || input.files.length === 0) return;
const file = input.files[0];
const statusEl = document.getElementById('cert-upload-status');
statusEl.textContent = '⌛ กำลังอัปโหลดไฟล์...';
statusEl.style.color = '#2563eb';
const formData = new FormData();
formData.append('template_image', file);
try {
const res = await fetch('/survey/api/surveys.php?action=uploadTemplate', {
method: 'POST',
body: formData
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'การอัปโหลดล้มเหลว');
document.getElementById('edit-survey-cert-template-path').value = data.template_path;
statusEl.textContent = '✅ อัปโหลดเทมเพลตสำเร็จ';
statusEl.style.color = '#16a34a';
// Update preview
const previewImg = document.getElementById('cert-template-preview');
const previewWrapper = document.getElementById('cert-preview-wrapper');
previewImg.src = '/survey/' + data.template_path;
previewWrapper.style.display = 'block';
// Add dynamic delete button if it doesn't exist
if (!document.getElementById('cert-delete-btn')) {
const btnHTML = `<button type="button" id="cert-delete-btn" class="btn btn-ghost btn-sm text-red-500" onclick="resetCertTemplate()" style="color:#ef4444; border:1px solid #ef4444; padding:2px 8px; margin-left:10px;">ลบรูปภาพ</button>`;
input.parentElement.insertAdjacentHTML('beforeend', btnHTML);
}
markDirty();
} catch (err) {
statusEl.textContent = '❌ ' + err.message;
statusEl.style.color = '#dc2626';
}
}
function resetCertTemplate() {
document.getElementById('edit-survey-cert-template-path').value = '';
document.getElementById('cert-upload-status').textContent = 'ใช้เทมเพลตเริ่มต้นของระบบ';
document.getElementById('cert-upload-status').style.color = '#6b7280';
document.getElementById('cert-preview-wrapper').style.display = 'none';
document.getElementById('cert-template-preview').src = '';
document.getElementById('edit-survey-cert-file').value = '';
const deleteBtn = document.getElementById('cert-delete-btn');
if (deleteBtn) deleteBtn.remove();
markDirty();
}
document.addEventListener('DOMContentLoaded', initEditor);
+1
View File
File diff suppressed because one or more lines are too long
+299
View File
@@ -0,0 +1,299 @@
/**
* TPU Survey - Standalone Statistics Logic
*/
function renderStatsQuestions(questions) {
let html = '';
const ratingQuestions = questions.filter(q => q.question_type === 'radio' && q.mean !== undefined);
const categoricalQuestions = questions.filter(q =>
(q.question_type === 'dropdown' || q.question_type === 'checkbox' || (q.question_type === 'radio' && q.mean === undefined))
&& q.frequencies && q.frequencies.length > 0
);
if (categoricalQuestions.length > 0) {
categoricalQuestions.forEach(q => {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `
<div class="categorical-stat mb-24" style="background:var(--gray-50); padding:20px; border-radius:var(--radius-md); border:1px solid var(--border-color);">
<p class="fw-800 mb-12" style="color:var(--primary-dark); font-size:18px;">${escapeHtml(cleanText)}</p>
<div class="table-container">
<table style="width:100%; border-collapse:collapse; background:white;">
<thead>
<tr>
<th style="text-align:left; background:var(--bg-secondary);">ตัวเลือก</th>
<th style="width:120px; text-align:center; background:var(--bg-secondary);">จำนวน</th>
<th style="width:120px; text-align:center; background:var(--bg-secondary);">ร้อยละ</th>
</tr>
</thead>
<tbody>
`;
q.frequencies.forEach(f => {
html += `
<tr>
<td style="text-align:left; font-weight:600;">${escapeHtml(String(f.value))}</td>
<td style="text-align:center;">${f.count}</td>
<td style="text-align:center;">${f.percentage}%</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
</div>
`;
});
}
if (ratingQuestions.length > 0) {
html += `<div class="table-container"><table>`;
html += `<thead><tr><th style="text-align:left">รายการประเมิน</th><th>5</th><th>4</th><th>3</th><th>2</th><th>1</th><th>X̄</th><th>S.D.</th><th>แปลผล</th></tr></thead>`;
html += `<tbody>`;
let totalMean = 0, totalSD = 0, count = 0;
ratingQuestions.forEach(q => {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `<tr style="font-size:18px;">`;
html += `<td style="text-align:left; font-weight:700; color:var(--text-main);">${escapeHtml(cleanText)}</td>`;
for (let i = 4; i >= 0; i--) {
const freq = q.frequencies?.[i] || { count: 0 };
html += `<td>${freq.count}</td>`;
}
html += `<td class="fw-700" style="color:var(--primary-color);">${q.mean?.toFixed(2) || '-'}</td>`;
html += `<td>${q.sd?.toFixed(2) || '-'}</td>`;
html += `<td><span class="badge badge-active">${q.interpretation || '-'}</span></td>`;
html += `</tr>`;
totalMean += q.mean || 0;
totalSD += q.sd || 0;
count++;
});
if (count > 0) {
const avgMean = totalMean / count;
const avgSD = totalSD / count;
let avgInterp = '';
if (avgMean >= 4.51) avgInterp = 'มากที่สุด';
else if (avgMean >= 3.51) avgInterp = 'มาก';
else if (avgMean >= 2.51) avgInterp = 'ปานกลาง';
else if (avgMean >= 1.51) avgInterp = 'น้อย';
else avgInterp = 'น้อยที่สุด';
html += `<tr style="font-weight:800;background:var(--gray-50); color:var(--primary-dark);">`;
html += `<td style="text-align:left">รวมเฉลี่ย</td><td colspan="5"></td>`;
html += `<td>${avgMean.toFixed(2)}</td><td>${avgSD.toFixed(2)}</td>`;
html += `<td><span class="badge badge-active">${avgInterp}</span></td></tr>`;
}
html += `</tbody></table></div>`;
}
// Text responses
questions.filter(q => q.question_type === 'text').forEach(q => {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `<div class="mt-32">`;
html += `<p class="fw-800 mb-12" style="color:var(--primary-dark); border-left:4px solid var(--primary-color); padding-left:12px; font-size:20px;">${escapeHtml(cleanText)}</p>`;
if (q.text_responses && q.text_responses.length > 0) {
q.text_responses.forEach((t, i) => {
html += `<div style="padding:12px; background:var(--bg-secondary); border-radius:var(--radius-md); margin-bottom:8px; font-size:14px; border-left:2px solid var(--border-color);">${i+1}. ${escapeHtml(t)}</div>`;
});
} else {
html += `<p class="text-muted text-sm italic" style="padding-left:16px;">(ไม่มีข้อคิดเห็น)</p>`;
}
html += `</div>`;
});
return html;
}
async function initStats() {
const urlParams = new URLSearchParams(window.location.search);
const surveyId = urlParams.get('survey_id');
if (!surveyId) {
alert('ไม่ระบุ ID แบบสอบถาม');
window.close();
return;
}
const statsEl = document.getElementById('statsMainContent');
const actionsEl = document.getElementById('statsActions');
const titleEl = document.getElementById('statsSurveyTitle');
try {
const [summary, detail, resListData] = await Promise.all([
API.get(`statistics.php?action=summary&survey_id=${surveyId}`),
API.get(`statistics.php?action=detail&survey_id=${surveyId}`),
API.get(`responses.php?action=list&survey_id=${surveyId}`)
]);
titleEl.textContent = `ผลสถิติ: ${summary.survey.title}`;
document.title = `ผลสถิติ: ${summary.survey.title} - TPU Survey`;
actionsEl.innerHTML = `
<button class="btn btn-success btn-sm" onclick="window.open('api/export.php?action=word&survey_id=${surveyId}','_blank')">📄 Export Word</button>
<button class="btn btn-primary btn-sm" onclick="window.open('api/export.php?action=pdf&survey_id=${surveyId}','_blank')">📑 Export PDF</button>
`;
let html = `
<div class="stat-grid mb-32">
<div class="stat-card">
<div class="stat-icon purple">📊</div>
<div class="stat-content">
<h4 style="font-size:0.9rem; font-weight:700;">ค่าเฉลี่ยรวม (X̄)</h4>
<div class="stat-value" style="font-size:36px; font-weight:800;">${summary.overall_mean.toFixed(2)}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon teal">📉</div>
<div class="stat-content">
<h4 style="font-size:0.9rem; font-weight:700;">ส่วนเบี่ยงเบนมาตรฐาน (S.D.)</h4>
<div class="stat-value" style="font-size:36px; font-weight:800;">${summary.overall_sd.toFixed(2)}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">👥</div>
<div class="stat-content">
<h4 style="font-size:0.9rem; font-weight:700;">จำนวนผู้ตอบ</h4>
<div class="stat-value" style="font-size:36px; font-weight:800;">${summary.total_responses}</div>
</div>
</div>
</div>
`;
(detail.sections || []).forEach((section, index) => {
html += `<div class="card mb-32" style="border:none; box-shadow:var(--shadow-md); border-radius:var(--radius-xl); overflow:hidden;">`;
html += `<div class="card-header" style="background:var(--bg-secondary); padding:24px 32px;">
<h3 style="color:var(--primary-dark); font-weight:800; font-size:24px;">${escapeHtml(section.title)}</h3>`;
if (index === 0) {
html += `
<div style="margin-top: 12px; padding: 12px 20px; background: rgba(255,255,255,0.5); border-radius: 12px; border: 1px solid var(--border-color); display: inline-block;">
<span style="font-weight: 700; color: var(--text-main); font-size: 1.1rem;">📊 จำนวนยอดการตอบแบบสอบถามทั้งหมด:</span>
<span style="font-size: 1.5rem; font-weight: 800; color: var(--primary-color); margin-left: 8px;">${detail.total_responses}</span>
<span style="font-weight: 700; color: var(--text-main); margin-left: 4px;">ชุด</span>
</div>
`;
}
html += `</div>`;
html += `<div class="card-body" style="padding:32px;">`;
// Collect all questions for this section (from headings + loose)
const allQuestions = [];
// Render headings
(section.headings || []).forEach(heading => {
if (heading.questions && heading.questions.length > 0) {
// Add heading sub-header
html += `
<div style="margin:24px 0 16px; padding:12px 16px; background:linear-gradient(135deg, rgba(37,99,235,0.06), rgba(37,99,235,0.10)); border-left:4px solid var(--primary-color); border-radius:0 8px 8px 0;">
<h4 style="margin:0; font-size:1.05rem; font-weight:700; color:var(--primary-dark);">📑 ${escapeHtml(heading.title)}</h4>
${heading.description ? `<p style="margin:4px 0 0; font-size:0.85rem; color:var(--text-muted);">${escapeHtml(heading.description)}</p>` : ''}
</div>
`;
html += renderStatsQuestions(heading.questions);
heading.questions.forEach(q => allQuestions.push(q));
}
});
// Render loose questions
const looseQuestions = section.questions || [];
if (looseQuestions.length > 0) {
if ((section.headings || []).length > 0) {
html += `<div style="margin:24px 0 16px; padding-bottom:8px; border-bottom:1px dashed var(--border-color);"><span style="font-size:13px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;">📋 คำถามทั่วไป</span></div>`;
}
html += renderStatsQuestions(looseQuestions);
looseQuestions.forEach(q => allQuestions.push(q));
}
html += `</div></div>`;
});
// --- ส่วนที่ 4: รายการคำตอบทั้งหมด (Individual Responses) ---
const responses = resListData.responses || [];
html += `
<div class="card mb-32" style="border:none; box-shadow:var(--shadow-md); border-radius:var(--radius-xl); overflow:hidden;">
<div class="card-header" style="background:var(--bg-secondary); padding:24px 32px;">
<h3 style="color:var(--primary-dark); font-weight:800; font-size:24px;">📝 รายการคำตอบทั้งหมด (${responses.length} ชุด)</h3>
</div>
<div class="card-body" style="padding:0;">
<div class="table-container">
<table style="width:100%; border-collapse:collapse;">
<thead>
<tr style="background:var(--bg-secondary);">
<th style="padding:16px 24px; text-align:center;">#</th>
<th style="padding:16px 24px; text-align:left;">วัน-เวลาที่ส่ง</th>
<th style="padding:16px 24px; text-align:left;">IP Address</th>
<th style="padding:16px 24px; text-align:center;">จัดการ</th>
</tr>
</thead>
<tbody>
`;
if (responses.length === 0) {
html += `<tr><td colspan="4" style="padding:40px; text-align:center; color:var(--text-muted);">ยังไม่มีผู้ตอบแบบสอบถาม</td></tr>`;
} else {
responses.forEach((r, idx) => {
html += `
<tr style="border-bottom:1px solid var(--border-color);">
<td style="padding:16px 24px; text-align:center; font-weight:600;">${responses.length - idx}</td>
<td style="padding:16px 24px;">${new Date(r.submitted_at).toLocaleString('th-TH')}</td>
<td style="padding:16px 24px;">${r.ip_address || '-'}</td>
<td style="padding:16px 24px; text-align:center;">
<button class="btn btn-ghost btn-sm" onclick="viewResponseDetail(${r.id})">🔍 ดูคำตอบ</button>
</td>
</tr>
`;
});
}
html += `
</tbody>
</table>
</div>
</div>
</div>
`;
statsEl.innerHTML = html;
} catch (err) {
statsEl.innerHTML = `<div class="empty-state"><div class="empty-icon">❌</div><h3>ไม่สามารถโหลดข้อมูลได้</h3><p>${err.message}</p><button class="btn btn-primary mt-16" onclick="window.close()">ปิดหน้านี้</button></div>`;
}
}
async function viewResponseDetail(responseId) {
try {
const data = await API.get(`responses.php?action=get&id=${responseId}`);
const r = data.response;
let html = `
<div style="margin-bottom:20px; padding:15px; background:var(--bg-secondary); border-radius:12px;">
<p><strong>วันเวลาที่ส่ง:</strong> ${new Date(r.submitted_at).toLocaleString('th-TH')}</p>
<p><strong>IP Address:</strong> ${r.ip_address}</p>
</div>
<div class="response-detail-list">
`;
r.answers.forEach((ans, idx) => {
const cleanText = ans.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--border-color);">
<div style="font-weight:700; color:var(--primary-dark); margin-bottom:4px;">${escapeHtml(cleanText)}</div>
<div style="padding-left:20px; color:var(--text-main);">
<span style="font-weight:600; color:var(--primary-color);">คำตอบ: </span>
<span>${escapeHtml(ans.answer_value)}</span>
</div>
</div>
`;
});
html += `</div>`;
Modal.create(`รายละเอียดคำตอบ #${r.id}`, html, '', 'modal-lg');
} catch (err) {
alert('ไม่สามารถโหลดรายละเอียดได้: ' + err.message);
}
}
document.addEventListener('DOMContentLoaded', initStats);
+294
View File
@@ -0,0 +1,294 @@
/**
* TPU Survey - Public Survey Response Logic
* รองรับ Section -> Heading -> Question
*/
let currentSurvey = null;
async function initSurvey() {
const params = new URLSearchParams(window.location.search);
const link = params.get('link');
if (!link) {
showError('ไม่พบลิงก์แบบสอบถาม');
return;
}
try {
const data = await fetch(`/survey/api/surveys.php?action=getByLink&link=${encodeURIComponent(link)}`).then(r => r.json());
if (data.error) {
showError(data.error);
return;
}
currentSurvey = data.survey;
renderSurvey();
} catch (err) {
showError('ไม่สามารถโหลดแบบสอบถามได้');
}
}
function showError(msg) {
document.getElementById('surveyContent').innerHTML = `
<div class="empty-state" style="padding:80px 20px;">
<div class="empty-icon">😔</div>
<h3>${msg}</h3>
<p class="text-muted mt-8">ลิงก์อาจไม่ถูกต้องหรือแบบสอบถามถูกปิดแล้ว</p>
</div>
`;
}
function renderSurvey() {
const container = document.getElementById('surveyContent');
let html = `
<div class="survey-header" style="text-align:center;">
<img src="pic/logo.png" alt="TPU Logo" style="display: block; margin-left: auto; margin-right: auto; margin-bottom: 16px; height: 80px; width: auto;">
<h1>${escapeHtml(currentSurvey.title)}</h1>
${currentSurvey.description ? `<p>${escapeHtml(currentSurvey.description)}</p>` : ''}
</div>
<form id="surveyResponseForm" onsubmit="submitSurveyResponse(event)">
${currentSurvey.has_certificate == 1 ? `
<!-- ส่วนข้อมูลสำหรับรับเกียรติบัตร -->
<div class="survey-section" style="border-top: 4px solid var(--primary-color, #2563eb);">
<div class="survey-section-header">
<h2 style="font-size:1.2rem; font-weight:700; color:#1e3a8a;">🎓 ข้อมูลสำหรับรับเกียรติบัตร (ข้อมูลจำเป็น)</h2>
<p style="color: #4b5563; font-size:0.9rem; margin-top:4px;">กรุณาระบุชื่อ-นามสกุลจริง และอีเมลมหาวิทยาลัยตาปี หรือ Gmail เพื่อระบบจะทำการส่งไฟล์เกียรติบัตรผ่านทางอีเมลโดยอัตโนมัติ</p>
</div>
<div class="survey-question" style="margin-top:16px;">
<div class="q-label" style="font-weight:700;">
<span>ชื่อ-นามสกุลจริง (ภาษาไทย เช่น นายสมชาย ใจดี)</span>
<span class="required">*</span>
</div>
<input type="text" class="form-input" id="respondentName" placeholder="กรอกชื่อ-นามสกุลจริง เช่น นายสมชาย ใจดี" required style="width: 100%; max-width: 500px; margin-top: 8px;">
</div>
<div class="survey-question" style="margin-top:16px;">
<div class="q-label" style="font-weight:700;">
<span>อีเมลสำหรับรับเกียรติบัตร (มหาวิทยาลัยตาปี หรือ Gmail)</span>
<span class="required">*</span>
</div>
<input type="email" class="form-input" id="respondentEmail" placeholder="เช่น name@gmail.com หรือ 651234567@student.tapee.ac.th" required
pattern="[a-zA-Z0-9._%+-]+@((student\\.)?tapee\\.ac\\.th|gmail\\.com|gmil\\.com)"
title="กรุณาใช้อีเมลมหาวิทยาลัยตาปี หรือ Gmail เท่านั้น"
style="width: 100%; max-width: 500px; margin-top: 8px;">
</div>
</div>
` : ''}
`;
(currentSurvey.sections || []).forEach(section => {
const cleanSectionTitle = section.title.replace(/^ส่วนที่\s*[\d๑-๙]+\s*/, '').replace(/^\s*[\d๑-๙]+[\.\)\s\-:\u00A0]*\s*/, '');
html += `
<div class="survey-section">
<div class="survey-section-header">
<h2>${escapeHtml(cleanSectionTitle)}</h2>
${section.description ? `<p>${escapeHtml(section.description)}</p>` : ''}
</div>
`;
// Render headings and their questions
(section.headings || []).forEach(heading => {
const cleanHeadingTitle = heading.title.replace(/^\s*[\d๑-๙]+[\.\)\s\-:\u00A0]*\s*/, '');
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;">
<h3 style="margin:0; font-size:1.1rem; font-weight:700; color:#1e3a8a;">📑 ${escapeHtml(cleanHeadingTitle)}</h3>
${heading.description ? `<p style="margin:4px 0 0; font-size:0.9rem; color:#475569;">${escapeHtml(heading.description)}</p>` : ''}
</div>
`;
(heading.questions || []).forEach(q => {
html += renderQuestion(q);
});
});
// Render loose questions (not in any heading)
(section.questions || []).forEach(q => {
html += renderQuestion(q);
});
html += `</div>`;
});
html += `
<div class="text-center mt-32">
<button type="submit" id="submitBtn" class="btn btn-primary btn-lg" style="min-width:200px;">
📤 ส่งแบบสอบถาม
</button>
</div>
</form>
`;
container.innerHTML = html;
}
function renderQuestion(q) {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s\-:\u00A0]*\s*/, '');
let html = `
<div class="survey-question">
<div class="q-label">
<span>${escapeHtml(cleanText)}</span>
${q.is_required ? '<span class="required">*</span>' : ''}
</div>
`;
if (q.description) {
html += `<p style="font-size:0.85rem; color:#000000; margin:-4px 0 10px 0;">${escapeHtml(q.description)}</p>`;
}
if (q.question_type === 'radio') {
if (q.options && q.options.length > 0) {
html += `<div class="form-radio-group" style="flex-direction:column; align-items:flex-start;">`;
q.options.forEach(opt => {
html += `
<label class="radio-option" style="width:100%; border:none; padding:8px 0;">
<input type="radio" name="q_${q.id}" value="${escapeHtml(opt)}" ${q.is_required ? 'required' : ''}>
<span>${escapeHtml(opt)}</span>
</label>
`;
});
html += `</div>`;
} else {
html += `<div class="rating-scale">`;
const labels = ['น้อยที่สุด', 'น้อย', 'ปานกลาง', 'มาก', 'มากที่สุด'];
for (let i = 1; i <= 5; i++) {
html += `
<label>
<input type="radio" name="q_${q.id}" value="${i}" ${q.is_required ? 'required' : ''}>
<span class="rating-number">${i}</span>
<span class="rating-label">${labels[i-1]}</span>
</label>
`;
}
html += `</div>`;
}
} else if (q.question_type === 'text') {
html += `<textarea class="form-input" name="q_${q.id}" placeholder="กรอกความคิดเห็น..." rows="3" ${q.is_required ? 'required' : ''}></textarea>`;
} else if (q.question_type === 'dropdown') {
const options = q.options || [];
html += `<select class="form-input" name="q_${q.id}" ${q.is_required ? 'required' : ''}>`;
html += `<option value="">-- กรุณาเลือก --</option>`;
options.forEach(opt => {
const val = typeof opt === 'object' ? opt.value : opt;
const label = typeof opt === 'object' ? opt.label : opt;
html += `<option value="${escapeHtml(val)}">${escapeHtml(label)}</option>`;
});
html += `</select>`;
} else if (q.question_type === 'checkbox') {
const options = q.options || [];
html += `<div class="form-checkbox-group">`;
options.forEach((opt, oi) => {
const val = typeof opt === 'object' ? opt.value : opt;
const label = typeof opt === 'object' ? opt.label : opt;
html += `
<label class="checkbox-option">
<input type="checkbox" name="q_${q.id}_${oi}" value="${escapeHtml(val)}">
<span>${escapeHtml(label)}</span>
</label>
`;
});
html += `</div>`;
}
html += `</div>`;
return html;
}
// Collect all questions flat for submission
function getAllQuestions() {
const questions = [];
(currentSurvey.sections || []).forEach(section => {
(section.headings || []).forEach(heading => {
(heading.questions || []).forEach(q => questions.push(q));
});
(section.questions || []).forEach(q => questions.push(q));
});
return questions;
}
async function submitSurveyResponse(e) {
e.preventDefault();
const btn = document.getElementById('submitBtn');
btn.disabled = true;
btn.textContent = 'กำลังส่ง...';
const form = document.getElementById('surveyResponseForm');
const answers = [];
const allQuestions = getAllQuestions();
allQuestions.forEach(q => {
if (q.question_type === 'checkbox') {
const values = [];
form.querySelectorAll(`[name^="q_${q.id}_"]`).forEach(cb => {
if (cb.checked) values.push(cb.value);
});
answers.push({ question_id: q.id, value: values.join(', ') });
} else {
const el = form.querySelector(`[name="q_${q.id}"]`);
if (el) {
if (el.type === 'radio') {
const checked = form.querySelector(`[name="q_${q.id}"]:checked`);
answers.push({ question_id: q.id, value: checked ? checked.value : '' });
} else {
answers.push({ question_id: q.id, value: el.value });
}
}
}
});
// Get respondent information
const nameEl = document.getElementById('respondentName');
const emailEl = document.getElementById('respondentEmail');
const respondentName = nameEl ? nameEl.value.trim() : '';
const respondentEmail = emailEl ? emailEl.value.trim() : '';
const params = new URLSearchParams(window.location.search);
try {
const res = await fetch('/survey/api/responses.php?action=submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
survey_link: params.get('link'),
answers: answers,
respondent_info: {
name: respondentName,
email: respondentEmail
}
})
});
const data = await res.json();
if (data.success) {
document.getElementById('surveyContent').innerHTML = `
<div class="success-page" style="min-height:auto;padding:60px 20px;">
<div style="text-align:center;">
<img src="pic/logo.png" alt="TPU Logo" style="display: block; margin-left: auto; margin-right: auto; margin-bottom: 24px; height: 80px; width: auto;">
<div class="success-icon">✓</div>
<h2 style="font-size:24px;margin-bottom:12px;">ส่งแบบสอบถามสำเร็จ!</h2>
<p class="text-muted">ขอบคุณที่สละเวลาตอบแบบสอบถาม</p>
</div>
</div>
`;
} else {
throw new Error(data.error || 'เกิดข้อผิดพลาด');
}
} catch (err) {
alert('เกิดข้อผิดพลาด: ' + err.message);
btn.disabled = false;
btn.textContent = '📤 ส่งแบบสอบถาม';
}
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
document.addEventListener('DOMContentLoaded', initSurvey);
+804
View File
@@ -0,0 +1,804 @@
/**
* TPU Survey - Teacher Dashboard Logic
*/
let teacherSurveys = [];
async function initTeacher() {
const loggedIn = await Auth.check();
if (!loggedIn || !Auth.isTeacher()) {
if (Auth.isAdmin()) {
window.location.href = 'admin.html';
return;
}
window.location.href = 'index.html';
return;
}
document.getElementById('userName').textContent = Auth.user.full_name;
document.getElementById('userRole').textContent = 'อาจารย์ / เจ้าของโครงการ';
document.getElementById('userAvatar').textContent = Auth.user.full_name.charAt(0);
showTeacherView('mySurveys');
}
function showTeacherView(view) {
document.querySelectorAll('.view-section').forEach(s => s.classList.add('hidden'));
document.getElementById(`view-${view}`)?.classList.remove('hidden');
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelector(`.nav-item[data-view="${view}"]`)?.classList.add('active');
const titles = { mySurveys: 'แบบสอบถามของฉัน', profile: 'ข้อมูลส่วนตัว' };
document.getElementById('pageTitle').textContent = titles[view] || '';
if (view === 'mySurveys') loadTeacherSurveys();
else if (view === 'profile') loadProfile();
}
async function loadTeacherSurveys() {
try {
const data = await API.get('surveys.php?action=list');
teacherSurveys = data.surveys || [];
renderTeacherSurveys();
} catch (err) {
Toast.error(err.message);
}
}
function renderTeacherSurveys() {
const container = document.getElementById('teacherSurveysList') || document.getElementById('teacherSurveysBody');
if (!container) return;
if (teacherSurveys.length === 0) {
const emptyHtml = `
<div class="card-tapi p-8 text-center bg-white rounded-lg border border-slate-200">
<i class="fas fa-clipboard-list text-4xl text-blue-300 mb-3 block"></i>
<p class="text-gray-600 font-bold">ยังไม่มีแบบสอบถาม กดปุ่ม "สร้างแบบสอบถาม" เพื่อเริ่มต้น</p>
</div>
`;
if (container.tagName === 'TBODY') {
container.innerHTML = `<tr><td colspan="5" class="text-center text-muted" style="padding:40px">ยังไม่มีแบบสอบถาม กดปุ่ม "สร้างแบบสอบถาม" เพื่อเริ่มต้น</td></tr>`;
} else {
container.innerHTML = emptyHtml;
}
return;
}
const statusMap = {
active: { label: 'เปิดใช้งาน', bg: 'bg-emerald-100 text-emerald-800 border-emerald-300' },
draft: { label: 'ฉบับร่าง', bg: 'bg-amber-100 text-amber-800 border-amber-300' },
closed: { label: 'ปิดแล้ว', bg: 'bg-rose-100 text-rose-800 border-rose-300' }
};
container.innerHTML = teacherSurveys.map(s => {
const st = statusMap[s.status] || { label: s.status || 'เปิดใช้งาน', bg: 'bg-emerald-100 text-emerald-800 border-emerald-300' };
return `
<div class="card-tapi p-3.5 rounded-xl bg-white flex flex-col lg:flex-row justify-between items-start lg:items-center gap-3 transition-all hover:shadow-md border border-slate-200">
<div class="flex flex-col sm:flex-row sm:items-center gap-2.5 flex-1 min-w-0">
<span class="px-2 py-0.5 text-[11px] font-extrabold rounded border whitespace-nowrap self-start sm:self-auto ${st.bg}">${st.label}</span>
<div class="space-y-0.5 flex-1 min-w-0">
<h4 class="font-bold text-xs sm:text-sm text-blue-950 truncate" title="${escapeHtml(s.title)}">
${escapeHtml(s.title)}
</h4>
<div class="flex items-center gap-3 text-[11px] text-gray-500 font-semibold flex-wrap">
<span><i class="fas fa-comments text-blue-600 mr-1"></i> ${s.response_count || 0} คำตอบ</span>
<span><i class="fas fa-calendar-alt text-amber-600 mr-1"></i> ${formatDate(s.created_at)}</span>
</div>
</div>
</div>
<div class="flex flex-wrap lg:flex-nowrap gap-1 items-center flex-shrink-0 w-full lg:w-auto overflow-x-auto pb-1 lg:pb-0">
<button onclick="teacherEditSurvey(${s.id})" class="bg-amber-400 hover:bg-amber-500 text-gray-900 border border-slate-800 px-2 py-1 rounded text-xs font-bold flex items-center gap-1 shadow-sm transition-all whitespace-nowrap active:scale-95" title="แก้ไขแบบสอบถาม">
✏️ แก้ไข
</button>
<button onclick="teacherDuplicateSurvey(${s.id})" class="bg-blue-600 hover:bg-blue-700 text-white border border-slate-800 px-2 py-1 rounded text-xs font-bold flex items-center gap-1 shadow-sm transition-all whitespace-nowrap active:scale-95" title="คัดลอกแบบสอบถาม">
📑 คัดลอก
</button>
<button onclick="teacherGenerateLink(${s.id})" class="bg-blue-900 hover:bg-blue-950 text-white border border-slate-800 px-2 py-1 rounded text-xs font-bold flex items-center gap-1 shadow-sm transition-all whitespace-nowrap active:scale-95" title="ลิงก์และ QR Code">
🔗 ลิงก์
</button>
<button onclick="showImportResponsesModal(${s.id}, '${escapeHtml(s.title)}')" class="bg-emerald-600 hover:bg-emerald-700 text-white border border-slate-800 px-2 py-1 rounded text-xs font-bold flex items-center gap-1 shadow-sm transition-all whitespace-nowrap active:scale-95" title="นำเข้าคำตอบจาก CSV">
📥 นำเข้าคำตอบ
</button>
<button onclick="teacherViewStats(${s.id})" class="bg-indigo-600 hover:bg-indigo-700 text-white border border-slate-800 px-2 py-1 rounded text-xs font-bold flex items-center gap-1 shadow-sm transition-all whitespace-nowrap active:scale-95" title="ผลสถิติ">
📊 สถิติ
</button>
<button onclick="teacherDeleteSurvey(${s.id}, '${escapeHtml(s.title)}')" class="bg-rose-600 hover:bg-rose-700 text-white border border-slate-800 px-2 py-1 rounded text-xs font-bold flex items-center gap-1 shadow-sm transition-all whitespace-nowrap active:scale-95" title="ลบแบบสอบถาม">
🗑️ ลบ
</button>
</div>
</div>
`;
}).join('');
}
async function teacherDeleteSurvey(id, title) {
if (!confirm(`คุณต้องการลบแบบสอบถาม "${title}" ใช่หรือไม่?`)) return;
try {
await API.get(`surveys.php?action=delete&id=${id}`);
Toast.success('ลบแบบสอบถามเรียบร้อยแล้ว');
loadTeacherSurveys();
} catch (err) {
Toast.error(err.message);
}
}
function showTeacherCreateModal() {
const html = `
<form id="teacherSurveyForm">
<div class="form-group">
<label class="form-label">ชื่อแบบสอบถาม *</label>
<input type="text" class="form-input" name="title" required placeholder="เช่น แบบประเมินความพึงพอใจโครงการ...">
</div>
<div class="form-group">
<label class="form-label">รายละเอียด</label>
<textarea class="form-input" name="description" placeholder="คำอธิบายเพิ่มเติม"></textarea>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="Modal.hide('tCreateModal');document.getElementById('tCreateModal').remove();">ยกเลิก</button>
<button class="btn btn-primary" onclick="submitTeacherCreate()">สร้างแบบสอบถาม (ใช้ 3 ส่วนมาตรฐาน)</button>
`;
Modal.create('สร้างแบบสอบถามใหม่', html, footer);
document.querySelector('.modal-overlay:last-child').id = 'tCreateModal';
}
async function submitTeacherCreate() {
const form = document.getElementById('teacherSurveyForm');
const fd = new FormData(form);
const data = Object.fromEntries(fd);
if (!data.title) { Toast.warning('กรุณากรอกชื่อ'); return; }
// Pre-fill 3 sections
data.sections = [
{
section_number: 1,
title: 'ส่วนที่ ข้อมูลทั่วไป',
category: 'general',
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 }
]
},
{
section_number: 2,
title: 'ส่วนที่ การประเมินความพึงพอใจ',
category: 'rating',
questions: [
{ question_text: 'ด้านเนื้อหาของการจัดโครงการ', question_type: 'radio', is_required: 1 },
{ question_text: 'ด้านวิทยากร / ผู้บรรยาย', question_type: 'radio', is_required: 1 }
]
},
{
section_number: 3,
title: 'ส่วนที่ ข้อเสนอแนะอื่นๆ',
category: 'suggestions',
questions: [
{ question_text: 'ข้อเสนอแนะเพิ่มเติม', question_type: 'text', is_required: 0 }
]
}
];
try {
const result = await API.post('surveys.php?action=create', data);
Toast.success('สร้างแบบสอบถามสำเร็จ');
Modal.hide('tCreateModal');
document.getElementById('tCreateModal').remove();
loadTeacherSurveys();
setTimeout(() => teacherEditSurvey(result.id), 500);
} catch (err) {
Toast.error(err.message);
}
}
function teacherEditSurvey(id) {
window.open(`edit.html?id=${id}`, '_blank');
}
function buildTeacherQEditor(si, qi, q = {}) {
const qType = q.question_type || 'radio';
const qText = q.question_text || '';
const qOptions = Array.isArray(q.options) ? q.options.join('\n') : '';
const showOptions = ['radio', 'checkbox', 'dropdown'].includes(qType);
const qDescription = q.description || '';
return `
<div class="question-builder">
<div class="qb-header">
<span class="qb-handle">⋮⋮</span>
<span class="text-sm fw-600 q-number-label">คำถาม</span>
<div class="qb-actions">
<button type="button" class="btn btn-ghost btn-sm" onclick="addTeacherQ(${si}, this.closest('.question-builder'))" title="แทรกคำถามใต้ข้อนี้">➕ แทรก</button>
<button type="button" class="btn btn-ghost btn-sm text-danger" onclick="removeTeacherQ(this, ${si})" title="ลบคำถาม">🗑️ ลบ</button>
</div>
</div>
<div class="form-group">
<label class="form-label">คำถาม / ข้อความ</label>
<input type="text" class="form-input q-text-input" value="${escapeHtml(qText)}" placeholder="พิมพ์คำถาม...">
</div>
<div class="form-group">
<label class="form-label">รายละเอียดเพิ่มเติม (คำใบ้/คำอธิบาย - ไม่บังคับ)</label>
<input type="text" class="form-input q-desc-input" value="${escapeHtml(qDescription)}" placeholder="เช่น กรุณาระบุเป็นภาษาไทย">
</div>
<div class="form-group">
<label class="form-label">ประเภทคำถาม</label>
<select class="form-input q-type-select" onchange="toggleTeacherOptions(this)">
<option value="radio" ${qType==='radio'?'selected':''}>📊 ระดับ 1-5</option>
<option value="text" ${qType==='text'?'selected':''}>📝 ข้อความ</option>
<option value="dropdown" ${qType==='dropdown'?'selected':''}>📋 Dropdown</option>
<option value="checkbox" ${qType==='checkbox'?'selected':''}>☑️ Checkbox</option>
</select>
</div>
<div class="form-group q-options-group" style="${showOptions ? '' : 'display:none;'}">
<label class="form-label">ตัวเลือก (1 รายการต่อบรรทัด)</label>
<textarea class="form-input q-options-input" placeholder="ตัวเลือกที่ 1\nตัวเลือกที่ 2" rows="3">${escapeHtml(qOptions)}</textarea>
<small class="text-muted">เว้นว่างสำหรับระดับ 1-5 (Radio)</small>
</div>
</div>
`;
}
function addTeacherSection() {
const container = document.getElementById('additionalTeacherSections');
if (!container) return;
const si = document.querySelectorAll('.card[data-section]').length;
const html = `
<div class="card mb-24" data-section="${si}">
<div class="card-header d-flex justify-between items-center">
<input type="text" class="form-input" style="font-weight:700;font-size:16px;border:none;padding:4px 0;background:transparent;"
value="ส่วนที่ ข้อมูลเพิ่มเติม" data-field="section-title-${si}">
<button type="button" class="btn btn-ghost btn-sm text-danger" onclick="this.closest('.card').remove()">🗑️ ลบส่วนนี้</button>
</div>
<div class="card-body">
<div id="t-questions-${si}"></div>
<button type="button" class="btn btn-secondary btn-sm mt-16" onclick="addTeacherQ(${si})">+ เพิ่มคำถาม</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
// Add one initial question to the new section
addTeacherQ(si);
}
function toggleTeacherOptions(select) {
const group = select.closest('.question-builder').querySelector('.q-options-group');
if (['radio', 'checkbox', 'dropdown'].includes(select.value)) {
group.style.display = 'block';
} else {
group.style.display = 'none';
}
}
function addTeacherQ(si, afterEl = null) {
const container = document.getElementById(`t-questions-${si}`);
if (!container) return;
// Create temporary div to parse HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = buildTeacherQEditor(si, 0);
const newQ = tempDiv.firstElementChild;
if (afterEl) {
afterEl.after(newQ);
} else {
container.appendChild(newQ);
}
reindexTeacherQuestions(si);
}
function removeTeacherQ(btn, si) {
if (!confirm('ยืนยันลบคำถามนี้?')) return;
btn.closest('.question-builder').remove();
reindexTeacherQuestions(si);
}
function reindexTeacherQuestions(si) {
const container = document.getElementById(`t-questions-${si}`);
if (!container) return;
container.querySelectorAll('.question-builder').forEach((qEl, idx) => {
const label = qEl.querySelector('.q-number-label');
if (label) label.textContent = `คำถาม`;
});
}
async function submitTeacherEdit() {
const form = document.getElementById('tEditForm');
const id = form.querySelector('[name="id"]').value;
const title = form.querySelector('[name="title"]').value;
const description = form.querySelector('[name="description"]').value;
const sections = [];
form.querySelectorAll('.card[data-section]').forEach((sectionEl, si) => {
const sTitleInput = sectionEl.querySelector('input[data-field^="section-title"]');
const sCategorySelect = sectionEl.querySelector('select[data-field^="section-category"]');
const sTitle = sTitleInput ? sTitleInput.value : `ส่วนที่ ${si+1}`;
const sCategory = sCategorySelect ? sCategorySelect.value : 'rating';
const questions = [];
sectionEl.querySelectorAll('.question-builder').forEach((qEl, qi) => {
const qTextInput = qEl.querySelector('.q-text-input');
const qDescInput = qEl.querySelector('.q-desc-input');
const qTypeSelect = qEl.querySelector('.q-type-select');
const qOptionsArea = qEl.querySelector('.q-options-input');
const qText = qTextInput ? qTextInput.value : '';
const qDescription = qDescInput ? qDescInput.value : '';
const qType = qTypeSelect ? qTypeSelect.value : 'radio';
const qOptionsRaw = qOptionsArea ? qOptionsArea.value : '';
const options = qOptionsRaw.split('\n').map(o => o.trim()).filter(o => o !== '');
if (qText.trim()) {
questions.push({
question_text: qText.trim(),
description: qDescription.trim() || null,
question_type: qType,
order_num: qi,
is_required: 1,
options: options.length > 0 ? options : null
});
}
});
sections.push({ section_number: si + 1, title: sTitle, category: sCategory, questions });
});
try {
await API.post('surveys.php?action=update', { id: parseInt(id), title, description, sections });
Toast.success('บันทึกสำเร็จ');
Modal.hide('tEditModal');
document.getElementById('tEditModal').remove();
loadTeacherSurveys();
} catch (err) {
Toast.error(err.message);
}
}
async function teacherDuplicateSurvey(id) {
if (!confirm('ยืนยันสร้างสำเนาแบบสอบถามนี้?')) return;
try {
await API.post('surveys.php?action=duplicate', { id });
Toast.success('คัดลอกสำเร็จ');
loadTeacherSurveys();
} catch (err) {
Toast.error(err.message);
}
}
async function teacherGenerateLink(id) {
try {
const data = await API.post('surveys.php?action=generateLink', { id });
const fullUrl = data.full_url || `${window.location.origin}/survey/respond.php?link=${data.unique_link}`;
// Find survey title
const survey = teacherSurveys.find(s => s.id == id);
const surveyTitle = survey ? survey.title : 'แบบสอบถาม';
const html = `
<div class="qr-link-container">
<div class="qr-link-section">
<div class="qr-link-label">🔗 ลิงก์แบบสอบถาม</div>
<div class="link-display">
<span class="flex-1" id="surveyLinkText">${fullUrl}</span>
<button class="copy-btn" onclick="copyTeacherLink()">📋 คัดลอก</button>
</div>
</div>
<div class="qr-code-section">
<div class="qr-link-label">📱 QR Code สำหรับตอบแบบสอบถาม</div>
<div class="qr-code-wrapper" id="qrCodeWrapper">
<div id="qrCodeContainer"></div>
</div>
<p class="qr-survey-title" id="qrSurveyTitle">${escapeHtml(surveyTitle)}</p>
<div class="qr-actions">
<button class="btn btn-primary btn-sm" onclick="teacherDownloadQR()">📥 ดาวน์โหลด QR</button>
<button class="btn btn-secondary btn-sm" onclick="teacherPrintQR()">🖨️ พิมพ์ QR</button>
</div>
</div>
<p class="mt-16 text-sm text-muted" style="text-align:center;">⚡ แบบสอบถามถูกเปิดใช้งานอัตโนมัติเมื่อสร้างลิงก์</p>
</div>
`;
Modal.create('🔗 ลิงก์และ QR Code', html, '', 'modal-lg');
// Generate QR Code using QRCodeJS
setTimeout(() => {
const container = document.getElementById('qrCodeContainer');
if (container && typeof QRCode !== 'undefined') {
new QRCode(container, {
text: fullUrl,
width: 280,
height: 280,
colorDark: '#1e3a8a',
colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.H
});
}
}, 100);
} catch (err) {
Toast.error(err.message);
}
}
function copyTeacherLink() {
const text = document.getElementById('surveyLinkText')?.textContent;
if (text) {
navigator.clipboard.writeText(text).then(() => Toast.success('คัดลอกแล้ว')).catch(() => {
const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove();
Toast.success('คัดลอกแล้ว');
});
}
}
function getTeacherQRImage() {
const container = document.getElementById('qrCodeContainer');
if (!container) return null;
return container.querySelector('canvas') || container.querySelector('img');
}
function teacherDownloadQR() {
const qrEl = getTeacherQRImage();
const title = document.getElementById('qrSurveyTitle')?.textContent || 'survey';
if (!qrEl) return;
let qrDataUrl;
if (qrEl.tagName === 'CANVAS') {
qrDataUrl = qrEl.toDataURL('image/png');
} else {
qrDataUrl = qrEl.src;
}
const exportCanvas = document.createElement('canvas');
const qrSize = 280;
const padding = 40;
const titleHeight = 60;
const footerHeight = 40;
exportCanvas.width = qrSize + padding * 2;
exportCanvas.height = qrSize + padding * 2 + titleHeight + footerHeight;
const ctx = exportCanvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
ctx.fillStyle = '#1a1a2e';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
const maxTitleWidth = exportCanvas.width - 40;
let displayTitle = title;
if (ctx.measureText(displayTitle).width > maxTitleWidth) {
while (ctx.measureText(displayTitle + '...').width > maxTitleWidth && displayTitle.length > 0) {
displayTitle = displayTitle.slice(0, -1);
}
displayTitle += '...';
}
ctx.fillText(displayTitle, exportCanvas.width / 2, padding + 20);
const img = new Image();
img.onload = function() {
ctx.drawImage(img, padding, padding + titleHeight, qrSize, qrSize);
ctx.fillStyle = '#6b7280';
ctx.font = '12px sans-serif';
ctx.fillText('สแกนเพื่อตอบแบบสอบถาม — TPU Survey', exportCanvas.width / 2, exportCanvas.height - 16);
const link = document.createElement('a');
const safeName = title.replace(/[^a-zA-Z0-9ก-๙]/g, '_').substring(0, 50);
link.download = `QR_${safeName}.png`;
link.href = exportCanvas.toDataURL('image/png');
link.click();
Toast.success('ดาวน์โหลด QR Code สำเร็จ');
};
img.src = qrDataUrl;
}
function teacherPrintQR() {
const qrEl = getTeacherQRImage();
const title = document.getElementById('qrSurveyTitle')?.textContent || 'แบบสอบถาม';
const linkText = document.getElementById('surveyLinkText')?.textContent || '';
if (!qrEl) return;
let qrDataUrl;
if (qrEl.tagName === 'CANVAS') {
qrDataUrl = qrEl.toDataURL('image/png');
} else {
qrDataUrl = qrEl.src;
}
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html><html><head><title>พิมพ์ QR Code</title>
<style>
body { font-family: 'Sarabun', sans-serif; text-align: center; padding: 40px; }
h2 { color: #1a1a2e; margin-bottom: 8px; }
.subtitle { color: #6b7280; font-size: 14px; margin-bottom: 24px; }
img { border: 2px solid #e5e7eb; border-radius: 12px; padding: 8px; }
.link { color: #6b7280; font-size: 11px; word-break: break-all; margin-top: 16px; }
.footer { margin-top: 32px; color: #9ca3af; font-size: 12px; }
</style></head>
<body>
<h2>${title}</h2>
<p class="subtitle">สแกน QR Code เพื่อตอบแบบสอบถาม</p>
<img src="${qrDataUrl}" width="300" height="300">
<p class="link">${linkText}</p>
<p class="footer">TPU Survey — มหาวิทยาลัยตาปี</p>
<script>window.onload = function() { window.print(); }<\/script>
</body></html>
`);
printWindow.document.close();
}
function teacherViewStats(surveyId) {
window.open(`stats.html?survey_id=${surveyId}`, '_blank');
}
// ============================================================
// Profile Management
// ============================================================
async function loadProfile() {
try {
const data = await API.get('profile.php?action=get');
const u = data.user;
// Fill the form
document.getElementById('profileCode').value = u.personnel_code || u.username || '-';
document.getElementById('profileName').value = u.full_name || '';
document.getElementById('profileEmail').value = u.email || '';
document.getElementById('profileSubject').value = u.subject_area || '';
document.getElementById('profileBranch').value = u.branch_code || '';
document.getElementById('profilePassword').value = '';
document.getElementById('profilePasswordConfirm').value = '';
// Fill the sidebar card
document.getElementById('profileDisplayName').textContent = u.full_name || '-';
document.getElementById('profileDisplayRole').textContent = u.role === 'admin' ? 'ผู้ดูแลระบบ' : 'อาจารย์ / เจ้าของโครงการ';
document.getElementById('profileAvatar').textContent = (u.full_name || 'T').charAt(0).toUpperCase();
document.getElementById('profilePersonnelCode').textContent = u.personnel_code || u.username || '-';
const badge = document.getElementById('profileBadge');
if (badge) {
badge.textContent = u.role === 'admin' ? '👑 ผู้ดูแลระบบ' : '🎓 อาจารย์';
badge.className = `user-card-badge ${u.role}`;
}
} catch (err) {
Toast.error(err.message);
}
}
async function saveProfile() {
const fullName = document.getElementById('profileName').value.trim();
const email = document.getElementById('profileEmail').value.trim();
const subjectArea = document.getElementById('profileSubject').value.trim();
const branchCode = document.getElementById('profileBranch').value.trim();
const password = document.getElementById('profilePassword').value;
const passwordConfirm = document.getElementById('profilePasswordConfirm').value;
if (!fullName) {
Toast.warning('กรุณากรอกชื่อ-นามสกุล');
return;
}
if (password && password.length < 4) {
Toast.warning('รหัสผ่านต้องมีอย่างน้อย 4 ตัวอักษร');
return;
}
if (password && password !== passwordConfirm) {
Toast.warning('รหัสผ่านไม่ตรงกัน');
return;
}
const payload = {
full_name: fullName,
email: email,
subject_area: subjectArea,
branch_code: branchCode
};
if (password) {
payload.password = password;
}
try {
await API.post('profile.php?action=update', payload);
Toast.success('บันทึกข้อมูลส่วนตัวสำเร็จ');
// Update sidebar display
document.getElementById('userName').textContent = fullName;
document.getElementById('userAvatar').textContent = fullName.charAt(0).toUpperCase();
// Reload profile card
loadProfile();
} catch (err) {
Toast.error(err.message);
}
}
// ============================================================
// Google Forms Import Modals & Functions
// ============================================================
function showImportGoogleFormModal() {
const html = `
<div style="margin-bottom:16px;">
<div style="display:flex;gap:12px;margin-bottom:16px;border-bottom:1px solid #e5e7eb;padding-bottom:10px;">
<button id="tabUrlBtn" type="button" class="btn btn-sm btn-primary" onclick="switchImportTab('url')">🌐 วาง URL / Link</button>
<button id="tabJsonBtn" type="button" class="btn btn-sm btn-secondary" onclick="switchImportTab('json')">📄 อัปโหลดไฟล์ JSON</button>
</div>
<!-- Tab 1: URL -->
<div id="importTabUrl">
<div class="form-group">
<label class="form-label">Google Form URL / Link *</label>
<input type="url" class="form-input" id="googleFormUrl" placeholder="https://docs.google.com/forms/d/e/1FAIpQLS.../viewform">
<small class="text-muted mt-8" style="display:block;">วางลิงก์แบบสอบถาม Google Forms ที่เปิดสาธารณะ (Public)</small>
</div>
</div>
<!-- Tab 2: JSON -->
<div id="importTabJson" style="display:none;">
<div class="form-group">
<label class="form-label">เลือกไฟล์ JSON โครงสร้างฟอร์ม *</label>
<input type="file" class="form-input" id="googleFormJsonFile" accept=".json">
</div>
<div class="form-group">
<label class="form-label">หรือวางข้อมูล JSON ตรงนี้</label>
<textarea class="form-input" id="googleFormJsonText" rows="4" placeholder='{"title": "แบบสอบถาม...", "sections": [...]}'></textarea>
</div>
</div>
</div>
`;
const footer = `
<button class="btn btn-secondary" onclick="Modal.hide('importGFormModal');document.getElementById('importGFormModal')?.remove();">ยกเลิก</button>
<button class="btn btn-primary" id="btnSubmitImportGForm" onclick="submitImportGoogleForm()">📥 นำเข้าแบบสอบถาม</button>
`;
const modalId = Modal.create('📥 นำเข้าแบบสอบถามจาก Google Forms', html, footer);
const modalEl = document.getElementById(modalId);
if (modalEl) modalEl.id = 'importGFormModal';
}
function switchImportTab(tab) {
const urlTab = document.getElementById('importTabUrl');
const jsonTab = document.getElementById('importTabJson');
const urlBtn = document.getElementById('tabUrlBtn');
const jsonBtn = document.getElementById('tabJsonBtn');
if (tab === 'url') {
if (urlTab) urlTab.style.display = 'block';
if (jsonTab) jsonTab.style.display = 'none';
if (urlBtn) urlBtn.className = 'btn btn-sm btn-primary';
if (jsonBtn) jsonBtn.className = 'btn btn-sm btn-secondary';
} else {
if (urlTab) urlTab.style.display = 'none';
if (jsonTab) jsonTab.style.display = 'block';
if (urlBtn) urlBtn.className = 'btn btn-sm btn-secondary';
if (jsonBtn) jsonBtn.className = 'btn btn-sm btn-primary';
}
}
async function submitImportGoogleForm() {
const urlTab = document.getElementById('importTabUrl');
const btn = document.getElementById('btnSubmitImportGForm');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ กำลังนำเข้าข้อมูล...';
}
try {
if (urlTab && urlTab.style.display !== 'none') {
const url = document.getElementById('googleFormUrl')?.value?.trim();
if (!url) {
Toast.warning('กรุณาระบุ Google Form URL');
if (btn) { btn.disabled = false; btn.textContent = '📥 นำเข้าแบบสอบถาม'; }
return;
}
const res = await API.post('import_google_form.php?action=importFromUrl', { url });
Toast.success(res.message || 'นำเข้าแบบสอบถามสำเร็จ!');
Modal.hide('importGFormModal');
document.getElementById('importGFormModal')?.remove();
if (typeof loadSurveys === 'function') loadSurveys();
if (typeof loadTeacherSurveys === 'function') loadTeacherSurveys();
} else {
const fileInput = document.getElementById('googleFormJsonFile');
const jsonText = document.getElementById('googleFormJsonText')?.value?.trim();
let surveyData = null;
if (fileInput && fileInput.files.length > 0) {
const text = await fileInput.files[0].text();
surveyData = JSON.parse(text);
} else if (jsonText) {
surveyData = JSON.parse(jsonText);
}
if (!surveyData) {
Toast.warning('กรุณาเลือกไฟล์ JSON หรือระบุข้อความ JSON');
if (btn) { btn.disabled = false; btn.textContent = '📥 นำเข้าแบบสอบถาม'; }
return;
}
const res = await API.post('import_google_form.php?action=importFromJson', { survey_data: surveyData });
Toast.success(res.message || 'นำเข้าแบบสอบถามสำเร็จ!');
Modal.hide('importGFormModal');
document.getElementById('importGFormModal')?.remove();
if (typeof loadSurveys === 'function') loadSurveys();
if (typeof loadTeacherSurveys === 'function') loadTeacherSurveys();
}
} catch (err) {
Toast.error(err.message || 'เกิดข้อผิดพลาดในการนำเข้าแบบสอบถาม');
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = '📥 นำเข้าแบบสอบถาม';
}
}
}
function showImportResponsesModal(surveyId, surveyTitle) {
const html = `
<form id="importRespForm">
<input type="hidden" name="survey_id" value="${surveyId}">
<div class="form-group">
<label class="form-label">แบบสอบถาม:</label>
<div style="font-weight:600;font-size:15px;color:var(--navy-600);">${escapeHtml(surveyTitle)}</div>
</div>
<div class="form-group">
<label class="form-label">เลือกไฟล์ CSV ข้อมูลคำตอบจาก Google Forms *</label>
<input type="file" class="form-input" name="csv_file" accept=".csv" required>
<small class="text-muted mt-8" style="display:block;">ไฟล์ .csv ที่ Export มาจาก Google Sheets / Google Forms (แถวแรกเป็นชื่อคำถาม)</small>
</div>
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="Modal.hide('importRespModal');document.getElementById('importRespModal')?.remove();">ยกเลิก</button>
<button class="btn btn-primary" id="btnSubmitImportResp" onclick="submitImportResponses(${surveyId})">📥 นำเข้าคำตอบลงระบบ</button>
`;
const modalId = Modal.create('📥 นำเข้าคำตอบจาก Google Forms (CSV)', html, footer);
const modalEl = document.getElementById(modalId);
if (modalEl) modalEl.id = 'importRespModal';
}
async function submitImportResponses(surveyId) {
const form = document.getElementById('importRespForm');
const fileInput = form?.querySelector('input[name="csv_file"]');
if (!fileInput || fileInput.files.length === 0) {
Toast.warning('กรุณาเลือกไฟล์ CSV คำตอบ');
return;
}
const btn = document.getElementById('btnSubmitImportResp');
if (btn) {
btn.disabled = true;
btn.textContent = '⏳ กำลังนำเข้าคำตอบ...';
}
const fd = new FormData(form);
try {
const res = await API.upload('import_responses.php', fd);
Toast.success(res.message || 'นำเข้าคำตอบสำเร็จ!');
Modal.hide('importRespModal');
document.getElementById('importRespModal')?.remove();
if (typeof loadSurveys === 'function') loadSurveys();
if (typeof loadTeacherSurveys === 'function') loadTeacherSurveys();
} catch (err) {
Toast.error(err.message || 'เกิดข้อผิดพลาดในการนำเข้าคำตอบ');
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = '📥 นำเข้าคำตอบลงระบบ';
}
}
}
// Global Window bindings
window.showImportGoogleFormModal = showImportGoogleFormModal;
window.switchImportTab = switchImportTab;
window.submitImportGoogleForm = submitImportGoogleForm;
window.showImportResponsesModal = showImportResponsesModal;
window.submitImportResponses = submitImportResponses;
document.addEventListener('DOMContentLoaded', initTeacher);