/** * TPU Survey - Admin Dashboard Logic (Redesigned) * Card-based UI, search, filters, charts, animated counters */ let currentView = 'dashboard'; let surveysList = []; let usersList = []; let currentSurveyFilter = 'all'; // ============================================================ // Initialization // ============================================================ async function initAdmin() { const loggedIn = await Auth.check(); if (!loggedIn || !Auth.isAdmin()) { window.location.href = 'index.html'; return; } // Set user info in sidebar const name = Auth.user.full_name || 'ผู้ดูแลระบบ'; document.getElementById('welcomeName').textContent = name; document.getElementById('userName').textContent = name; document.getElementById('userAvatar').textContent = name.charAt(0).toUpperCase(); // Set current date setWelcomeDate(); showView('dashboard'); } function setWelcomeDate() { const el = document.getElementById('welcomeDate'); if (!el) return; const now = new Date(); const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; el.textContent = '📅 ' + now.toLocaleDateString('th-TH', options); } // ============================================================ // Navigation // ============================================================ function showView(view) { currentView = 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 = { dashboard: 'ภาพรวมระบบ', users: 'จัดการผู้ใช้', surveys: 'จัดการแบบสอบถาม', profile: 'ข้อมูลส่วนตัว' }; document.getElementById('pageTitle').textContent = titles[view] || ''; if (view === 'dashboard') loadDashboardStats(); else if (view === 'users') loadUsers(); else if (view === 'surveys') loadSurveys(); else if (view === 'profile') loadProfile(); } // ============================================================ // Animated Counter // ============================================================ function animateCounter(el, target) { const duration = 800; const start = parseInt(el.textContent) || 0; const diff = target - start; if (diff === 0) { el.textContent = target; return; } const startTime = performance.now(); function step(currentTime) { const progress = Math.min((currentTime - startTime) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic el.textContent = Math.round(start + diff * eased); if (progress < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } // ============================================================ // Dashboard Stats // ============================================================ async function loadDashboardStats() { try { const [usersData, surveysData] = await Promise.all([ API.get('users.php?action=list'), API.get('surveys.php?action=list') ]); usersList = usersData.users || []; surveysList = surveysData.surveys || []; const totalUsers = usersList.length; const totalSurveys = surveysList.length; const activeSurveys = surveysList.filter(s => s.status === 'active').length; const draftSurveys = surveysList.filter(s => s.status === 'draft').length; const closedSurveys = surveysList.filter(s => s.status === 'closed').length; const totalResponses = surveysList.reduce((sum, s) => sum + (parseInt(s.response_count) || 0), 0); // Animate stat counters animateCounter(document.getElementById('statUsers'), totalUsers); animateCounter(document.getElementById('statSurveys'), totalSurveys); animateCounter(document.getElementById('statActive'), activeSurveys); animateCounter(document.getElementById('statResponses'), totalResponses); // Render status chart renderStatusChart(activeSurveys, draftSurveys, closedSurveys, totalSurveys); // Recent surveys list renderRecentSurveys(surveysList.slice(0, 5)); } catch (err) { Toast.error(err.message); } } function renderStatusChart(active, draft, closed, total) { const max = total || 1; const activeEl = document.getElementById('chartActive'); const draftEl = document.getElementById('chartDraft'); const closedEl = document.getElementById('chartClosed'); if (activeEl) { setTimeout(() => { activeEl.style.width = Math.max((active / max) * 100, active > 0 ? 8 : 0) + '%'; }, 100); } if (draftEl) { setTimeout(() => { draftEl.style.width = Math.max((draft / max) * 100, draft > 0 ? 8 : 0) + '%'; }, 200); } if (closedEl) { setTimeout(() => { closedEl.style.width = Math.max((closed / max) * 100, closed > 0 ? 8 : 0) + '%'; }, 300); } const activeCount = document.getElementById('chartActiveCount'); const draftCount = document.getElementById('chartDraftCount'); const closedCount = document.getElementById('chartClosedCount'); if (activeCount) activeCount.textContent = `${active} (${total ? Math.round(active / total * 100) : 0}%)`; if (draftCount) draftCount.textContent = `${draft} (${total ? Math.round(draft / total * 100) : 0}%)`; if (closedCount) closedCount.textContent = `${closed} (${total ? Math.round(closed / total * 100) : 0}%)`; } function renderRecentSurveys(surveys) { const list = document.getElementById('recentSurveysList'); if (!list) return; if (surveys.length === 0) { list.innerHTML = `
  • ยังไม่มีแบบสอบถาม
  • `; return; } const statusIcons = { active: '🟢', draft: '📝', closed: '🔴' }; const statusLabels = { active: 'เปิดใช้', draft: 'ร่าง', closed: 'ปิด' }; list.innerHTML = surveys.map(s => `
  • ${statusIcons[s.status] || '📄'}
    ${escapeHtml(s.title)} สร้างโดย ${escapeHtml(s.creator_name || '-')} · ${statusLabels[s.status] || s.status} · ${s.response_count || 0} คำตอบ
  • `).join(''); } // ============================================================ // User Management — Card View // ============================================================ async function loadUsers() { try { const data = await API.get('users.php?action=list'); usersList = data.users || []; renderUserCards(); } catch (err) { Toast.error(err.message); } } function renderUserCards(filteredList) { const container = document.getElementById('userCardsContainer'); const summary = document.getElementById('userSummary'); if (!container) return; const users = filteredList || usersList; if (users.length === 0) { container.innerHTML = `

    ไม่พบผู้ใช้ ลองค้นหาด้วยคำอื่น หรือเพิ่มผู้ใช้ใหม่

    `; if (summary) summary.textContent = ''; return; } const roleMap = { admin: { label: '👑 ผู้ดูแลระบบ', bg: 'bg-blue-100 text-blue-800 border-blue-300' }, teacher: { label: '🎓 อาจารย์ / เจ้าของโครงการ', bg: 'bg-amber-100 text-amber-800 border-amber-300' } }; container.innerHTML = users.map(u => { const roleInfo = roleMap[u.role] || { label: u.role, bg: 'bg-slate-100 text-slate-800 border-slate-300' }; const isCurrentUser = u.id == Auth.user.id; return `
    ${(u.full_name || 'U').charAt(0).toUpperCase()}
    ${roleInfo.label}

    ${escapeHtml(u.full_name)}

    รหัส: ${escapeHtml(u.personnel_code || u.username || '-')} ${u.email ? ` ${escapeHtml(u.email)}` : ''} ${u.subject_area ? ` ${escapeHtml(u.subject_area)}` : ''}
    ${!isCurrentUser ? ` ` : 'บัญชีคุณ'}
    `; }).join(''); // Summary const admins = users.filter(u => u.role === 'admin').length; const teachers = users.filter(u => u.role === 'teacher').length; if (summary) { summary.textContent = `รวม ${users.length} ผู้ใช้ — 👑 ผู้ดูแล ${admins} คน · 🎓 อาจารย์ ${teachers} คน`; } } function filterUsers() { const query = document.getElementById('userSearchInput')?.value?.toLowerCase() || ''; if (!query) { renderUserCards(); return; } const filtered = usersList.filter(u => (u.full_name || '').toLowerCase().includes(query) || (u.personnel_code || '').toLowerCase().includes(query) || (u.username || '').toLowerCase().includes(query) ); renderUserCards(filtered); } function showAddUserModal() { const html = `
    ใช้เป็นชื่อผู้ใช้และรหัสผ่านสำหรับเข้าสู่ระบบ
    `; const footer = ` `; Modal.create('เพิ่มผู้ใช้ใหม่', html, footer); document.querySelector('.modal-overlay:last-child').id = 'addUserModal'; } async function submitAddUser() { const form = document.getElementById('userForm'); const fd = new FormData(form); const data = Object.fromEntries(fd); if (!data.personnel_code || !data.full_name) { Toast.warning('กรุณากรอกรหัสบุคลากรและชื่อ-นามสกุล'); return; } // ใช้รหัสบุคลากรเป็น username และ password data.username = data.personnel_code; data.password = data.personnel_code; try { await API.post('users.php?action=create', data); Toast.success('สร้างผู้ใช้สำเร็จ (รหัสผ่าน = รหัสบุคลากร)'); Modal.hide('addUserModal'); document.getElementById('addUserModal').remove(); loadUsers(); } catch (err) { Toast.error(err.message); } } async function editUser(id) { try { const data = await API.get(`users.php?action=get&id=${id}`); const u = data.user; const html = `
    `; const footer = ` `; Modal.create('แก้ไขผู้ใช้', html, footer); document.querySelector('.modal-overlay:last-child').id = 'editUserModal'; } catch (err) { Toast.error(err.message); } } async function submitEditUser() { const form = document.getElementById('editUserForm'); const fd = new FormData(form); const data = Object.fromEntries(fd); try { await API.post('users.php?action=update', data); Toast.success('อัปเดตผู้ใช้สำเร็จ'); Modal.hide('editUserModal'); document.getElementById('editUserModal').remove(); loadUsers(); } catch (err) { Toast.error(err.message); } } async function deleteUser(id, name) { if (!confirm(`ต้องการลบผู้ใช้ "${name}" ใช่หรือไม่?`)) return; try { await API.post('users.php?action=delete', { id }); Toast.success('ลบผู้ใช้สำเร็จ'); loadUsers(); } catch (err) { Toast.error(err.message); } } // ============================================================ // Survey Management — Card View with Filter // ============================================================ async function loadSurveys() { try { const data = await API.get('surveys.php?action=list'); surveysList = data.surveys || []; updateSurveyFilterCounts(); renderSurveyCards(); } catch (err) { Toast.error(err.message); } } function updateSurveyFilterCounts() { const all = surveysList.length; const active = surveysList.filter(s => s.status === 'active').length; const draft = surveysList.filter(s => s.status === 'draft').length; const closed = surveysList.filter(s => s.status === 'closed').length; const countAll = document.getElementById('countAll'); const countActive = document.getElementById('countActive'); const countDraft = document.getElementById('countDraft'); const countClosed = document.getElementById('countClosed'); if (countAll) countAll.textContent = all; if (countActive) countActive.textContent = active; if (countDraft) countDraft.textContent = draft; if (countClosed) countClosed.textContent = closed; } function getFilteredSurveys() { let list = surveysList; if (currentSurveyFilter !== 'all') { list = list.filter(s => s.status === currentSurveyFilter); } const query = document.getElementById('surveySearchInput')?.value?.toLowerCase() || ''; if (query) { list = list.filter(s => (s.title || '').toLowerCase().includes(query) || (s.creator_name || '').toLowerCase().includes(query) ); } return list; } function renderSurveyCards() { const container = document.getElementById('surveyCardsContainer'); const summary = document.getElementById('surveySummary'); if (!container) return; const surveys = getFilteredSurveys(); if (surveys.length === 0) { container.innerHTML = `

    ไม่พบแบบสอบถาม ลองค้นหาด้วยคำอื่น หรือสร้างแบบสอบถามใหม่

    `; if (summary) summary.textContent = ''; 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 = surveys.map(s => { const st = statusMap[s.status] || { label: s.status || 'เปิดใช้งาน', bg: 'bg-emerald-100 text-emerald-800 border-emerald-300' }; return `
    ${st.label}

    ${escapeHtml(s.title)}

    ${escapeHtml(s.creator_name || '-')} ${s.response_count || 0} คำตอบ ${formatDate(s.created_at)}
    `; }).join(''); if (summary) { summary.textContent = `แสดง ${surveys.length} จาก ${surveysList.length} แบบสอบถาม`; } } function filterSurveys(filter, btn) { currentSurveyFilter = filter; document.querySelectorAll('#surveyFilterTabs .filter-tab').forEach(t => t.classList.remove('active')); if (btn) btn.classList.add('active'); renderSurveyCards(); } function filterSurveysBySearch() { renderSurveyCards(); } function showCreateSurveyModal() { const html = `

    ระบบจะสร้างแบบสอบถามพร้อม 3 ส่วนเริ่มต้น คุณสามารถแก้ไขคำถามได้ภายหลัง

    `; const footer = ` `; Modal.create('สร้างแบบสอบถามใหม่', html, footer); document.querySelector('.modal-overlay:last-child').id = 'createSurveyModal'; } async function submitCreateSurvey() { const form = document.getElementById('surveyForm'); const fd = new FormData(form); const data = Object.fromEntries(fd); if (!data.title) { Toast.warning('กรุณากรอกชื่อแบบสอบถาม'); return; } // Pre-fill 3 sections structure 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('createSurveyModal'); document.getElementById('createSurveyModal').remove(); loadSurveys(); setTimeout(() => editSurvey(result.id), 500); } catch (err) { Toast.error(err.message); } } function editSurvey(id) { window.open(`edit.html?id=${id}`, '_blank'); } function buildQuestionEditor(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 `
    ⋮⋮ คำถาม
    เว้นว่างไว้หากต้องการใช้ระดับ 1-5 (สำหรับ Radio)
    `; } function addSection() { const container = document.getElementById('additionalSections'); if (!container) return; const si = document.querySelectorAll('.card[data-section]').length; const html = `
    `; container.insertAdjacentHTML('beforeend', html); addQuestion(si); } 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'; } } function addQuestion(sectionIdx, afterEl = null) { const container = document.getElementById(`questions-${sectionIdx}`); if (!container) return; const tempDiv = document.createElement('div'); tempDiv.innerHTML = buildQuestionEditor(sectionIdx, 0); const newQ = tempDiv.firstElementChild; if (afterEl) { afterEl.after(newQ); } else { container.appendChild(newQ); } reindexQuestions(sectionIdx); } function removeQuestion(btn, sectionIdx) { if (!confirm('ยืนยันลบคำถามนี้?')) return; btn.closest('.question-builder').remove(); reindexQuestions(sectionIdx); } function reindexQuestions(sectionIdx) { const container = document.getElementById(`questions-${sectionIdx}`); if (!container) return; container.querySelectorAll('.question-builder').forEach((qEl, idx) => { const label = qEl.querySelector('.q-number-label'); if (label) label.textContent = `คำถาม`; }); } async function submitEditSurvey() { const form = document.getElementById('editSurveyForm'); 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 sTitle = sectionEl.querySelector(`[data-field="section-title-${si}"]`)?.value || `ส่วนที่ ${si+1}`; const sCategory = sectionEl.querySelector(`[data-field="section-category-${si}"]`)?.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('editSurveyModal'); document.getElementById('editSurveyModal').remove(); loadSurveys(); } catch (err) { Toast.error(err.message); } } async function duplicateSurvey(id) { if (!confirm('ยืนยันสร้างสำเนาแบบสอบถามนี้?')) return; try { await API.post('surveys.php?action=duplicate', { id }); Toast.success('คัดลอกสำเร็จ'); loadSurveys(); } catch (err) { Toast.error(err.message); } } async function changeStatus(id, status) { try { await API.post('surveys.php?action=updateStatus', { id, status }); Toast.success('อัปเดตสถานะสำเร็จ'); // Refresh the card view const survey = surveysList.find(s => s.id == id); if (survey) survey.status = status; updateSurveyFilterCounts(); renderSurveyCards(); } catch (err) { Toast.error(err.message); loadSurveys(); } } async function generateLink(id) { try { const data = await API.post('surveys.php?action=generateLink', { id }); const basePath = window.location.pathname.includes('/survey') ? '/survey' : ''; const fullUrl = data.full_url || `${window.location.origin}${basePath}/respond.php?link=${data.unique_link}`; // Find survey title const survey = surveysList.find(s => s.id == id); const surveyTitle = survey ? survey.title : 'แบบสอบถาม'; const html = ` `; 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 copyLink() { 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 getQRImage() { const container = document.getElementById('qrCodeContainer'); if (!container) return null; // QRCodeJS generates both canvas and img — prefer canvas for export return container.querySelector('canvas') || container.querySelector('img'); } function downloadQR() { const qrEl = getQRImage(); const title = document.getElementById('qrSurveyTitle')?.textContent || 'survey'; if (!qrEl) return; // Get data URL from canvas or img let qrDataUrl; if (qrEl.tagName === 'CANVAS') { qrDataUrl = qrEl.toDataURL('image/png'); } else { qrDataUrl = qrEl.src; } // Create a new canvas with title and styling 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); // Title 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); // Draw QR const img = new Image(); img.onload = function() { ctx.drawImage(img, padding, padding + titleHeight, qrSize, qrSize); // Footer text ctx.fillStyle = '#6b7280'; ctx.font = '12px sans-serif'; ctx.fillText('สแกนเพื่อตอบแบบสอบถาม — TPU Survey', exportCanvas.width / 2, exportCanvas.height - 16); // Download 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 printQR() { const qrEl = getQRImage(); 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(` พิมพ์ QR Code

    ${title}

    สแกน QR Code เพื่อตอบแบบสอบถาม