Initial commit - TPU Survey System with Google Forms & Certificate Generator
This commit is contained in:
+1266
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+299
@@ -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
@@ -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
@@ -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);
|
||||
Reference in New Issue
Block a user