| <!DOCTYPE html> |
| <html lang="th"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ระบบค้นหาและวิเคราะห์สถิติการยื่นคำร้องเรียน</title> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet"> |
| |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script> |
| |
| <script src="https://unpkg.com/lucide@latest"></script> |
| |
| <style> |
| body { |
| font-family: 'Kanit', 'Sarabun', sans-serif; |
| background-color: #f8fafc; |
| } |
| .custom-scrollbar::-webkit-scrollbar { |
| width: 6px; |
| height: 6px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-track { |
| background: #f1f5f9; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb { |
| background: #cbd5e1; |
| border-radius: 4px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { |
| background: #94a3b8; |
| } |
| </style> |
| </head> |
| <body class="text-slate-800 flex flex-col min-h-screen"> |
|
|
| |
| <header class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white shadow-lg"> |
| <div class="max-w-7xl mx-auto px-4 py-5 sm:px-6 lg:px-8 flex flex-col sm:flex-row justify-between items-center gap-4"> |
| <div class="flex items-center gap-3"> |
| <div class="p-2.5 bg-white/10 rounded-xl backdrop-blur-sm"> |
| <i data-lucide="graduation-cap" class="w-8 h-8 text-amber-300"></i> |
| </div> |
| <div> |
| <h1 class="text-2xl font-bold tracking-wide">ระบบวิเคราะห์ข้อมูลคำร้องเรียน</h1> |
| <p class="text-xs text-blue-100 font-light">วิเคราะห์ ค้นหา และเปรียบเทียบสถิติการเรียนกรณีพิเศษ</p> |
| </div> |
| </div> |
| <div class="flex items-center gap-3"> |
| <span class="text-xs bg-emerald-500/20 text-emerald-300 px-3 py-1.5 rounded-full border border-emerald-500/30 flex items-center gap-1.5 font-medium"> |
| <span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> |
| พร้อมใช้งานออฟไลน์ |
| </span> |
| </div> |
| </div> |
| </header> |
|
|
| <main class="flex-grow max-w-7xl w-full mx-auto px-4 py-8 sm:px-6 lg:px-8"> |
| |
| |
| <div id="upload-section" class="mb-8"> |
| <div class="bg-white rounded-2xl border-2 border-dashed border-slate-300 hover:border-blue-500 transition-colors duration-300 p-8 text-center cursor-pointer shadow-sm relative group" id="drop-zone"> |
| <input type="file" id="file-input" accept=".csv" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"> |
| <div class="max-w-md mx-auto flex flex-col items-center"> |
| <div class="w-16 h-16 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300"> |
| <i data-lucide="file-up" class="w-8 h-8"></i> |
| </div> |
| <h3 class="text-lg font-semibold text-slate-700 mb-1">อัปโหลดไฟล์ข้อมูลคำร้อง (book.csv)</h3> |
| <p class="text-sm text-slate-500 mb-4">ลากไฟล์มาวางที่นี่ หรือคลิกเพื่อเลือกไฟล์จากคอมพิวเตอร์ของคุณ</p> |
| <div class="inline-flex items-center gap-2 bg-slate-100 px-4 py-2 rounded-lg text-xs text-slate-600 font-medium border border-slate-200"> |
| <i data-lucide="info" class="w-4 h-4 text-slate-400"></i> |
| รองรับรูปแบบไฟล์ตารางของมหาวิทยาลัยโดยเฉพาะ |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="dashboard-content" class="hidden space-y-8 animate-fade-in"> |
| |
| |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5"> |
| |
| <div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between"> |
| <div> |
| <p class="text-sm text-slate-500 font-medium">จำนวนคำร้องเรียนทั้งหมด</p> |
| <h4 id="stat-total-petitions" class="text-2xl font-bold text-slate-800 mt-1">-</h4> |
| </div> |
| <div class="p-3 bg-blue-50 text-blue-600 rounded-xl"> |
| <i data-lucide="file-text" class="w-6 h-6"></i> |
| </div> |
| </div> |
| |
| <div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between"> |
| <div> |
| <p class="text-sm text-slate-500 font-medium">จำนวนนักศึกษา (คน)</p> |
| <h4 id="stat-unique-students" class="text-2xl font-bold text-slate-800 mt-1">-</h4> |
| </div> |
| <div class="p-3 bg-indigo-50 text-indigo-600 rounded-xl"> |
| <i data-lucide="users" class="w-6 h-6"></i> |
| </div> |
| </div> |
| |
| <div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between"> |
| <div> |
| <p class="text-sm text-slate-500 font-medium">รวมหน่วยกิตเรียนกรณีพิเศษ</p> |
| <h4 id="stat-total-credits" class="text-2xl font-bold text-slate-800 mt-1">-</h4> |
| </div> |
| <div class="p-3 bg-amber-50 text-amber-600 rounded-xl"> |
| <i data-lucide="bookmark" class="w-6 h-6"></i> |
| </div> |
| </div> |
| |
| <div class="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm flex items-center justify-between"> |
| <div> |
| <p class="text-sm text-slate-500 font-medium">จำนวนเงินที่อนุมัติรวม</p> |
| <h4 id="stat-total-amount" class="text-2xl font-bold text-slate-800 mt-1">-</h4> |
| </div> |
| <div class="p-3 bg-emerald-50 text-emerald-600 rounded-xl"> |
| <i data-lucide="credit-card" class="w-6 h-6"></i> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| |
| <div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm lg:col-span-2 flex flex-col"> |
| <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-6"> |
| <div> |
| <h3 class="text-lg font-bold text-slate-800">วิเคราะห์เชิงเปรียบเทียบแต่ละสาขาวิชา</h3> |
| <p class="text-xs text-slate-500">เปรียบเทียบอัตราการยื่นคำร้องจำแนกตามสาขาวิชา/หลักสูตร</p> |
| </div> |
| <div class="flex gap-2"> |
| <select id="chart-metric" class="text-xs bg-slate-50 border border-slate-200 text-slate-700 px-3 py-1.5 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-medium"> |
| <option value="petitions">จำนวนคำร้อง (รายการ)</option> |
| <option value="credits">จำนวนหน่วยกิตสะสม</option> |
| <option value="amount">จำนวนเงินรวม (บาท)</option> |
| </select> |
| </div> |
| </div> |
| <div class="relative flex-grow min-h-[320px] max-h-[450px]"> |
| <canvas id="majorBarChart"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm flex flex-col"> |
| <div class="mb-6"> |
| <h3 class="text-lg font-bold text-slate-800">สัดส่วนคำร้องจำแนก</h3> |
| <p class="text-xs text-slate-500">สัดส่วนการยื่นคำร้องของสาขาวิชาหลัก</p> |
| </div> |
| <div class="relative flex-grow flex items-center justify-center min-h-[250px] max-h-[300px]"> |
| <canvas id="majorPieChart"></canvas> |
| </div> |
| <div class="mt-4 text-center"> |
| <p class="text-xs text-slate-400">แสดงผลเฉพาะ 5 อันดับแรกที่มีคำร้องสูงสุด</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-6"> |
| <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6"> |
| <div> |
| <h3 class="text-lg font-bold text-slate-800">ค้นหารายการคำร้องเรียน</h3> |
| <p class="text-xs text-slate-500">พิมพ์ค้นหาได้ทันทีด้วย ชื่อ-สกุล, รหัสนักศึกษา หรือชื่อสาขาวิชา</p> |
| </div> |
| <div class="flex flex-col sm:flex-row gap-3 w-full md:w-auto"> |
| |
| <div class="relative flex-grow sm:w-80"> |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400"> |
| <i data-lucide="search" class="w-4 h-4"></i> |
| </div> |
| <input type="text" id="search-input" placeholder="ค้นหารหัสนักศึกษา, ชื่อ, สาขาวิชา..." class="w-full pl-10 pr-4 py-2 bg-slate-55 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-slate-800 transition-all duration-150 placeholder:text-slate-400"> |
| </div> |
| |
| <button id="clear-search-btn" class="px-4 py-2 border border-slate-200 hover:bg-slate-50 text-slate-600 rounded-xl text-sm font-medium transition-colors duration-150 flex items-center justify-center gap-1"> |
| <i data-lucide="rotate-ccw" class="w-4 h-4"></i> |
| รีเซ็ต |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="overflow-x-auto custom-scrollbar border border-slate-100 rounded-xl"> |
| <table class="w-full text-left border-collapse"> |
| <thead> |
| <tr class="bg-slate-50/75 border-b border-slate-100 text-slate-600 text-xs font-semibold uppercase tracking-wider"> |
| <th class="py-3 px-4">รหัสนักศึกษา</th> |
| <th class="py-3 px-4">ชื่อนักศึกษา</th> |
| <th class="py-3 px-4">สาขาวิชา/คณะ</th> |
| <th class="py-3 px-4 text-center">จำนวนหน่วยกิต</th> |
| <th class="py-3 px-4 text-right">จำนวนเงิน</th> |
| <th class="py-3 px-4 text-center">วันที่ยื่น</th> |
| <th class="py-3 px-4 text-center">การจัดการ</th> |
| </tr> |
| </thead> |
| <tbody id="table-body" class="divide-y divide-slate-50 text-sm text-slate-700"> |
| |
| </tbody> |
| </table> |
| </div> |
|
|
| |
| <div id="no-results" class="hidden py-12 text-center"> |
| <div class="text-slate-400 mb-2"> |
| <i data-lucide="search-code" class="w-12 h-12 mx-auto"></i> |
| </div> |
| <h4 class="font-medium text-slate-600">ไม่พบข้อมูลที่ตรงกับเงื่อนไขการค้นหา</h4> |
| <p class="text-xs text-slate-400 mt-1">ลองเปลี่ยนคำค้นหาใหม่อีกครั้ง</p> |
| </div> |
|
|
| |
| <div class="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-slate-100"> |
| <div class="text-xs text-slate-500 font-medium"> |
| แสดงข้อมูลที่ <span id="page-start" class="text-slate-800 font-semibold">-</span> ถึง |
| <span id="page-end" class="text-slate-800 font-semibold">-</span> จากทั้งหมด |
| <span id="total-filtered-rows" class="text-slate-800 font-semibold">-</span> รายการ |
| </div> |
| <div class="flex items-center gap-1.5"> |
| <button id="btn-prev" class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none transition-colors duration-150"> |
| <i data-lucide="chevron-left" class="w-4 h-4"></i> |
| </button> |
| <div id="pagination-pages" class="flex gap-1"> |
| |
| </div> |
| <button id="btn-next" class="p-2 border border-slate-200 rounded-lg hover:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none transition-colors duration-150"> |
| <i data-lucide="chevron-right" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| </main> |
|
|
| |
| <div id="student-modal" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 z-50 hidden transition-opacity duration-300"> |
| <div class="bg-white rounded-2xl max-w-lg w-full overflow-hidden shadow-2xl border border-slate-100 flex flex-col max-h-[90vh]"> |
| |
| <div class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white p-6 relative"> |
| <button onclick="closeModal()" class="absolute top-4 right-4 text-white/80 hover:text-white hover:bg-white/10 p-1.5 rounded-lg transition-colors"> |
| <i data-lucide="x" class="w-5 h-5"></i> |
| </button> |
| <div class="flex items-center gap-3"> |
| <div class="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center text-xl font-bold"> |
| <i data-lucide="user" class="w-6 h-6 text-amber-300"></i> |
| </div> |
| <div> |
| <h3 id="modal-student-name" class="text-lg font-bold">ชื่อนักศึกษา</h3> |
| <p id="modal-student-id" class="text-xs text-blue-100 font-light">รหัสนักศึกษา</p> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="p-6 overflow-y-auto custom-scrollbar space-y-5"> |
| <div> |
| <h4 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">ข้อมูลการศึกษา</h4> |
| <div class="bg-slate-50 p-4 rounded-xl border border-slate-100 space-y-2"> |
| <div class="flex justify-between text-sm"> |
| <span class="text-slate-500">สาขาวิชา/คณะ:</span> |
| <span id="modal-student-major" class="font-medium text-slate-800">-</span> |
| </div> |
| </div> |
| </div> |
|
|
| <div> |
| <h4 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">ประวัติการยื่นคำร้องทั้งหมดของนักศึกษาท่านนี้</h4> |
| <div id="modal-petition-list" class="space-y-3"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="p-4 bg-slate-50 border-t border-slate-100 flex justify-end"> |
| <button onclick="closeModal()" class="px-5 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-xl text-sm font-medium transition-colors"> |
| ปิดหน้าต่าง |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <footer class="bg-slate-950 text-slate-500 border-t border-slate-900 py-6 mt-auto"> |
| <div class="max-w-7xl mx-auto px-4 text-center text-xs space-y-2"> |
| <p>ระบบแดชบอร์ดค้นหาและวิเคราะห์สถิติตามข้อมูลคำร้องเรียนร่วม/เรียนกรณีพิเศษ</p> |
| <p class="text-slate-600">วิเคราะห์ข้อมูลผ่านสคริปต์ความปลอดภัยสูงบนเบราว์เซอร์ของคุณ ไม่มีข้อมูลที่ถูกส่งออกไปนอกเครื่อง</p> |
| </div> |
| </footer> |
|
|
| |
| <script> |
| |
| let rawData = []; |
| let filteredData = []; |
| let currentPage = 1; |
| const rowsPerPage = 10; |
| |
| |
| let barChartInstance = null; |
| let pieChartInstance = null; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| lucide.createIcons(); |
| setupEventListeners(); |
| }); |
| |
| |
| function setupEventListeners() { |
| const fileInput = document.getElementById('file-input'); |
| const dropZone = document.getElementById('drop-zone'); |
| const searchInput = document.getElementById('search-input'); |
| const clearBtn = document.getElementById('clear-search-btn'); |
| const chartMetric = document.getElementById('chart-metric'); |
| |
| |
| fileInput.addEventListener('change', (e) => { |
| if (e.target.files.length > 0) { |
| processFile(e.target.files[0]); |
| } |
| }); |
| |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| dropZone.addEventListener(eventName, (e) => { |
| e.preventDefault(); |
| dropZone.classList.add('border-blue-500', 'bg-blue-50/50'); |
| }, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| dropZone.addEventListener(eventName, (e) => { |
| e.preventDefault(); |
| dropZone.classList.remove('border-blue-500', 'bg-blue-50/50'); |
| }, false); |
| }); |
| |
| |
| dropZone.addEventListener('drop', (e) => { |
| const dt = e.dataTransfer; |
| const files = dt.files; |
| if (files.length > 0) { |
| processFile(files[0]); |
| } |
| }); |
| |
| |
| searchInput.addEventListener('input', () => { |
| currentPage = 1; |
| filterAndRender(); |
| }); |
| |
| |
| clearBtn.addEventListener('click', () => { |
| searchInput.value = ''; |
| currentPage = 1; |
| filterAndRender(); |
| }); |
| |
| |
| chartMetric.addEventListener('change', () => { |
| renderCharts(); |
| }); |
| |
| |
| document.getElementById('btn-prev').addEventListener('click', () => { |
| if (currentPage > 1) { |
| currentPage--; |
| renderTable(); |
| } |
| }); |
| |
| document.getElementById('btn-next').addEventListener('click', () => { |
| const totalPages = Math.ceil(filteredData.length / rowsPerPage); |
| if (currentPage < totalPages) { |
| currentPage++; |
| renderTable(); |
| } |
| }); |
| } |
| |
| |
| function processFile(file) { |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| let text = e.target.result; |
| |
| |
| const lines = text.split(/\r?\n/); |
| if (lines[0] && lines[0].includes('ข้อมูลคำร้องเรียนร่วม/เรียนกรณีพิเศษ')) { |
| |
| lines.shift(); |
| text = lines.join('\n'); |
| } |
| |
| Papa.parse(text, { |
| header: true, |
| skipEmptyLines: true, |
| complete: function(results) { |
| const parsed = results.data; |
| if (parsed && parsed.length > 0) { |
| formatAndLoadData(parsed); |
| } else { |
| alert('ไม่พบข้อมูลคำร้องเรียนใดๆ ในไฟล์ กรุณาตรวจสอบรูปแบบอีกครั้ง'); |
| } |
| }, |
| error: function(err) { |
| alert('เกิดข้อผิดพลาดในการอ่านไฟล์ CSV: ' + err.message); |
| } |
| }); |
| }; |
| reader.readAsText(file, 'UTF-8'); |
| } |
| |
| |
| function formatAndLoadData(data) { |
| rawData = data.map(item => { |
| |
| return { |
| no: item['ลำดับ'] ? item['ลำดับ'].trim() : '', |
| regNo: item['เลขที่ใบลงทะเบียน'] ? item['เลขที่ใบลงทะเบียน'].trim() : '', |
| date: item['วันที่'] ? item['วันที่'].trim() : '', |
| credits: parseInt(item['จำนวนหน่วยกิต']) || 0, |
| amount: parseFloat(item['จำนวนเงิน']) || 0, |
| studentId: item['รหัสนักศึกษา'] ? item['รหัสนักศึกษา'].trim() : '', |
| studentName: item['ชื่อนักศึกษา'] ? item['ชื่อนักศึกษา'].trim() : 'ไม่ระบุชื่อ', |
| major: item['สาขาวิชา'] ? item['สาขาวิชา'].trim() : 'ไม่ระบุสาขา' |
| }; |
| }).filter(item => item.studentId !== ''); |
| |
| filteredData = [...rawData]; |
| currentPage = 1; |
| |
| |
| document.getElementById('upload-section').classList.add('hidden'); |
| document.getElementById('dashboard-content').classList.remove('hidden'); |
| |
| calculateQuickStats(); |
| filterAndRender(); |
| } |
| |
| |
| function calculateQuickStats() { |
| |
| const totalPetitions = rawData.length; |
| document.getElementById('stat-total-petitions').textContent = totalPetitions.toLocaleString('th-TH'); |
| |
| |
| const uniqueStudentsSet = new Set(rawData.map(item => item.studentId)); |
| document.getElementById('stat-unique-students').textContent = uniqueStudentsSet.size.toLocaleString('th-TH'); |
| |
| |
| const totalCredits = rawData.reduce((sum, item) => sum + item.credits, 0); |
| document.getElementById('stat-total-credits').textContent = totalCredits.toLocaleString('th-TH'); |
| |
| |
| const totalAmount = rawData.reduce((sum, item) => sum + item.amount, 0); |
| document.getElementById('stat-total-amount').textContent = totalAmount.toLocaleString('th-TH') + ' บาท'; |
| } |
| |
| |
| function filterAndRender() { |
| const query = document.getElementById('search-input').value.toLowerCase().trim(); |
| |
| if (query === '') { |
| filteredData = [...rawData]; |
| } else { |
| filteredData = rawData.filter(item => { |
| return item.studentName.toLowerCase().includes(query) || |
| item.studentId.toLowerCase().includes(query) || |
| item.major.toLowerCase().includes(query); |
| }); |
| } |
| |
| renderTable(); |
| renderCharts(); |
| } |
| |
| |
| function renderTable() { |
| const tableBody = document.getElementById('table-body'); |
| const noResults = document.getElementById('no-results'); |
| tableBody.innerHTML = ''; |
| |
| if (filteredData.length === 0) { |
| noResults.classList.remove('hidden'); |
| document.getElementById('page-start').textContent = '0'; |
| document.getElementById('page-end').textContent = '0'; |
| document.getElementById('total-filtered-rows').textContent = '0'; |
| updatePaginationControls(0); |
| return; |
| } |
| |
| noResults.classList.add('hidden'); |
| |
| const totalFiltered = filteredData.length; |
| const totalPages = Math.ceil(totalFiltered / rowsPerPage); |
| |
| |
| if (currentPage > totalPages) currentPage = totalPages; |
| if (currentPage < 1) currentPage = 1; |
| |
| const startIndex = (currentPage - 1) * rowsPerPage; |
| const endIndex = Math.min(startIndex + rowsPerPage, totalFiltered); |
| |
| const pageData = filteredData.slice(startIndex, endIndex); |
| |
| pageData.forEach(item => { |
| const tr = document.createElement('tr'); |
| tr.className = "hover:bg-slate-50/50 transition-colors duration-100"; |
| tr.innerHTML = ` |
| <td class="py-3 px-4 font-mono text-xs text-slate-500 font-semibold">${item.studentId}</td> |
| <td class="py-3 px-4 font-medium text-slate-800">${item.studentName}</td> |
| <td class="py-3 px-4 text-xs text-slate-600">${item.major}</td> |
| <td class="py-3 px-4 text-center text-sm font-semibold">${item.credits}</td> |
| <td class="py-3 px-4 text-right text-emerald-600 font-mono font-semibold">${item.amount.toLocaleString('th-TH')} ฿</td> |
| <td class="py-3 px-4 text-center text-xs font-mono text-slate-500">${item.date}</td> |
| <td class="py-3 px-4 text-center"> |
| <button onclick="openModal('${item.studentId}')" class="px-2.5 py-1 text-xs font-medium bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors inline-flex items-center gap-1"> |
| <i data-lucide="eye" class="w-3.5 h-3.5"></i> |
| ประวัติ |
| </button> |
| </td> |
| `; |
| tableBody.appendChild(tr); |
| }); |
| |
| |
| document.getElementById('page-start').textContent = (startIndex + 1).toLocaleString('th-TH'); |
| document.getElementById('page-end').textContent = endIndex.toLocaleString('th-TH'); |
| document.getElementById('total-filtered-rows').textContent = totalFiltered.toLocaleString('th-TH'); |
| |
| updatePaginationControls(totalPages); |
| lucide.createIcons(); |
| } |
| |
| |
| function updatePaginationControls(totalPages) { |
| const btnPrev = document.getElementById('btn-prev'); |
| const btnNext = document.getElementById('btn-next'); |
| const pagesContainer = document.getElementById('pagination-pages'); |
| |
| btnPrev.disabled = currentPage === 1 || totalPages === 0; |
| btnNext.disabled = currentPage === totalPages || totalPages === 0; |
| |
| pagesContainer.innerHTML = ''; |
| |
| if (totalPages <= 1) return; |
| |
| |
| const startPage = Math.max(1, currentPage - 1); |
| const endPage = Math.min(totalPages, currentPage + 1); |
| |
| for (let i = startPage; i <= endPage; i++) { |
| const btn = document.createElement('button'); |
| btn.className = `px-3 py-1 rounded-lg text-xs font-medium border transition-colors ${ |
| i === currentPage |
| ? 'bg-blue-600 text-white border-blue-600 shadow-sm' |
| : 'border-slate-200 hover:bg-slate-50 text-slate-600' |
| }`; |
| btn.textContent = i; |
| btn.onclick = () => { |
| currentPage = i; |
| renderTable(); |
| }; |
| pagesContainer.appendChild(btn); |
| } |
| } |
| |
| |
| function renderCharts() { |
| const metric = document.getElementById('chart-metric').value; |
| |
| |
| const majorGroups = {}; |
| filteredData.forEach(item => { |
| if (!majorGroups[item.major]) { |
| majorGroups[item.major] = { |
| name: item.major, |
| petitions: 0, |
| credits: 0, |
| amount: 0 |
| }; |
| } |
| majorGroups[item.major].petitions += 1; |
| majorGroups[item.major].credits += item.credits; |
| majorGroups[item.major].amount += item.amount; |
| }); |
| |
| |
| const groupedArray = Object.values(majorGroups); |
| groupedArray.sort((a, b) => b[metric] - a[metric]); |
| |
| |
| const topGrouped = groupedArray.slice(0, 10); |
| const labels = topGrouped.map(item => item.name); |
| const datasetValues = topGrouped.map(item => item[metric]); |
| |
| let metricLabel = 'จำนวนคำร้อง (รายการ)'; |
| let metricBgColor = 'rgba(59, 130, 246, 0.8)'; |
| let metricBorderColor = 'rgb(59, 130, 246)'; |
| |
| if (metric === 'credits') { |
| metricLabel = 'จำนวนหน่วยกิตสะสม'; |
| metricBgColor = 'rgba(245, 158, 11, 0.8)'; |
| metricBorderColor = 'rgb(245, 158, 11)'; |
| } else if (metric === 'amount') { |
| metricLabel = 'จำนวนเงินรวม (บาท)'; |
| metricBgColor = 'rgba(16, 185, 129, 0.8)'; |
| metricBorderColor = 'rgb(16, 185, 129)'; |
| } |
| |
| |
| const ctxBar = document.getElementById('majorBarChart').getContext('2d'); |
| if (barChartInstance) barChartInstance.destroy(); |
| |
| barChartInstance = new Chart(ctxBar, { |
| type: 'bar', |
| data: { |
| labels: labels.map(l => l.length > 25 ? l.substring(0, 25) + '...' : l), |
| datasets: [{ |
| label: metricLabel, |
| data: datasetValues, |
| backgroundColor: metricBgColor, |
| borderColor: metricBorderColor, |
| borderWidth: 1.5, |
| borderRadius: 8, |
| maxBarThickness: 32 |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| indexAxis: 'y', |
| plugins: { |
| legend: { |
| display: false |
| }, |
| tooltip: { |
| backgroundColor: '#0f172a', |
| titleFont: { family: 'Kanit', size: 13 }, |
| bodyFont: { family: 'Sarabun', size: 12 }, |
| callbacks: { |
| title: function(context) { |
| |
| return labels[context[0].dataIndex]; |
| }, |
| label: function(context) { |
| let value = context.parsed.x; |
| if (metric === 'amount') value = value.toLocaleString('th-TH') + ' บาท'; |
| else if (metric === 'credits') value = value.toLocaleString('th-TH') + ' หน่วยกิต'; |
| else value = value.toLocaleString('th-TH') + ' รายการ'; |
| return ` ${context.dataset.label}: ${value}`; |
| } |
| } |
| } |
| }, |
| scales: { |
| x: { |
| grid: { color: '#f1f5f9' }, |
| ticks: { font: { family: 'Kanit', size: 11 } } |
| }, |
| y: { |
| grid: { display: false }, |
| ticks: { font: { family: 'Kanit', size: 11 } } |
| } |
| } |
| } |
| }); |
| |
| |
| const ctxPie = document.getElementById('majorPieChart').getContext('2d'); |
| if (pieChartInstance) pieChartInstance.destroy(); |
| |
| |
| const pieGrouped = groupedArray.slice(0, 5); |
| const pieLabels = pieGrouped.map(item => item.name); |
| const pieValues = pieGrouped.map(item => item[metric]); |
| |
| |
| const totalOther = groupedArray.slice(5).reduce((sum, item) => sum + item[metric], 0); |
| if (totalOther > 0) { |
| pieLabels.push('สาขาอื่นๆ'); |
| pieValues.push(totalOther); |
| } |
| |
| pieChartInstance = new Chart(ctxPie, { |
| type: 'doughnut', |
| data: { |
| labels: pieLabels.map(l => l.length > 18 ? l.substring(0, 18) + '...' : l), |
| datasets: [{ |
| data: pieValues, |
| backgroundColor: [ |
| 'rgba(59, 130, 246, 0.85)', |
| 'rgba(16, 185, 129, 0.85)', |
| 'rgba(245, 158, 11, 0.85)', |
| 'rgba(139, 92, 246, 0.85)', |
| 'rgba(236, 72, 153, 0.85)', |
| 'rgba(148, 163, 184, 0.85)' |
| ], |
| borderWidth: 2, |
| borderColor: '#ffffff' |
| }] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| plugins: { |
| legend: { |
| position: 'bottom', |
| labels: { |
| font: { family: 'Kanit', size: 10 }, |
| boxWidth: 12, |
| padding: 12 |
| } |
| }, |
| tooltip: { |
| backgroundColor: '#0f172a', |
| titleFont: { family: 'Kanit', size: 12 }, |
| bodyFont: { family: 'Sarabun', size: 12 }, |
| callbacks: { |
| title: function(context) { |
| return pieLabels[context[0].dataIndex]; |
| }, |
| label: function(context) { |
| let value = context.parsed; |
| if (metric === 'amount') value = value.toLocaleString('th-TH') + ' บาท'; |
| else if (metric === 'credits') value = value.toLocaleString('th-TH') + ' หน่วยกิต'; |
| else value = value.toLocaleString('th-TH') + ' รายการ'; |
| return ` ${value}`; |
| } |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| function openModal(studentId) { |
| const studentPetitions = rawData.filter(item => item.studentId === studentId); |
| if (studentPetitions.length === 0) return; |
| |
| const baseStudent = studentPetitions[0]; |
| |
| document.getElementById('modal-student-name').textContent = baseStudent.studentName; |
| document.getElementById('modal-student-id').textContent = `รหัสนักศึกษา: ${baseStudent.studentId}`; |
| document.getElementById('modal-student-major').textContent = baseStudent.major; |
| |
| const listContainer = document.getElementById('modal-petition-list'); |
| listContainer.innerHTML = ''; |
| |
| studentPetitions.forEach((p, idx) => { |
| const div = document.createElement('div'); |
| div.className = "p-3.5 bg-slate-50 border border-slate-100 rounded-xl space-y-1.5 hover:shadow-sm transition-shadow duration-150"; |
| div.innerHTML = ` |
| <div class="flex justify-between items-center"> |
| <span class="text-xs bg-indigo-50 text-indigo-600 px-2 py-0.5 rounded-md font-medium">คำร้องใบลงทะเบียนที่: ${p.regNo || '-'}</span> |
| <span class="text-xs text-slate-400 font-mono">${p.date}</span> |
| </div> |
| <div class="flex justify-between text-sm"> |
| <span class="text-slate-600">จำนวนหน่วยกิตที่ร้องขอ:</span> |
| <span class="font-semibold text-slate-800">${p.credits} หน่วยกิต</span> |
| </div> |
| <div class="flex justify-between text-sm"> |
| <span class="text-slate-600">ค่าธรรมเนียมอนุมัติ:</span> |
| <span class="font-bold text-emerald-600">${p.amount.toLocaleString('th-TH')} ฿</span> |
| </div> |
| `; |
| listContainer.appendChild(div); |
| }); |
| |
| const modal = document.getElementById('student-modal'); |
| modal.classList.remove('hidden'); |
| document.body.classList.add('overflow-hidden'); |
| setTimeout(() => { |
| modal.classList.add('opacity-100'); |
| }, 10); |
| |
| lucide.createIcons(); |
| } |
| |
| |
| function closeModal() { |
| const modal = document.getElementById('student-modal'); |
| modal.classList.remove('opacity-100'); |
| setTimeout(() => { |
| modal.classList.add('hidden'); |
| document.body.classList.remove('overflow-hidden'); |
| }, 300); |
| } |
| </script> |
| </body> |
| </html> |