Download datasource/index.html from SawKae/REG69: direct link, hf CLI and curl.
- Browser
- Download file 43.1 kB
-
https://huggingface.co/spaces/SawKae/REG69/resolve/main/datasource/index.html
- Command line
-
hf download hf://spaces/SawKae/REG69/datasource/index.html
-
curl -L -o index.html https://huggingface.co/spaces/SawKae/REG69/resolve/main/datasource/index.html
43.1 kB
| <html lang="th"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ระบบค้นหาและวิเคราะห์สถิติการยื่นคำร้องเรียน</title> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Google Fonts (Kanit & Sarabun) --> | |
| <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"> | |
| <!-- Chart.js CDN --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <!-- PapaParse CDN (for CSV Parsing) --> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script> | |
| <!-- Lucide Icons --> | |
| <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 Navigation --> | |
| <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"> | |
| <!-- Upload Zone (Show initially) --> | |
| <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> | |
| <!-- Dashboard Content (Hidden until file is uploaded) --> | |
| <div id="dashboard-content" class="hidden space-y-8 animate-fade-in"> | |
| <!-- Quick Stats Cards --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5"> | |
| <!-- Stat 1 --> | |
| <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> | |
| <!-- Stat 2 --> | |
| <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> | |
| <!-- Stat 3 --> | |
| <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> | |
| <!-- Stat 4 --> | |
| <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> | |
| <!-- Charts Section (Comparative analysis) --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <!-- Bar Chart --> | |
| <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> | |
| <!-- Pie / Doughnut Chart --> | |
| <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> | |
| <!-- Search and Filter Section --> | |
| <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"> | |
| <!-- Search Box --> | |
| <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> | |
| <!-- Clear Search button --> | |
| <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> | |
| <!-- Table Container --> | |
| <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"> | |
| <!-- Rows will be dynamically loaded here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Empty State within Table --> | |
| <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> | |
| <!-- Table Pagination Footer --> | |
| <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"> | |
| <!-- Page numbers dynamically populated --> | |
| </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> | |
| <!-- Student Detail Modal --> | |
| <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]"> | |
| <!-- Modal Header --> | |
| <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> | |
| <!-- Modal Body (Scrollable) --> | |
| <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"> | |
| <!-- Petitions list dynamically populated --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal Footer --> | |
| <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> | |
| <!-- Page Footer --> | |
| <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> | |
| <!-- Core Scripts --> | |
| <script> | |
| // Global State | |
| let rawData = []; | |
| let filteredData = []; | |
| let currentPage = 1; | |
| const rowsPerPage = 10; | |
| // Chart Instances | |
| let barChartInstance = null; | |
| let pieChartInstance = null; | |
| // On DOM Load | |
| document.addEventListener('DOMContentLoaded', () => { | |
| lucide.createIcons(); | |
| setupEventListeners(); | |
| }); | |
| // Set up interactions | |
| 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'); | |
| // File input changed | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| processFile(e.target.files[0]); | |
| } | |
| }); | |
| // Drag over / Drag leave transitions | |
| ['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); | |
| }); | |
| // Handle dropped files | |
| dropZone.addEventListener('drop', (e) => { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length > 0) { | |
| processFile(files[0]); | |
| } | |
| }); | |
| // Search input typing | |
| searchInput.addEventListener('input', () => { | |
| currentPage = 1; | |
| filterAndRender(); | |
| }); | |
| // Clear search | |
| clearBtn.addEventListener('click', () => { | |
| searchInput.value = ''; | |
| currentPage = 1; | |
| filterAndRender(); | |
| }); | |
| // Metric dropdown selection | |
| chartMetric.addEventListener('change', () => { | |
| renderCharts(); | |
| }); | |
| // Table pagination buttons | |
| 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(); | |
| } | |
| }); | |
| } | |
| // CSV parsing & processing | |
| function processFile(file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| let text = e.target.result; | |
| // Clean university specific headers (skip the title line if present) | |
| const lines = text.split(/\r?\n/); | |
| if (lines[0] && lines[0].includes('ข้อมูลคำร้องเรียนร่วม/เรียนกรณีพิเศษ')) { | |
| // Remove the non-standard top title line | |
| 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'); | |
| } | |
| // Map & Format data structure | |
| function formatAndLoadData(data) { | |
| rawData = data.map(item => { | |
| // Mapping the Thai CSV headers | |
| 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 !== ''); // Clean data check | |
| filteredData = [...rawData]; | |
| currentPage = 1; | |
| // Display elements | |
| document.getElementById('upload-section').classList.add('hidden'); | |
| document.getElementById('dashboard-content').classList.remove('hidden'); | |
| calculateQuickStats(); | |
| filterAndRender(); | |
| } | |
| // Calculate Overview Cards | |
| function calculateQuickStats() { | |
| // Total petitions | |
| const totalPetitions = rawData.length; | |
| document.getElementById('stat-total-petitions').textContent = totalPetitions.toLocaleString('th-TH'); | |
| // Unique Students | |
| const uniqueStudentsSet = new Set(rawData.map(item => item.studentId)); | |
| document.getElementById('stat-unique-students').textContent = uniqueStudentsSet.size.toLocaleString('th-TH'); | |
| // Total Credits | |
| const totalCredits = rawData.reduce((sum, item) => sum + item.credits, 0); | |
| document.getElementById('stat-total-credits').textContent = totalCredits.toLocaleString('th-TH'); | |
| // Total Amount | |
| const totalAmount = rawData.reduce((sum, item) => sum + item.amount, 0); | |
| document.getElementById('stat-total-amount').textContent = totalAmount.toLocaleString('th-TH') + ' บาท'; | |
| } | |
| // Filter and re-render everything | |
| 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(); | |
| } | |
| // Render data to Table | |
| 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); | |
| // Boundary checks | |
| 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); | |
| }); | |
| // Info text | |
| 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(); // Reactivate icons | |
| } | |
| // Setup Pagination buttons | |
| 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; | |
| // Generate simple responsive page numbering | |
| 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); | |
| } | |
| } | |
| // Render Comparative Charts (Bar & Pie Chart) | |
| function renderCharts() { | |
| const metric = document.getElementById('chart-metric').value; | |
| // Group data by major | |
| 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; | |
| }); | |
| // Convert to array and sort descending by active metric | |
| const groupedArray = Object.values(majorGroups); | |
| groupedArray.sort((a, b) => b[metric] - a[metric]); | |
| // Top Majors for visualization | |
| 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)'; | |
| } | |
| // --- 1. Bar Chart --- | |
| 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', // Horizontal bars | |
| plugins: { | |
| legend: { | |
| display: false | |
| }, | |
| tooltip: { | |
| backgroundColor: '#0f172a', | |
| titleFont: { family: 'Kanit', size: 13 }, | |
| bodyFont: { family: 'Sarabun', size: 12 }, | |
| callbacks: { | |
| title: function(context) { | |
| // Map back to full label name | |
| 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 } } | |
| } | |
| } | |
| } | |
| }); | |
| // --- 2. Doughnut / Pie Chart --- | |
| const ctxPie = document.getElementById('majorPieChart').getContext('2d'); | |
| if (pieChartInstance) pieChartInstance.destroy(); | |
| // Doughnut data limit to top 5 | |
| const pieGrouped = groupedArray.slice(0, 5); | |
| const pieLabels = pieGrouped.map(item => item.name); | |
| const pieValues = pieGrouped.map(item => item[metric]); | |
| // Add "Others" segment if needed | |
| 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)', // Blue | |
| 'rgba(16, 185, 129, 0.85)', // Emerald | |
| 'rgba(245, 158, 11, 0.85)', // Amber | |
| 'rgba(139, 92, 246, 0.85)', // Violet | |
| 'rgba(236, 72, 153, 0.85)', // Pink | |
| 'rgba(148, 163, 184, 0.85)' // Slate (Others) | |
| ], | |
| 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}`; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Open Modal showing history of a single student | |
| 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(); | |
| } | |
| // Close Modal | |
| 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> |