Initial commit - TPU Survey System with Google Forms & Certificate Generator

This commit is contained in:
pongpon pitisuk
2026-09-12 11:56:34 +07:00
commit 72eb7cd0c8
355 changed files with 117012 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
/**
* TPU Survey - Standalone Statistics Logic
*/
function renderStatsQuestions(questions) {
let html = '';
const ratingQuestions = questions.filter(q => q.question_type === 'radio' && q.mean !== undefined);
const categoricalQuestions = questions.filter(q =>
(q.question_type === 'dropdown' || q.question_type === 'checkbox' || (q.question_type === 'radio' && q.mean === undefined))
&& q.frequencies && q.frequencies.length > 0
);
if (categoricalQuestions.length > 0) {
categoricalQuestions.forEach(q => {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `
<div class="categorical-stat mb-24" style="background:var(--gray-50); padding:20px; border-radius:var(--radius-md); border:1px solid var(--border-color);">
<p class="fw-800 mb-12" style="color:var(--primary-dark); font-size:18px;">${escapeHtml(cleanText)}</p>
<div class="table-container">
<table style="width:100%; border-collapse:collapse; background:white;">
<thead>
<tr>
<th style="text-align:left; background:var(--bg-secondary);">ตัวเลือก</th>
<th style="width:120px; text-align:center; background:var(--bg-secondary);">จำนวน</th>
<th style="width:120px; text-align:center; background:var(--bg-secondary);">ร้อยละ</th>
</tr>
</thead>
<tbody>
`;
q.frequencies.forEach(f => {
html += `
<tr>
<td style="text-align:left; font-weight:600;">${escapeHtml(String(f.value))}</td>
<td style="text-align:center;">${f.count}</td>
<td style="text-align:center;">${f.percentage}%</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
</div>
`;
});
}
if (ratingQuestions.length > 0) {
html += `<div class="table-container"><table>`;
html += `<thead><tr><th style="text-align:left">รายการประเมิน</th><th>5</th><th>4</th><th>3</th><th>2</th><th>1</th><th>X̄</th><th>S.D.</th><th>แปลผล</th></tr></thead>`;
html += `<tbody>`;
let totalMean = 0, totalSD = 0, count = 0;
ratingQuestions.forEach(q => {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `<tr style="font-size:18px;">`;
html += `<td style="text-align:left; font-weight:700; color:var(--text-main);">${escapeHtml(cleanText)}</td>`;
for (let i = 4; i >= 0; i--) {
const freq = q.frequencies?.[i] || { count: 0 };
html += `<td>${freq.count}</td>`;
}
html += `<td class="fw-700" style="color:var(--primary-color);">${q.mean?.toFixed(2) || '-'}</td>`;
html += `<td>${q.sd?.toFixed(2) || '-'}</td>`;
html += `<td><span class="badge badge-active">${q.interpretation || '-'}</span></td>`;
html += `</tr>`;
totalMean += q.mean || 0;
totalSD += q.sd || 0;
count++;
});
if (count > 0) {
const avgMean = totalMean / count;
const avgSD = totalSD / count;
let avgInterp = '';
if (avgMean >= 4.51) avgInterp = 'มากที่สุด';
else if (avgMean >= 3.51) avgInterp = 'มาก';
else if (avgMean >= 2.51) avgInterp = 'ปานกลาง';
else if (avgMean >= 1.51) avgInterp = 'น้อย';
else avgInterp = 'น้อยที่สุด';
html += `<tr style="font-weight:800;background:var(--gray-50); color:var(--primary-dark);">`;
html += `<td style="text-align:left">รวมเฉลี่ย</td><td colspan="5"></td>`;
html += `<td>${avgMean.toFixed(2)}</td><td>${avgSD.toFixed(2)}</td>`;
html += `<td><span class="badge badge-active">${avgInterp}</span></td></tr>`;
}
html += `</tbody></table></div>`;
}
// Text responses
questions.filter(q => q.question_type === 'text').forEach(q => {
const cleanText = q.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `<div class="mt-32">`;
html += `<p class="fw-800 mb-12" style="color:var(--primary-dark); border-left:4px solid var(--primary-color); padding-left:12px; font-size:20px;">${escapeHtml(cleanText)}</p>`;
if (q.text_responses && q.text_responses.length > 0) {
q.text_responses.forEach((t, i) => {
html += `<div style="padding:12px; background:var(--bg-secondary); border-radius:var(--radius-md); margin-bottom:8px; font-size:14px; border-left:2px solid var(--border-color);">${i+1}. ${escapeHtml(t)}</div>`;
});
} else {
html += `<p class="text-muted text-sm italic" style="padding-left:16px;">(ไม่มีข้อคิดเห็น)</p>`;
}
html += `</div>`;
});
return html;
}
async function initStats() {
const urlParams = new URLSearchParams(window.location.search);
const surveyId = urlParams.get('survey_id');
if (!surveyId) {
alert('ไม่ระบุ ID แบบสอบถาม');
window.close();
return;
}
const statsEl = document.getElementById('statsMainContent');
const actionsEl = document.getElementById('statsActions');
const titleEl = document.getElementById('statsSurveyTitle');
try {
const [summary, detail, resListData] = await Promise.all([
API.get(`statistics.php?action=summary&survey_id=${surveyId}`),
API.get(`statistics.php?action=detail&survey_id=${surveyId}`),
API.get(`responses.php?action=list&survey_id=${surveyId}`)
]);
titleEl.textContent = `ผลสถิติ: ${summary.survey.title}`;
document.title = `ผลสถิติ: ${summary.survey.title} - TPU Survey`;
actionsEl.innerHTML = `
<button class="btn btn-success btn-sm" onclick="window.open('api/export.php?action=word&survey_id=${surveyId}','_blank')">📄 Export Word</button>
<button class="btn btn-primary btn-sm" onclick="window.open('api/export.php?action=pdf&survey_id=${surveyId}','_blank')">📑 Export PDF</button>
`;
let html = `
<div class="stat-grid mb-32">
<div class="stat-card">
<div class="stat-icon purple">📊</div>
<div class="stat-content">
<h4 style="font-size:0.9rem; font-weight:700;">ค่าเฉลี่ยรวม (X̄)</h4>
<div class="stat-value" style="font-size:36px; font-weight:800;">${summary.overall_mean.toFixed(2)}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon teal">📉</div>
<div class="stat-content">
<h4 style="font-size:0.9rem; font-weight:700;">ส่วนเบี่ยงเบนมาตรฐาน (S.D.)</h4>
<div class="stat-value" style="font-size:36px; font-weight:800;">${summary.overall_sd.toFixed(2)}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">👥</div>
<div class="stat-content">
<h4 style="font-size:0.9rem; font-weight:700;">จำนวนผู้ตอบ</h4>
<div class="stat-value" style="font-size:36px; font-weight:800;">${summary.total_responses}</div>
</div>
</div>
</div>
`;
(detail.sections || []).forEach((section, index) => {
html += `<div class="card mb-32" style="border:none; box-shadow:var(--shadow-md); border-radius:var(--radius-xl); overflow:hidden;">`;
html += `<div class="card-header" style="background:var(--bg-secondary); padding:24px 32px;">
<h3 style="color:var(--primary-dark); font-weight:800; font-size:24px;">${escapeHtml(section.title)}</h3>`;
if (index === 0) {
html += `
<div style="margin-top: 12px; padding: 12px 20px; background: rgba(255,255,255,0.5); border-radius: 12px; border: 1px solid var(--border-color); display: inline-block;">
<span style="font-weight: 700; color: var(--text-main); font-size: 1.1rem;">📊 จำนวนยอดการตอบแบบสอบถามทั้งหมด:</span>
<span style="font-size: 1.5rem; font-weight: 800; color: var(--primary-color); margin-left: 8px;">${detail.total_responses}</span>
<span style="font-weight: 700; color: var(--text-main); margin-left: 4px;">ชุด</span>
</div>
`;
}
html += `</div>`;
html += `<div class="card-body" style="padding:32px;">`;
// Collect all questions for this section (from headings + loose)
const allQuestions = [];
// Render headings
(section.headings || []).forEach(heading => {
if (heading.questions && heading.questions.length > 0) {
// Add heading sub-header
html += `
<div style="margin:24px 0 16px; padding:12px 16px; background:linear-gradient(135deg, rgba(37,99,235,0.06), rgba(37,99,235,0.10)); border-left:4px solid var(--primary-color); border-radius:0 8px 8px 0;">
<h4 style="margin:0; font-size:1.05rem; font-weight:700; color:var(--primary-dark);">📑 ${escapeHtml(heading.title)}</h4>
${heading.description ? `<p style="margin:4px 0 0; font-size:0.85rem; color:var(--text-muted);">${escapeHtml(heading.description)}</p>` : ''}
</div>
`;
html += renderStatsQuestions(heading.questions);
heading.questions.forEach(q => allQuestions.push(q));
}
});
// Render loose questions
const looseQuestions = section.questions || [];
if (looseQuestions.length > 0) {
if ((section.headings || []).length > 0) {
html += `<div style="margin:24px 0 16px; padding-bottom:8px; border-bottom:1px dashed var(--border-color);"><span style="font-size:13px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;">📋 คำถามทั่วไป</span></div>`;
}
html += renderStatsQuestions(looseQuestions);
looseQuestions.forEach(q => allQuestions.push(q));
}
html += `</div></div>`;
});
// --- ส่วนที่ 4: รายการคำตอบทั้งหมด (Individual Responses) ---
const responses = resListData.responses || [];
html += `
<div class="card mb-32" style="border:none; box-shadow:var(--shadow-md); border-radius:var(--radius-xl); overflow:hidden;">
<div class="card-header" style="background:var(--bg-secondary); padding:24px 32px;">
<h3 style="color:var(--primary-dark); font-weight:800; font-size:24px;">📝 รายการคำตอบทั้งหมด (${responses.length} ชุด)</h3>
</div>
<div class="card-body" style="padding:0;">
<div class="table-container">
<table style="width:100%; border-collapse:collapse;">
<thead>
<tr style="background:var(--bg-secondary);">
<th style="padding:16px 24px; text-align:center;">#</th>
<th style="padding:16px 24px; text-align:left;">วัน-เวลาที่ส่ง</th>
<th style="padding:16px 24px; text-align:left;">IP Address</th>
<th style="padding:16px 24px; text-align:center;">จัดการ</th>
</tr>
</thead>
<tbody>
`;
if (responses.length === 0) {
html += `<tr><td colspan="4" style="padding:40px; text-align:center; color:var(--text-muted);">ยังไม่มีผู้ตอบแบบสอบถาม</td></tr>`;
} else {
responses.forEach((r, idx) => {
html += `
<tr style="border-bottom:1px solid var(--border-color);">
<td style="padding:16px 24px; text-align:center; font-weight:600;">${responses.length - idx}</td>
<td style="padding:16px 24px;">${new Date(r.submitted_at).toLocaleString('th-TH')}</td>
<td style="padding:16px 24px;">${r.ip_address || '-'}</td>
<td style="padding:16px 24px; text-align:center;">
<button class="btn btn-ghost btn-sm" onclick="viewResponseDetail(${r.id})">🔍 ดูคำตอบ</button>
</td>
</tr>
`;
});
}
html += `
</tbody>
</table>
</div>
</div>
</div>
`;
statsEl.innerHTML = html;
} catch (err) {
statsEl.innerHTML = `<div class="empty-state"><div class="empty-icon">❌</div><h3>ไม่สามารถโหลดข้อมูลได้</h3><p>${err.message}</p><button class="btn btn-primary mt-16" onclick="window.close()">ปิดหน้านี้</button></div>`;
}
}
async function viewResponseDetail(responseId) {
try {
const data = await API.get(`responses.php?action=get&id=${responseId}`);
const r = data.response;
let html = `
<div style="margin-bottom:20px; padding:15px; background:var(--bg-secondary); border-radius:12px;">
<p><strong>วันเวลาที่ส่ง:</strong> ${new Date(r.submitted_at).toLocaleString('th-TH')}</p>
<p><strong>IP Address:</strong> ${r.ip_address}</p>
</div>
<div class="response-detail-list">
`;
r.answers.forEach((ans, idx) => {
const cleanText = ans.question_text.replace(/^\s*[\d๑-๙]+[\.\)\s-:]*\s*/, '');
html += `
<div style="margin-bottom:16px; padding-bottom:16px; border-bottom:1px solid var(--border-color);">
<div style="font-weight:700; color:var(--primary-dark); margin-bottom:4px;">${escapeHtml(cleanText)}</div>
<div style="padding-left:20px; color:var(--text-main);">
<span style="font-weight:600; color:var(--primary-color);">คำตอบ: </span>
<span>${escapeHtml(ans.answer_value)}</span>
</div>
</div>
`;
});
html += `</div>`;
Modal.create(`รายละเอียดคำตอบ #${r.id}`, html, '', 'modal-lg');
} catch (err) {
alert('ไม่สามารถโหลดรายละเอียดได้: ' + err.message);
}
}
document.addEventListener('DOMContentLoaded', initStats);