REG69 / datasource /index.html
SawKae's picture
Upload 2 files
e9c17c6 verified
Raw
History Blame Contribute Delete
43.1 kB
<!DOCTYPE html>
<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>