Initial commit - TPU Survey System with Google Forms & Certificate Generator
This commit is contained in:
+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);
|
||||
Reference in New Issue
Block a user