Demo_dashboard / datasource /index.html
SawKae's picture
Upload 2 files
8339eb2 verified
Raw
History Blame Contribute Delete
104 kB
<!DOCTYPE html>
<html lang="th" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ระบบคุมและติดตามหลักฐานการเบิกจ่ายงบประมาณ (มรพส.)</title>
<!-- Google Fonts: Sarabun & JetBrains Mono -->
<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=JetBrains+Mono:wght@400;500;700&family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS v3 -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
slate: {
900: '#0F172A',
800: '#1E293B',
700: '#334155',
500: '#64748B',
400: '#94A3B8',
100: '#F1F5F9',
},
indigo: {
500: '#6366F1',
600: '#4F46E5',
},
cyan: {
400: '#22D3EE',
},
emerald: {
400: '#34D399',
},
amber: {
400: '#FBBF24',
},
red: {
400: '#F87171',
}
},
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
}
}
}
}
</script>
<!-- FontAwesome for Premium Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- PapaParse & SheetJS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: 'Sarabun', sans-serif;
background-color: #0F172A;
color: #F1F5F9;
}
.font-mono {
font-family: 'JetBrains Mono', monospace;
font-variant-numeric: tabular-nums;
}
/* Custom scrollbar for premium dark feel */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0F172A;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #6366F1;
}
</style>
</head>
<body class="min-h-screen flex flex-col lg:flex-row overflow-x-hidden">
<!-- Sidebar For Desktop & Navigation Bar for Mobile -->
<aside aria-label="เมนูด้านข้างและตัวกรอง" class="w-full lg:w-96 bg-slate-800 border-b lg:border-b-0 lg:border-r border-slate-700 flex flex-col shrink-0 z-20">
<!-- Brand Section -->
<div class="p-6 border-b border-slate-700 bg-slate-900/50 flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-indigo-500 rounded-lg flex items-center justify-center text-slate-100 shadow-md shadow-indigo-500/20">
<i class="fa-solid fa-file-invoice-dollar text-xl"></i>
</div>
<div>
<h1 class="font-bold text-sm leading-tight text-slate-100">ระบบคุมและติดตามหลักฐาน</h1>
<p class="text-xs text-cyan-400 font-mono tracking-wider">มรพส. BUDGET CONTROLLER</p>
</div>
</div>
<!-- Mobile Menu Toggle Button (Optional Visual Accent) -->
<button id="mobile-menu-btn" class="lg:hidden p-2 text-slate-400 hover:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 rounded-md" aria-label="เปิดเมนูกรอง">
<i class="fa-solid fa-sliders text-lg"></i>
</button>
</div>
<!-- Ingestion & Filter Controls Panel -->
<div id="sidebar-controls" class="p-6 flex-1 overflow-y-auto space-y-6 hidden lg:block">
<!-- Ingestion Hub (Upload Zone) -->
<div class="bg-slate-900/60 p-4 rounded-xl border border-slate-700 space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xs font-semibold text-slate-400 uppercase tracking-wider">
<i class="fa-solid fa-cloud-arrow-up mr-1 text-indigo-500"></i> นำเข้าข้อมูล
</h2>
<span id="data-source-badge" class="px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-500/20 text-indigo-400 border border-indigo-500/30">
Default Loaded
</span>
</div>
<!-- Drag & Drop Area -->
<div id="drop-zone" class="border-2 border-dashed border-slate-700 hover:border-indigo-500 rounded-lg p-4 text-center cursor-pointer transition-colors duration-200 bg-slate-800/50">
<input type="file" id="file-input" class="hidden" accept=".csv,.xlsx">
<i class="fa-solid fa-file-excel text-2xl text-slate-500 mb-2" id="upload-icon"></i>
<p class="text-xs text-slate-300 font-semibold mb-1">ลากไฟล์ CSV / Excel วางที่นี่</p>
<p class="text-[10px] text-slate-500">หรือคลิกเพื่อค้นหาจากเครื่อง</p>
</div>
<!-- Google Sheets Link Ingestion -->
<div class="space-y-2">
<label for="sheet-url" class="block text-xs font-medium text-slate-400">หรือระบุ Public Google Sheets URL</label>
<div class="flex space-x-2">
<input type="url" id="sheet-url" placeholder="https://docs.google.com/spreadsheets/d/..."
class="flex-1 bg-slate-800 border border-slate-700 rounded p-2 text-xs text-slate-100 placeholder-slate-600 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
aria-label="Google Sheets URL">
<button id="btn-load-sheet" class="bg-indigo-500 hover:bg-indigo-600 text-slate-100 text-xs px-3 py-2 rounded font-semibold transition-colors">
ดึงข้อมูล
</button>
</div>
</div>
<!-- Quick Simulation Button -->
<button id="btn-load-demo" class="w-full bg-slate-800 hover:bg-slate-700 text-xs text-cyan-400 border border-cyan-400/30 rounded py-2 font-semibold transition-all flex items-center justify-center space-x-2 shadow-inner">
<i class="fa-solid fa-database text-[11px]"></i>
<span>โหลดข้อมูลทดสอบ (40 รายการ)</span>
</button>
</div>
<!-- Dynamic Filter Controls -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xs font-semibold text-slate-400 uppercase tracking-wider">
<i class="fa-solid fa-filter mr-1 text-cyan-400"></i> ตัวกรองข้อมูล
</h2>
<button id="btn-reset-filters" class="text-xs text-red-400 hover:text-red-300 font-semibold flex items-center space-x-1" aria-label="ล้างตัวกรองทั้งหมด">
<i class="fa-solid fa-rotate-left"></i>
<span>รีเซ็ตตัวกรอง</span>
</button>
</div>
<!-- Search Input Box -->
<div class="space-y-1">
<label for="search-box" class="block text-xs font-medium text-slate-400">ค้นหาด่วน (รายการ/เลขที่เบิก/บย.)</label>
<div class="relative">
<input type="text" id="search-box" placeholder="พิมพ์คีย์เวิร์ดเพื่อค้นหา..."
class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 pl-9 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
aria-label="พิมพ์คำค้นหา">
<i class="fa-solid fa-magnifying-glass absolute left-3 top-3.5 text-slate-500 text-xs"></i>
</div>
</div>
<!-- Department Selector -->
<div class="space-y-1">
<label for="filter-department" class="block text-xs font-medium text-slate-400">ผู้ขอเบิก (คณะ/หน่วยงาน)</label>
<select id="filter-department" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" aria-label="กรองตามคณะ">
<option value="">-- ทั้งหมดทุกคณะ/หน่วยงาน --</option>
</select>
</div>
<!-- Budget Type Selector -->
<div class="space-y-1">
<label for="filter-category" class="block text-xs font-medium text-slate-400">หมวดรายจ่าย</label>
<select id="filter-category" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" aria-label="กรองตามหมวดรายจ่าย">
<option value="">-- ทั้งหมดทุกหมวด --</option>
</select>
</div>
<!-- Month Selector -->
<div class="space-y-1">
<label for="filter-month" class="block text-xs font-medium text-slate-400">เดือนที่ส่งเบิก</label>
<select id="filter-month" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" aria-label="กรองตามเดือนที่ส่งเบิก">
<option value="">-- ทั้งหมดทุกเดือน --</option>
</select>
</div>
</div>
<!-- Footer Info -->
<div class="pt-6 border-t border-slate-700 text-center text-[11px] text-slate-500">
<p>พัฒนาสำหรับระบบติดตามหลักฐานงบประมาณ มรพส.</p>
<p class="font-mono mt-1">v2.1.0-Production (2026)</p>
</div>
</div>
</aside>
<!-- Main Workspace Content Panel -->
<main class="flex-1 flex flex-col min-w-0 overflow-y-auto max-h-screen">
<!-- Header Banner Section -->
<header class="bg-slate-800 border-b border-slate-700 px-6 py-4 flex flex-col md:flex-row md:items-center justify-between shrink-0 space-y-4 md:space-y-0">
<div>
<h2 class="text-lg font-bold text-slate-100 flex items-center space-x-2">
<span>แผงควบคุมและประมวลสถานะการเบิกจ่าย</span>
<span id="data-count-badge" class="ml-2 px-2 py-0.5 rounded-full text-xs font-mono font-bold bg-indigo-500/20 text-indigo-400 border border-indigo-500/40">
0 / 0 รายการ
</span>
</h2>
<p class="text-xs text-slate-400">วิเคราะห์ข้อมูล ยอดคุมค้าง อัตราความสำเร็จ และสถิติความล่าช้าจำแนกกลุ่มงบประมาณ</p>
</div>
<!-- Export Buttons Container -->
<div class="flex items-center space-x-2">
<button id="btn-export-xlsx" class="bg-emerald-500 hover:bg-emerald-600 text-slate-900 font-bold text-xs px-4 py-2.5 rounded-lg flex items-center space-x-2 shadow-lg shadow-emerald-500/10 transition-all hover:-translate-y-0.5">
<i class="fa-solid fa-file-excel"></i>
<span>Export XLSX</span>
</button>
<button id="btn-export-charts" class="bg-indigo-500 hover:bg-indigo-600 text-slate-100 font-bold text-xs px-4 py-2.5 rounded-lg flex items-center space-x-2 shadow-lg shadow-indigo-500/10 transition-all hover:-translate-y-0.5">
<i class="fa-solid fa-chart-line"></i>
<span>Export Chart Images</span>
</button>
</div>
</header>
<!-- Dynamic Working Dashboard Grid Space -->
<div class="p-6 space-y-6 flex-1">
<!-- KPI Cards Grid (Desktop: 4 columns, Mobile: 1-2 columns) -->
<section aria-label="ดัชนีชี้วัดผลงานหลัก (KPI)" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- KPI 1: Total Claims & Money -->
<div class="bg-slate-800 border border-slate-700 rounded-xl p-5 shadow-sm hover:shadow-lg hover:border-slate-600 transition-all duration-300 transform hover:-translate-y-1">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">ส่งเบิกทั้งหมด</span>
<div class="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center text-indigo-400">
<i class="fa-solid fa-folder-open text-sm"></i>
</div>
</div>
<div class="space-y-1">
<div class="text-3xl font-bold font-mono text-slate-100" id="kpi-total-claims">0</div>
<div class="text-xs text-slate-400 flex items-center space-x-1">
<span>มูลค่ารวม:</span>
<span class="font-mono text-cyan-400 font-bold" id="kpi-total-amount">฿0.00</span>
</div>
</div>
</div>
<!-- KPI 2: Successful Claims & Money -->
<div class="bg-slate-800 border border-slate-700 rounded-xl p-5 shadow-sm hover:shadow-lg hover:border-slate-600 transition-all duration-300 transform hover:-translate-y-1">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">เบิกจ่ายสำเร็จ</span>
<div class="w-8 h-8 rounded-lg bg-emerald-500/10 flex items-center justify-center text-emerald-400">
<i class="fa-solid fa-circle-check text-sm"></i>
</div>
</div>
<div class="space-y-1">
<div class="text-3xl font-bold font-mono text-emerald-400" id="kpi-success-claims">0</div>
<div class="text-xs text-slate-400 flex items-center space-x-1">
<span>จ่ายแล้วรวม:</span>
<span class="font-mono text-emerald-400 font-bold" id="kpi-success-amount">฿0.00</span>
</div>
</div>
</div>
<!-- KPI 3: Returned / Under Edit Claims & Money -->
<div class="bg-slate-800 border border-slate-700 rounded-xl p-5 shadow-sm hover:shadow-lg hover:border-slate-600 transition-all duration-300 transform hover:-translate-y-1">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">เอกสารตีกลับแก้ไข</span>
<div class="w-8 h-8 rounded-lg bg-red-500/10 flex items-center justify-center text-red-400">
<i class="fa-solid fa-circle-exclamation text-sm"></i>
</div>
</div>
<div class="space-y-1">
<div class="text-3xl font-bold font-mono text-red-400" id="kpi-returned-claims">0</div>
<div class="text-xs text-slate-400 flex items-center space-x-1">
<span>มูลค่าติดขัด:</span>
<span class="font-mono text-red-400 font-bold" id="kpi-returned-amount">฿0.00</span>
</div>
</div>
</div>
<!-- KPI 4: Ratio Stats -->
<div class="bg-slate-800 border border-slate-700 rounded-xl p-5 shadow-sm hover:shadow-lg hover:border-slate-600 transition-all duration-300 transform hover:-translate-y-1">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider">อัตราประสิทธิผล</span>
<div class="w-8 h-8 rounded-lg bg-amber-500/10 flex items-center justify-center text-amber-400">
<i class="fa-solid fa-chart-pie text-sm"></i>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between items-center text-xs text-slate-400">
<span>ความสำเร็จ:</span>
<span class="font-mono font-bold text-emerald-400 text-sm" id="kpi-rate-success">0.00%</span>
</div>
<div class="w-full bg-slate-900 rounded-full h-1.5 overflow-hidden">
<div id="kpi-rate-bar" class="bg-emerald-400 h-1.5 rounded-full" style="width: 0%"></div>
</div>
<div class="flex justify-between items-center text-xs text-slate-400">
<span>อัตราการตีกลับแก้ไข:</span>
<span class="font-mono font-bold text-red-400 text-sm" id="kpi-rate-returned">0.00%</span>
</div>
</div>
</div>
</section>
<!-- Chart Visualization Engine Panels (Grid: Line and Stacked Bar) -->
<section aria-label="วิเคราะห์ข้อมูลเชิงภาพ" class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<!-- Chart 1: Line Chart for Monthly Trend -->
<div class="bg-slate-800 border border-slate-700 rounded-xl p-5 flex flex-col shadow-sm">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-sm font-semibold text-slate-100 flex items-center space-x-1.5">
<i class="fa-solid fa-chart-line text-indigo-500"></i>
<span>แนวโน้มการส่งและเบิกจ่ายรายเดือน</span>
</h3>
<p class="text-[11px] text-slate-400">แสดงแนวทางวิเคราะห์ปริมาณเอกสารที่ส่ง, เบิกจ่ายสำเร็จ และตีกลับแก้ไข</p>
</div>
<span class="text-[10px] text-slate-400 font-mono uppercase bg-slate-900 px-2 py-1 rounded">Time-series Grid</span>
</div>
<div class="relative flex-1 min-h-[300px]">
<canvas id="lineTrendChart"></canvas>
</div>
</div>
<!-- Chart 2: Stacked Bar Chart for Faculty BottleNecks -->
<div class="bg-slate-800 border border-slate-700 rounded-xl p-5 flex flex-col shadow-sm">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-sm font-semibold text-slate-100 flex items-center space-x-1.5">
<i class="fa-solid fa-chart-bar text-cyan-400"></i>
<span>ปริมาณหลักฐานเบิกจ่ายแยกตามคณะ / หน่วยงาน</span>
</h3>
<p class="text-[11px] text-slate-400">วิเคราะห์หาจุดติดขัด (Bottleneck) จากจำนวนการเบิกจ่ายสำเร็จเทียบสถิติตีกลับ</p>
</div>
<span class="text-[10px] text-slate-400 font-mono uppercase bg-slate-900 px-2 py-1 rounded">Stacked Stats</span>
</div>
<div class="relative flex-1 min-h-[300px]">
<canvas id="stackedBarChart"></canvas>
</div>
</div>
</section>
<!-- Custom User Feedback Banner / Alerts Info -->
<div id="feedback-alert" class="hidden px-4 py-3.5 rounded-lg border flex items-center justify-between transition-opacity duration-300">
<div class="flex items-center space-x-2">
<span id="feedback-icon" class="text-lg"></span>
<span id="feedback-message" class="text-xs font-semibold"></span>
</div>
<button onclick="hideFeedbackAlert()" class="text-slate-400 hover:text-slate-100" aria-label="ปิดแจ้งเตือน">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<!-- Enhanced Interactive Data Table Component -->
<section aria-label="ตารางฐานข้อมูลผู้เบิกจ่ายทั้งหมด" class="bg-slate-800 border border-slate-700 rounded-xl shadow-sm flex flex-col overflow-hidden">
<!-- Table Header Controls -->
<div class="px-6 py-4 border-b border-slate-700 bg-slate-900/40 flex flex-col sm:flex-row sm:items-center justify-between space-y-3 sm:space-y-0">
<h3 class="text-sm font-bold text-slate-100 flex items-center space-x-2">
<i class="fa-solid fa-list-check text-slate-400"></i>
<span>ตารางแสดงข้อมูลเอกสารงบประมาณคุมเบิก</span>
</h3>
<div class="flex items-center space-x-3 text-xs">
<label for="table-page-size" class="text-slate-400 whitespace-nowrap">แสดงแถวต่อหน้า:</label>
<select id="table-page-size" class="bg-slate-800 border border-slate-700 rounded p-1 text-slate-100 font-mono">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<!-- Table Content Container (Horizontal Scrolling Guard) -->
<div class="overflow-x-auto w-full max-h-[500px]">
<table class="w-full text-left border-collapse table-auto">
<thead class="bg-slate-900/80 text-[11px] text-slate-400 font-semibold uppercase tracking-wider sticky top-0 border-b border-slate-700 z-10 backdrop-blur-sm">
<tr>
<th class="py-3 px-4 text-center w-12">#</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none whitespace-nowrap" onclick="toggleSort('วันที่')">
วันที่ส่งเบิก <i id="sort-icon-วันที่" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none whitespace-nowrap" onclick="toggleSort('ผู้ขอเบิก')">
ผู้ขอเบิก (คณะ) <i id="sort-icon-ผู้ขอเบิก" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none min-w-[200px]" onclick="toggleSort('รายการ')">
รายการคำขอ <i id="sort-icon-รายการ" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none whitespace-nowrap" onclick="toggleSort('หมวดรายจ่าย')">
หมวดรายจ่าย <i id="sort-icon-หมวดรายจ่าย" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none text-right whitespace-nowrap" onclick="toggleSort('จำนวนเงิน')">
จำนวนเงิน <i id="sort-icon-จำนวนเงิน" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none text-center whitespace-nowrap" onclick="toggleSort('Status')">
สถานะการเบิกจ่าย <i id="sort-icon-Status" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 cursor-pointer hover:text-slate-100 select-none whitespace-nowrap text-right" onclick="toggleSort('เลขที่เบิก')">
เลขที่เบิก <i id="sort-icon-เลขที่เบิก" class="fa-solid fa-sort ml-1"></i>
</th>
<th class="py-3 px-4 whitespace-nowrap text-right">เลขที่ บย.</th>
</tr>
</thead>
<tbody id="table-body" class="text-xs divide-y divide-slate-700/60 font-medium">
<!-- Dynamic Row Injection -->
</tbody>
</table>
</div>
<!-- Table Empty State -->
<div id="table-empty-state" class="hidden p-12 text-center text-slate-500">
<i class="fa-solid fa-folder-open text-4xl mb-3"></i>
<p class="text-sm">ไม่พบข้อมูลตามเงื่อนไขการค้นหา/ตัวกรองที่คุณระบุ</p>
</div>
<!-- Table Footer & Pagination Navigation -->
<div class="px-6 py-4 bg-slate-900/40 border-t border-slate-700 flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 text-xs">
<span id="table-pagination-info" class="text-slate-400 font-mono">
กำลังแสดงรายการ 0 - 0 จากทั้งหมด 0 รายการ
</span>
<nav aria-label="ปุ่มจำแนกหน้ากระดาษ" class="flex items-center space-x-1">
<button id="btn-page-first" class="px-3 py-2 bg-slate-800 text-slate-400 hover:text-slate-100 hover:bg-slate-700 disabled:opacity-50 disabled:hover:bg-slate-800 disabled:hover:text-slate-400 rounded transition-all font-mono" disabled>
<i class="fa-solid fa-angles-left"></i>
</button>
<button id="btn-page-prev" class="px-3 py-2 bg-slate-800 text-slate-400 hover:text-slate-100 hover:bg-slate-700 disabled:opacity-50 disabled:hover:bg-slate-800 disabled:hover:text-slate-400 rounded transition-all font-mono" disabled>
<i class="fa-solid fa-angle-left"></i>
</button>
<span id="table-current-page" class="px-4 py-2 bg-slate-900 border border-slate-700 rounded text-slate-100 font-mono font-bold">
1
</span>
<button id="btn-page-next" class="px-3 py-2 bg-slate-800 text-slate-400 hover:text-slate-100 hover:bg-slate-700 disabled:opacity-50 disabled:hover:bg-slate-800 disabled:hover:text-slate-400 rounded transition-all font-mono" disabled>
<i class="fa-solid fa-angle-right"></i>
</button>
<button id="btn-page-last" class="px-3 py-2 bg-slate-800 text-slate-400 hover:text-slate-100 hover:bg-slate-700 disabled:opacity-50 disabled:hover:bg-slate-800 disabled:hover:text-slate-400 rounded transition-all font-mono" disabled>
<i class="fa-solid fa-angles-right"></i>
</button>
</nav>
</div>
</section>
</div>
</main>
<!-- Detailed Logic Core Scripts -->
<script>
// High Quality Simulated Dataset matching 'มรพส.' domain structure & budget codes
const simulatedDataset = [
{"วันที่": "1-พ.ค.-69", "ผู้ขอเบิก": "คณะวิทยาศาสตร์", "รายการ": "จัดซื้อสารเคมีและวัสดุเคมีสำหรับปฏิบัติการเคมีอินทรีย์ชั้นปีที่ 2", "หมวดรายจ่าย": "ค่าวัสดุ", "จำนวนเงิน": "45000", "เลขที่ตัดยอด": "TY-001/69", "เลขที่ใบงบหน้า": "GB-101/69", "เลขที่เบิก": "BK-9001/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0122", "created_by": "officer_A"},
{"วันที่": "2-พ.ค.-69", "ผู้ขอเบิก": "คณะวิศวกรรมศาสตร์", "รายการ": "จัดซื้ออุปกรณ์คอมพิวเตอร์และหน่วยประมวลผล GPU สตรีมมิ่งเซิร์ฟเวอร์", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "320000", "เลขที่ตัดยอด": "TY-002/69", "เลขที่ใบงบหน้า": "GB-102/69", "เลขที่เบิก": "BK-9002/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0123", "created_by": "officer_A"},
{"วันที่": "3-พ.ค.-69", "ผู้ขอเบิก": "คณะเทคโนโลยีการเกษตร", "รายการ": "จ้างเหมาเตรียมแปลงเกษตรทดลองและเพาะเลี้ยงต้นกล้าสายพันธุ์ใหม่", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "18500", "เลขที่ตัดยอด": "TY-003/69", "เลขที่ใบงบหน้า": "GB-103/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "10-พ.ค.-69", "รายการแก้ไข": "แก้ไขจำนวนเงินและแนบใบเสนอราคาใหม่", "วันที่รับคืน": "15-พ.ค.-69", "เลขที่ บย.": "บย.69-0124", "created_by": "officer_B"},
{"วันที่": "5-พ.ค.-69", "ผู้ขอเบิก": "คณะมนุษยศาสตร์", "รายการ": "จัดโครงการพัฒนาทักษะภาษาอังกฤษเพื่ออาชีพการบริการยุคดิจิทัล", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "54000", "เลขที่ตัดยอด": "TY-004/69", "เลขที่ใบงบหน้า": "GB-104/69", "เลขที่เบิก": "BK-9003/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0125", "created_by": "officer_C"},
{"วันที่": "8-พ.ค.-69", "ผู้ขอเบิก": "คณะครุศาสตร์", "รายการ": "จัดซื้อชุดสื่อการเรียนรู้ภาษาไทยระดับประถมศึกษาสาธิต มรพส.", "หมวดรายจ่าย": "ค่าวัสดุ", "จำนวนเงิน": "29500", "เลขที่ตัดยอด": "TY-005/69", "เลขที่ใบงบหน้า": "GB-105/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0126", "created_by": "officer_A"},
{"วันที่": "12-พ.ค.-69", "ผู้ขอเบิก": "คณะวิทยาการจัดการ", "รายการ": "จัดซื้อตู้จัดเก็บเอกสารสารสนเทศทนไฟจำนวน 2 ตู้", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "48000", "เลขที่ตัดยอด": "TY-006/69", "เลขที่ใบงบหน้า": "GB-106/69", "เลขที่เบิก": "BK-9004/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0127", "created_by": "officer_B"},
{"วันที่": "15-พ.ค.-69", "ผู้ขอเบิก": "คณะวิทยาศาสตร์", "รายการ": "จ้างเหมาบำรุงรักษาเครื่องจ่ายไฟสำรอง UPS กลางห้องปฏิบัติการชีววิทยา", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "15000", "เลขที่ตัดยอด": "TY-007/69", "เลขที่ใบงบหน้า": "GB-107/69", "เลขที่เบิก": "BK-9005/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0128", "created_by": "officer_C"},
{"วันที่": "18-พ.ค.-69", "ผู้ขอเบิก": "คณะวิศวกรรมศาสตร์", "รายการ": "จ้างปรับปรุงซ่อมแซมฝ้าเพดานทางเดินอาคารปฏิบัติการชั้น 3", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "89000", "เลขที่ตัดยอด": "TY-008/69", "เลขที่ใบงบหน้า": "GB-108/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "25-พ.ค.-69", "รายการแก้ไข": "แนบแบบรูปงวดงานแก้ไขสเปกสายไฟใหม่", "วันที่รับคืน": "29-พ.ค.-69", "เลขที่ บย.": "บย.69-0129", "created_by": "officer_A"},
{"วันที่": "20-พ.ค.-69", "ผู้ขอเบิก": "คณะพยาบาลศาสตร์", "รายการ": "จัดซื้อหุ่นจำลองฝึกปฐมพยาบาลขั้นพื้นฐานพร้อมชุดรายงานอัจฉริยะ", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "185000", "เลขที่ตัดยอด": "TY-009/69", "เลขที่ใบงบหน้า": "GB-109/69", "เลขที่เบิก": "BK-9006/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0130", "created_by": "officer_B"},
{"วันที่": "25-พ.ค.-69", "ผู้ขอเบิก": "คณะเทคโนโลยีการเกษตร", "รายการ": "จัดซื้อระบบควบคุมและให้ปุ๋ยอัจฉริยะผ่านแอปพลิเคชันมือถือ", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "96000", "เลขที่ตัดยอด": "TY-010/69", "เลขที่ใบงบหน้า": "GB-110/69", "เลขที่เบิก": "BK-9007/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0131", "created_by": "officer_C"},
{"วันที่": "2-มิ.ย.-69", "ผู้ขอเบิก": "คณะวิทยาศาสตร์", "รายการ": "จัดซื้อวัสดุคอมพิวเตอร์และหมึกพิมพ์สำนักงานบริหารคณะ", "หมวดรายจ่าย": "ค่าวัสดุ", "จำนวนเงิน": "12500", "เลขที่ตัดยอด": "TY-011/69", "เลขที่ใบงบหน้า": "GB-111/69", "เลขที่เบิก": "BK-9008/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0132", "created_by": "officer_A"},
{"วันที่": "5-มิ.ย.-69", "ผู้ขอเบิก": "คณะวิทยาการจัดการ", "รายการ": "จัดโครงการบ่มเพาะนักศึกษาผู้ประกอบการโลจิสติกส์ยุคหน้า", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "45000", "เลขที่ตัดยอด": "TY-012/69", "เลขที่ใบงบหน้า": "GB-112/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "12-มิ.ย.-69", "รายการแก้ไข": "แก้ไขใบสำคัญรับเงินของผู้เข้าร่วมอบรม", "วันที่รับคืน": "16-มิ.ย.-69", "เลขที่ บย.": "บย.69-0133", "created_by": "officer_A"},
{"วันที่": "8-มิ.ย.-69", "ผู้ขอเบิก": "คณะครุศาสตร์", "รายการ": "จัดซื้อวัสดุและกระดาษสำหรับใช้ประมวลผลเกรดรวมภาคการศึกษา", "หมวดรายจ่าย": "ค่าวัสดุ", "จำนวนเงิน": "15000", "เลขที่ตัดยอด": "TY-013/69", "เลขที่ใบงบหน้า": "GB-113/69", "เลขที่เบิก": "BK-9009/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0134", "created_by": "officer_B"},
{"วันที่": "10-มิ.ย.-69", "ผู้ขอเบิก": "คณะมนุษยศาสตร์", "รายการ": "จัดซื้อตู้พ่นฆ่าเชื้อเอกสารประวัติศาสตร์ห้องพิพิธภัณฑ์ศิลปะ", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "75000", "เลขที่ตัดยอด": "TY-014/69", "เลขที่ใบงบหน้า": "GB-114/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0135", "created_by": "officer_C"},
{"วันที่": "15-มิ.ย.-69", "ผู้ขอเบิก": "คณะวิศวกรรมศาสตร์", "รายการ": "จ้างปรับแต่งทางลาดและราวจับผู้พิการทางเดินเชื่อมอาคารสโมสร", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "120000", "เลขที่ตัดยอด": "TY-015/69", "เลขที่ใบงบหน้า": "GB-115/69", "เลขที่เบิก": "BK-9010/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0136", "created_by": "officer_A"},
{"วันที่": "18-มิ.ย.-69", "ผู้ขอเบิก": "คณะพยาบาลศาสตร์", "รายการ": "จัดจ้างตรวจสอบปรับเทียบมาตรฐานเครื่องวัดความดันโลหิตและชีพจร", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "24000", "เลขที่ตัดยอด": "TY-016/69", "เลขที่ใบงบหน้า": "GB-116/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "22-มิ.ย.-69", "รายการแก้ไข": "ขอให้จัดแนบตารางปรับเทียบเครื่องมือแพทย์เพิ่ม", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0137", "created_by": "officer_B"},
{"วันที่": "20-มิ.ย.-69", "ผู้ขอเบิก": "คณะเทคโนโลยีการเกษตร", "รายการ": "จัดโครงการส่งเสริมผลิตภัณฑ์เห็ดนางฟ้าและกล้วยฉาบในชุมชนท้องถิ่น", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "32000", "เลขที่ตัดยอด": "TY-017/69", "เลขที่ใบงบหน้า": "GB-117/69", "เลขที่เบิก": "BK-9011/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0138", "created_by": "officer_C"},
{"วันที่": "24-มิ.ย.-69", "ผู้ขอเบิก": "คณะวิทยาศาสตร์", "รายการ": "จ้างตรวจเช็กสภาพหัวพ่นโครมาโทกราฟีแก๊สประสิทธิภาพสูงประจำศูนย์วิจัย", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "65000", "เลขที่ตัดยอด": "TY-018/69", "เลขที่ใบงบหน้า": "GB-118/69", "เลขที่เบิก": "BK-9012/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0139", "created_by": "officer_A"},
{"วันที่": "28-มิ.ย.-69", "ผู้ขอเบิก": "คณะวิทยาการจัดการ", "รายการ": "จัดซื้อตู้กดน้ำดื่มสเตนเลสประหยัดพลังงานไฟเบอร์ 5 จำนวน 3 ตู้", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "54000", "เลขที่ตัดยอด": "TY-019/69", "เลขที่ใบงบหน้า": "GB-119/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "3-ก.ค.-69", "รายการแก้ไข": "เปลี่ยนแบบฐานรองรับน้ำทิ้ง", "วันที่รับคืน": "7-ก.ค.-69", "เลขที่ บย.": "บย.69-0140", "created_by": "officer_B"},
{"วันที่": "30-มิ.ย.-69", "ผู้ขอเบิก": "คณะมนุษยศาสตร์", "รายการ": "จัดหาเก้าอี้เรียนรู้แบบอเนกประสงค์ปรับระดับได้สำหรับลานสร้างสรรค์", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "35000", "เลขที่ตัดยอด": "TY-020/69", "เลขที่ใบงบหน้า": "GB-120/69", "เลขที่เบิก": "BK-9013/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0141", "created_by": "officer_C"},
{"วันที่": "2-ก.ค.-69", "ผู้ขอเบิก": "คณะวิศวกรรมศาสตร์", "รายการ": "จัดซื้อท่อน้ำระบายความร้อนและฟิลเตอร์ป้องกันฝุ่นเครื่องทดสอบกำลังวัสดุ", "หมวดรายจ่าย": "ค่าวัสดุ", "จำนวนเงิน": "19000", "เลขที่ตัดยอด": "TY-021/69", "เลขที่ใบงบหน้า": "GB-121/69", "เลขที่เบิก": "BK-9014/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0142", "created_by": "officer_A"},
{"วันที่": "5-ก.ค.-69", "ผู้ขอเบิก": "คณะครุศาสตร์", "รายการ": "จัดซื้อกระดานอัจฉริยะจอสัมผัสขนาด 65 นิ้วสำหรับห้องเรียนทดลองพัฒนาทักษะ", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "125000", "เลขที่ตัดยอด": "TY-022/69", "เลขที่ใบงบหน้า": "GB-122/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0143", "created_by": "officer_B"},
{"วันที่": "8-ก.ค.-69", "ผู้ขอเบิก": "คณะพยาบาลศาสตร์", "รายการ": "จัดจ้างโครงการอบรมพยาบาลส่งเสริมสุขภาวะผู้สูงวัยเชิงรับมือภัยพิบัติ", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "67000", "เลขที่ตัดยอด": "TY-023/69", "เลขที่ใบงบหน้า": "GB-123/69", "เลขที่เบิก": "BK-9015/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0144", "created_by": "officer_C"},
{"วันที่": "10-ก.ค.-69", "ผู้ขอเบิก": "คณะเทคโนโลยีการเกษตร", "รายการ": "จัดซื้อเครื่องวัดและรายงานความชื้นหน้าดินไร้สายภาคสนาม จำนวน 10 ชุด", "หมวดรายจ่าย": "ค่าวัสดุ", "จำนวนเงิน": "28000", "เลขที่ตัดยอด": "TY-024/69", "เลขที่ใบงบหน้า": "GB-124/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "15-ก.ค.-69", "รายการแก้ไข": "แก้ไขระบุพิกัดพื้นที่การใช้สอยประโยชน์เพิ่มเติม", "วันที่รับคืน": "18-ก.ค.-69", "เลขที่ บย.": "บย.69-0145", "created_by": "officer_A"},
{"วันที่": "12-ก.ค.-69", "ผู้ขอเบิก": "คณะวิทยาศาสตร์", "รายการ": "จัดทำโครงการเปิดรับการทดสอบเครื่องมือเชิงระบบฟิสิกส์แก่เครือข่ายวิจัย", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "41000", "เลขที่ตัดยอด": "TY-025/69", "เลขที่ใบงบหน้า": "GB-125/69", "เลขที่เบิก": "BK-9016/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0146", "created_by": "officer_B"},
{"วันที่": "15-ก.ค.-69", "ผู้ขอเบิก": "คณะวิทยาการจัดการ", "รายการ": "จัดซื้อเก้าอี้สำนักงานแกนไฮดรอลิกสีดำ จำนวน 12 ตัว", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "36000", "เลขที่ตัดยอด": "TY-026/69", "เลขที่ใบงบหน้า": "GB-126/69", "เลขที่เบิก": "BK-9017/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0147", "created_by": "officer_C"},
{"วันที่": "18-ก.ค.-69", "ผู้ขอเบิก": "คณะมนุษยศาสตร์", "รายการ": "จัดหาเครื่องบันทึกเสียงและวิดีโอคุณภาพเสียงสเตอริโอระดับสตูดิโอ", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "15900", "เลขที่ตัดยอด": "TY-027/69", "เลขที่ใบงบหน้า": "GB-127/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0148", "created_by": "officer_A"},
{"วันที่": "20-ก.ค.-69", "ผู้ขอเบิก": "คณะวิศวกรรมศาสตร์", "รายการ": "จ้างบำรุงรักษาเชิงป้องกันเครื่อง CNC Mill ประจำศูนย์ฝึกช่างกลชั้นนำ", "หมวดรายจ่าย": "ค่าใช้สอย", "จำนวนเงิน": "54000", "เลขที่ตัดยอด": "TY-028/69", "เลขที่ใบงบหน้า": "GB-128/69", "เลขที่เบิก": "BK-9018/69", "วันที่ส่งคืน": "", "รายการแก้ไข": "", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0149", "created_by": "officer_B"},
{"วันที่": "22-ก.ค.-69", "ผู้ขอเบิก": "คณะครุศาสตร์", "รายการ": "จัดหาตู้เหล็กวางและตรวจสอบหนังสืออ้างอิงสาขาวิชาชีพครูวิทยาศาสตร์", "หมวดรายจ่าย": "ค่าครุภัณฑ์", "จำนวนเงิน": "21000", "เลขที่ตัดยอด": "TY-029/69", "เลขที่ใบงบหน้า": "GB-129/69", "เลขที่เบิก": "", "วันที่ส่งคืน": "28-ก.ค.-69", "รายการแก้ไข": "แก้ไขสเปกตู้เหล็กและเอกสารจัดซื้อจัดจ้างใหม่", "วันที่รับคืน": "", "เลขที่ บย.": "บย.69-0150", "created_by": "officer_C"}
];
// System Constants for Date Parsing & Translation
const thMonths = {
'ม.ค.': 0, 'ก.พ.': 1, 'มี.ค.': 2, 'เม.ย.': 3, 'พ.ค.': 4, 'มิ.ย.': 5,
'ก.ค.': 6, 'ส.ค.': 7, 'ก.ย.': 8, 'ต.ค.': 9, 'พ.ย.': 10, 'ธ.ค.': 11
};
const thFullMonthNames = [
"มกราคม", "กุมภาพันธ์", "มีนาคม", "เมษายน", "พฤษภาคม", "มิถุนายน",
"กรกฎาคม", "สิงหาคม", "กันยายน", "ตุลาคม", "พฤศจิกายน", "ธันวาคม"
];
// Global State variables
let rawDatasetGlobal = [];
let cleanedDatasetGlobal = [];
let filteredDatasetGlobal = [];
// Pagination and Sorting states
let currentPage = 1;
let pageSize = 20;
let currentSortColumn = 'วันที่';
let currentSortDirection = 'desc'; // 'asc' or 'desc'
// Chart References
let trendChartInstance = null;
let deptChartInstance = null;
// Document Load entry point
window.onload = function() {
// Attempt to load the default local source first
autoLoadDefaultSource();
// Wire up all structural event listeners
initEventListeners();
};
// Attempting default load
function autoLoadDefaultSource() {
const defaultPath = './datasource/ทะเบียนคุมหลักฐานการเบิก_20260615_165709 (CSV).csv';
fetch(defaultPath)
.then(response => {
if (!response.ok) {
throw new Error('Default file not found, loading fallback dataset.');
}
return response.text();
})
.then(csvText => {
parseCSV(csvText);
showFeedback('success', 'เชื่อมต่อและดึงข้อมูลทะเบียนคุมหลักฐานสำเร็จอัตโนมัติ!');
updateDataSourceBadge('Auto Loaded');
})
.catch(err => {
console.log(err.message);
// Use simulated standard fallback dataset on default failure
loadDataset(simulatedDataset);
showFeedback('info', 'ยินดีต้อนรับ! กำลังใช้งานชุดข้อมูลตัวอย่างสาธิตงบเบิกจ่าย มรพส.');
updateDataSourceBadge('Simulated Data');
});
}
// Initialize user interaction controls
function initEventListeners() {
// Mobile menu toggling
document.getElementById('mobile-menu-btn').addEventListener('click', () => {
const sidebarControls = document.getElementById('sidebar-controls');
sidebarControls.classList.toggle('hidden');
});
// Drag and Drop Zone events
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500', 'bg-indigo-500/10');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500', 'bg-indigo-500/10');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500', 'bg-indigo-500/10');
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
processUploadedFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
processUploadedFile(e.target.files[0]);
}
});
// Load Demo Action
document.getElementById('btn-load-demo').addEventListener('click', () => {
loadDataset(simulatedDataset);
showFeedback('success', 'โหลดไฟล์จำลอง มรพส. จำแนก 40 รายการเรียบร้อย');
updateDataSourceBadge('Simulated Data');
});
// Google Sheets load action
document.getElementById('btn-load-sheet').addEventListener('click', () => {
const url = document.getElementById('sheet-url').value.trim();
if (!url) {
showFeedback('danger', 'กรุณาระบุมาร์กอัป Google Sheets URL ที่ถูกต้อง');
return;
}
loadFromGoogleSheets(url);
});
// Filtering Event Listeners
document.getElementById('search-box').addEventListener('input', applyFilters);
document.getElementById('filter-department').addEventListener('change', applyFilters);
document.getElementById('filter-category').addEventListener('change', applyFilters);
document.getElementById('filter-month').addEventListener('change', applyFilters);
document.getElementById('btn-reset-filters').addEventListener('click', resetFilters);
// Table Pagination actions
document.getElementById('table-page-size').addEventListener('change', (e) => {
pageSize = parseInt(e.target.value);
currentPage = 1;
renderTable();
});
document.getElementById('btn-page-first').addEventListener('click', () => { currentPage = 1; renderTable(); });
document.getElementById('btn-page-prev').addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderTable(); } });
document.getElementById('btn-page-next').addEventListener('click', () => {
const maxPage = Math.ceil(filteredDatasetGlobal.length / pageSize);
if (currentPage < maxPage) { currentPage++; renderTable(); }
});
document.getElementById('btn-page-last').addEventListener('click', () => {
const maxPage = Math.ceil(filteredDatasetGlobal.length / pageSize);
currentPage = maxPage;
renderTable();
});
// Utility Export Actions
document.getElementById('btn-export-xlsx').addEventListener('click', exportCleanedDataToExcel);
document.getElementById('btn-export-charts').addEventListener('click', downloadChartSnapshots);
}
// Process custom input file uploaded by user
function processUploadedFile(file) {
const ext = file.name.split('.').pop().toLowerCase();
const reader = new FileReader();
if (ext === 'csv') {
reader.onload = function(e) {
parseCSV(e.target.result);
showFeedback('success', `อัปโหลดและสกัดข้อมูล CSV: ${file.name} สำเร็จ`);
updateDataSourceBadge('Custom CSV');
};
reader.readAsText(file, 'UTF-8');
} else if (ext === 'xlsx') {
reader.onload = function(e) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {type: 'array'});
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, {raw: false});
loadDataset(jsonData);
showFeedback('success', `อัปโหลดและคลีนชีต Excel: ${file.name} สำเร็จ`);
updateDataSourceBadge('Custom Excel');
};
reader.readAsArrayBuffer(file);
} else {
showFeedback('danger', 'รองรับเฉพาะไฟล์ข้อมูลนามสกุล .csv และ .xlsx เท่านั้น');
}
}
// Connect and load published CSV from Google Sheets URL
function loadFromGoogleSheets(url) {
// Check if user entered normal link instead of published link, and try to convert
let fetchUrl = url;
if (url.includes('docs.google.com/spreadsheets') && !url.includes('output=csv')) {
const parts = url.split('/d/');
if (parts.length > 1) {
const sheetId = parts[1].split('/')[0];
fetchUrl = `https://docs.google.com/spreadsheets/d/${sheetId}/export?format=csv`;
}
}
showFeedback('info', 'กำลังดึงเอกสารงบประมาณและเชื่อมต่อ API ของ Google...');
fetch(fetchUrl)
.then(res => {
if (!res.ok) throw new Error('ไม่สามารถดึงแผ่นงานของคุณได้ ตรวจสอบสถานะการแชร์แบบสาธารณะ');
return res.text();
})
.then(csvText => {
parseCSV(csvText);
showFeedback('success', 'เชื่อมและดึงฐานข้อมูลจาก Google Sheets เรียบร้อย');
updateDataSourceBadge('Google Sheets');
})
.catch(err => {
showFeedback('danger', `เกิดข้อผิดพลาด: ${err.message}`);
});
}
// Parse Raw CSV text
function parseCSV(csvText) {
Papa.parse(csvText, {
header: true,
skipEmptyLines: true,
complete: function(results) {
loadDataset(results.data);
},
error: function(err) {
showFeedback('danger', `ไม่สามารถคลีนเอกสาร CSV ได้: ${err.message}`);
}
});
}
// Handle incoming dataset structure normalization
function loadDataset(rawRows) {
rawDatasetGlobal = rawRows;
cleanAndSynthesizeData();
populateFilterDropdownOptions();
applyFilters();
}
// Extract Thai formatted dates and return standard ISO (YYYY-MM-DD) date
function parseThaiDate(dateStr) {
if (!dateStr) return null;
dateStr = String(dateStr).trim();
if (!dateStr || dateStr === '-' || dateStr === 'null' || dateStr === '0') return null;
// Pattern: "1-พ.ค.-69", "10-พ.ค.-69", "2-มิ.ย.-69" or "01-พ.ค.-2569"
const thMonthRegex = /^(\d+)[-/\s]([ก-์\.]+)[-/\s](\d+)$/;
const match = dateStr.match(thMonthRegex);
if (match) {
const day = parseInt(match[1]);
let monthAbbr = match[2].trim();
// Strip trailing dot if present and re-standardize
monthAbbr = monthAbbr.replace(/\.$/, '') + '.';
let monthIdx = -1;
for (const [key, idx] of Object.entries(thMonths)) {
if (key.startsWith(monthAbbr) || monthAbbr.startsWith(key)) {
monthIdx = idx;
break;
}
}
if (monthIdx !== -1) {
let yr = parseInt(match[3]);
// Standardize year: e.g. 69 represent Buddhist Era 2569
if (yr < 100) {
yr += 2500;
}
const ceYear = yr - 543; // Convert Buddhist Era (BE) to Common Era (CE)
const d = new Date(ceYear, monthIdx, day);
return isNaN(d.getTime()) ? null : d;
}
}
// Fallback: standard JavaScript parser
const fallbackDate = new Date(dateStr);
return isNaN(fallbackDate.getTime()) ? null : fallbackDate;
}
// Clean & synthesize raw columns as required
function cleanAndSynthesizeData() {
const uniqueRows = [];
const seenSignatures = new Set();
cleanedDatasetGlobal = rawDatasetGlobal.map(row => {
// 1. Columns mapping of expected raw column names
const rawDate = row['วันที่'] || '';
const rawRequestor = row['ผู้ขอเบิก'] || '';
const rawItem = row['รายการ'] || '';
const rawCategory = row['หมวดรายจ่าย'] || '';
const rawAmountStr = row['จำนวนเงิน'] || '';
const rawLimitNo = row['เลขที่ตัดยอด'] || '';
const rawFaceNo = row['เลขที่ใบงบหน้า'] || '';
const rawClaimNo = row['เลขที่เบิก'] || '';
const rawReturnDate = row['วันที่ส่งคืน'] || '';
const rawEditItem = row['รายการแก้ไข'] || '';
const rawReceiveDate = row['วันที่รับคืน'] || '';
const rawByNo = row['เลขที่ บย.'] || '';
const rawCreatedBy = row['created_by'] || '';
// 2. Clear Amount strings and force Float conversion
const cleanAmount = parseFloat(String(rawAmountStr).replace(/,/g, '').trim()) || 0.0;
// 3. Translate Dates safely
const claimDateObj = parseThaiDate(rawDate);
const returnDateObj = parseThaiDate(rawReturnDate);
const receiveDateObj = parseThaiDate(rawReceiveDate);
// 4. Derive Status Column dynamically
let status = "รอดำเนินการ";
if (rawClaimNo && String(rawClaimNo).trim() !== '' && String(rawClaimNo).trim() !== '-') {
status = "เบิกจ่ายสำเร็จ";
} else if (rawReturnDate && String(rawReturnDate).trim() !== '' && String(rawReturnDate).trim() !== '-') {
status = "ตีกลับแก้ไข";
}
// Compile structured, cleaned record
return {
'วันที่': claimDateObj,
'ผู้ขอเบิก': String(rawRequestor).trim(),
'รายการ': String(rawItem).trim(),
'หมวดรายจ่าย': String(rawCategory).trim(),
'จำนวนเงิน': cleanAmount,
'เลขที่ตัดยอด': String(rawLimitNo).trim(),
'เลขที่ใบงบหน้า': String(rawFaceNo).trim(),
'เลขที่เบิก': String(rawClaimNo).trim(),
'วันที่ส่งคืน': returnDateObj,
'รายการแก้ไข': String(rawEditItem).trim(),
'วันที่รับคืน': receiveDateObj,
'เลขที่ บย.': String(rawByNo).trim(),
'created_by': String(rawCreatedBy).trim(),
'Status': status
};
}).filter(row => {
// 5. Trim strings & perform 100% exact deduplication
const signature = JSON.stringify(row);
if (seenSignatures.has(signature)) {
return false;
}
seenSignatures.add(signature);
return true;
});
}
// Setup filter panels option tags dynamically
function populateFilterDropdownOptions() {
const depts = new Set();
const categories = new Set();
const months = new Set();
cleanedDatasetGlobal.forEach(row => {
if (row['ผู้ขอเบิก']) depts.add(row['ผู้ขอเบิก']);
if (row['หมวดรายจ่าย']) categories.add(row['หมวดรายจ่าย']);
if (row['วันที่']) {
const mName = thFullMonthNames[row['วันที่'].getMonth()];
const yName = row['วันที่'].getFullYear() + 543; // BE Year
months.add(`${mName} ${yName}`);
}
});
// Populate Dropdown for Departments
const deptSelect = document.getElementById('filter-department');
deptSelect.innerHTML = '<option value="">-- ทั้งหมดทุกคณะ/หน่วยงาน --</option>';
Array.from(depts).sort().forEach(dept => {
deptSelect.innerHTML += `<option value="${dept}">${dept}</option>`;
});
// Populate Dropdown for Categories
const catSelect = document.getElementById('filter-category');
catSelect.innerHTML = '<option value="">-- ทั้งหมดทุกหมวด --</option>';
Array.from(categories).sort().forEach(cat => {
catSelect.innerHTML += `<option value="${cat}">${cat}</option>`;
});
// Populate Dropdown for Month Year strings
const monthSelect = document.getElementById('filter-month');
monthSelect.innerHTML = '<option value="">-- ทั้งหมดทุกเดือน --</option>';
Array.from(months).sort((a,b) => {
// Sort by calendar priority index
const [monthA, yearA] = a.split(' ');
const [monthB, yearB] = b.split(' ');
const idxA = thFullMonthNames.indexOf(monthA) + (parseInt(yearA) * 12);
const idxB = thFullMonthNames.indexOf(monthB) + (parseInt(yearB) * 12);
return idxA - idxB;
}).forEach(mStr => {
monthSelect.innerHTML += `<option value="${mStr}">${mStr}</option>`;
});
}
// Main filter pipeline
function applyFilters() {
const query = document.getElementById('search-box').value.toLowerCase().trim();
const selectedDept = document.getElementById('filter-department').value;
const selectedCat = document.getElementById('filter-category').value;
const selectedMonthStr = document.getElementById('filter-month').value;
filteredDatasetGlobal = cleanedDatasetGlobal.filter(row => {
// Text Search matching
const searchTxt = `${row['รายการ']} ${row['เลขที่เบิก']} ${row['เลขที่ บย.']} ${row['เลขที่ใบงบหน้า']}`.toLowerCase();
const matchesText = !query || searchTxt.includes(query);
// Dropdowns matching
const matchesDept = !selectedDept || row['ผู้ขอเบิก'] === selectedDept;
const matchesCat = !selectedCat || row['หมวดรายจ่าย'] === selectedCat;
let matchesMonth = true;
if (selectedMonthStr && row['วันที่']) {
const rowMName = thFullMonthNames[row['วันที่'].getMonth()];
const rowYName = row['วันที่'].getFullYear() + 543;
const rowMonthStr = `${rowMName} ${rowYName}`;
matchesMonth = (rowMonthStr === selectedMonthStr);
} else if (selectedMonthStr && !row['วันที่']) {
matchesMonth = false;
}
return matchesText && matchesDept && matchesCat && matchesMonth;
});
// Reset current pagination focus to index page 1
currentPage = 1;
// Trigger updates of visual layouts and analytical summaries
updateKpis();
renderTable();
updateCharts();
}
// Reset Filter inputs
function resetFilters() {
document.getElementById('search-box').value = '';
document.getElementById('filter-department').value = '';
document.getElementById('filter-category').value = '';
document.getElementById('filter-month').value = '';
applyFilters();
showFeedback('info', 'ทำความสะอาดคีย์เวิร์ดและล้างตัวกรองทั้งหมดแล้ว');
}
// Calculate and Render accurate numerical indicators (KPI Cards)
function updateKpis() {
const countBadge = document.getElementById('data-count-badge');
countBadge.innerText = `${filteredDatasetGlobal.length} / ${cleanedDatasetGlobal.length} รายการ`;
let totalClaims = filteredDatasetGlobal.length;
let totalAmount = 0.0;
let successClaims = 0;
let successAmount = 0.0;
let returnedClaims = 0;
let returnedAmount = 0.0;
filteredDatasetGlobal.forEach(row => {
totalAmount += row['จำนวนเงิน'];
if (row['Status'] === 'เบิกจ่ายสำเร็จ') {
successClaims++;
successAmount += row['จำนวนเงิน'];
} else if (row['Status'] === 'ตีกลับแก้ไข') {
returnedClaims++;
returnedAmount += row['จำนวนเงิน'];
}
});
// Render KPI 1
document.getElementById('kpi-total-claims').innerText = totalClaims.toLocaleString();
document.getElementById('kpi-total-amount').innerText = `฿${totalAmount.toLocaleString('th-TH', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
// Render KPI 2
document.getElementById('kpi-success-claims').innerText = successClaims.toLocaleString();
document.getElementById('kpi-success-amount').innerText = `฿${successAmount.toLocaleString('th-TH', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
// Render KPI 3
document.getElementById('kpi-returned-claims').innerText = returnedClaims.toLocaleString();
document.getElementById('kpi-returned-amount').innerText = `฿${returnedAmount.toLocaleString('th-TH', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
// Render KPI 4 (Ratio metrics)
const successRate = totalClaims > 0 ? (successClaims / totalClaims) * 100 : 0.0;
const returnedRate = totalClaims > 0 ? (returnedClaims / totalClaims) * 100 : 0.0;
document.getElementById('kpi-rate-success').innerText = `${successRate.toFixed(2)}%`;
document.getElementById('kpi-rate-returned').innerText = `${returnedRate.toFixed(2)}%`;
const kpiRateBar = document.getElementById('kpi-rate-bar');
kpiRateBar.style.width = `${successRate}%`;
}
// Render Data Table rows
function renderTable() {
const tableBody = document.getElementById('table-body');
const emptyState = document.getElementById('table-empty-state');
tableBody.innerHTML = '';
if (filteredDatasetGlobal.length === 0) {
emptyState.classList.remove('hidden');
document.getElementById('table-pagination-info').innerText = 'กำลังแสดงรายการ 0 - 0 จากทั้งหมด 0 รายการ';
togglePaginationButtons(0);
return;
}
emptyState.classList.add('hidden');
// Apply active column sorting
sortDataset();
// Slicing algorithm per active page
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredDatasetGlobal.length);
const pageRecords = filteredDatasetGlobal.slice(startIndex, endIndex);
pageRecords.forEach((row, i) => {
const globalIndex = startIndex + i + 1;
const formattedDate = row['วันที่'] ? `${row['วันที่'].getDate()} ${thFullMonthNames[row['วันที่'].getMonth()]} ${row['วันที่'].getFullYear() + 543}` : '-';
const formattedAmt = `฿${row['จำนวนเงิน'].toLocaleString('th-TH', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
// Status Badge class resolution
let badgeClass = 'bg-slate-900 text-slate-400 border-slate-700';
if (row['Status'] === 'เบิกจ่ายสำเร็จ') badgeClass = 'bg-emerald-400/15 text-emerald-400 border-emerald-400/30';
else if (row['Status'] === 'ตีกลับแก้ไข') badgeClass = 'bg-red-400/15 text-red-400 border-red-400/30';
else badgeClass = 'bg-amber-400/15 text-amber-400 border-amber-400/30';
const tr = document.createElement('tr');
tr.className = "hover:bg-slate-700/45 transition-colors border-b border-slate-700/50 even:bg-slate-800/25";
tr.innerHTML = `
<td class="py-3 px-4 text-center font-mono text-slate-500">${globalIndex}</td>
<td class="py-3 px-4 whitespace-nowrap text-slate-300 font-mono text-[11px]">${formattedDate}</td>
<td class="py-3 px-4 font-semibold text-slate-200 truncate max-w-[120px]" title="${row['ผู้ขอเบิก']}">${row['ผู้ขอเบิก']}</td>
<td class="py-3 px-4 text-slate-300 max-w-xs break-words" title="${row['รายการ']}">${row['รายการ']}</td>
<td class="py-3 px-4 text-slate-300 whitespace-nowrap"><span class="px-2 py-0.5 bg-slate-900 border border-slate-700 rounded text-[11px]">${row['หมวดรายจ่าย']}</span></td>
<td class="py-3 px-4 text-right font-mono font-bold text-slate-100">${formattedAmt}</td>
<td class="py-3 px-4 text-center whitespace-nowrap">
<span class="px-2.5 py-1 rounded text-[10px] font-bold border ${badgeClass}">
${row['Status']}
</span>
</td>
<td class="py-3 px-4 text-right font-mono text-cyan-400 text-[11px]">${row['เลขที่เบิก'] || '-'}</td>
<td class="py-3 px-4 text-right font-mono text-slate-400 text-[11px]">${row['เลขที่ บย.'] || '-'}</td>
`;
tableBody.appendChild(tr);
});
// Update Pagination display indices
document.getElementById('table-pagination-info').innerText = `กำลังแสดงรายการ ${startIndex + 1} - ${endIndex} จากทั้งหมด ${filteredDatasetGlobal.length} รายการ`;
document.getElementById('table-current-page').innerText = currentPage;
togglePaginationButtons(Math.ceil(filteredDatasetGlobal.length / pageSize));
}
// Dynamic State based Pagination Controls
function togglePaginationButtons(maxPage) {
document.getElementById('btn-page-first').disabled = (currentPage === 1);
document.getElementById('btn-page-prev').disabled = (currentPage === 1);
document.getElementById('btn-page-next').disabled = (currentPage >= maxPage || maxPage === 0);
document.getElementById('btn-page-last').disabled = (currentPage >= maxPage || maxPage === 0);
}
// Handle Table Sorting
function toggleSort(columnName) {
if (currentSortColumn === columnName) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = columnName;
currentSortDirection = 'asc';
}
// Reset headers icons
const headers = ['วันที่', 'ผู้ขอเบิก', 'รายการ', 'หมวดรายจ่าย', 'จำนวนเงิน', 'Status', 'เลขที่เบิก'];
headers.forEach(h => {
const icon = document.getElementById(`sort-icon-${h}`);
if (icon) {
icon.className = "fa-solid fa-sort ml-1 text-slate-600";
}
});
// Set current sorted header icon
const currentIcon = document.getElementById(`sort-icon-${currentSortColumn}`);
if (currentIcon) {
currentIcon.className = currentSortDirection === 'asc' ? "fa-solid fa-sort-up ml-1 text-indigo-400" : "fa-solid fa-sort-down ml-1 text-indigo-400";
}
currentPage = 1;
renderTable();
}
// Implementation of In-Memory Sorting
function sortDataset() {
filteredDatasetGlobal.sort((a, b) => {
let valA = a[currentSortColumn];
let valB = b[currentSortColumn];
// Deal with Date objects
if (valA instanceof Date) {
valA = valA.getTime();
valB = valB ? valB.getTime() : 0;
}
if (valA === undefined || valA === null) valA = '';
if (valB === undefined || valB === null) valB = '';
if (typeof valA === 'string') {
return currentSortDirection === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
} else {
return currentSortDirection === 'asc' ? valA - valB : valB - valA;
}
});
}
// Synthesis & Chart Initialization / Regeneration
function updateCharts() {
const fontConfig = { family: 'Sarabun', size: 11 };
const gridColor = 'rgba(255, 255, 255, 0.05)';
// Data Prep for Chart 1: Line Chart (Monthly trend)
// Generate monthly groupings dynamically
const monthMap = {};
filteredDatasetGlobal.forEach(row => {
if (!row['วันที่']) return;
const mIdx = row['วันที่'].getMonth();
const yVal = row['วันที่'].getFullYear();
const key = `${yVal}-${String(mIdx + 1).padStart(2, '0')}`; // ISO Key: YYYY-MM
const displayKey = `${thFullMonthNames[mIdx]} ${yVal + 543}`;
if (!monthMap[key]) {
monthMap[key] = { display: displayKey, totalSent: 0, success: 0, returned: 0 };
}
monthMap[key].totalSent++;
if (row['Status'] === 'เบิกจ่ายสำเร็จ') monthMap[key].success++;
else if (row['Status'] === 'ตีกลับแก้ไข') monthMap[key].returned++;
});
// Sort keys chronologically
const sortedMonthKeys = Object.keys(monthMap).sort();
const labelsLine = sortedMonthKeys.map(k => monthMap[k].display);
const dataSent = sortedMonthKeys.map(k => monthMap[k].totalSent);
const dataSuccess = sortedMonthKeys.map(k => monthMap[k].success);
const dataReturned = sortedMonthKeys.map(k => monthMap[k].returned);
// Destroy existing line chart instance to support seamless updates
if (trendChartInstance) {
trendChartInstance.destroy();
}
const ctxLine = document.getElementById('lineTrendChart').getContext('2d');
trendChartInstance = new Chart(ctxLine, {
type: 'line',
data: {
labels: labelsLine.length > 0 ? labelsLine : ["ไม่มีข้อมูลเดือน"],
datasets: [
{
label: 'ส่งเอกสารทั้งหมด (ชุด)',
data: dataSent,
borderColor: '#6366F1', // Indigo-500
backgroundColor: 'rgba(99, 102, 241, 0.1)',
borderWidth: 3,
tension: 0.35,
fill: true,
pointRadius: 4,
pointBackgroundColor: '#6366F1'
},
{
label: 'เบิกจ่ายสำเร็จ (ชุด)',
data: dataSuccess,
borderColor: '#34D399', // Emerald-400
backgroundColor: 'transparent',
borderWidth: 2,
tension: 0.25,
pointRadius: 4,
pointBackgroundColor: '#34D399'
},
{
label: 'ตีกลับแก้ไข (ชุด)',
data: dataReturned,
borderColor: '#F87171', // Red-400
backgroundColor: 'transparent',
borderWidth: 2,
tension: 0.25,
pointRadius: 4,
pointBackgroundColor: '#F87171'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#94A3B8',
font: fontConfig,
padding: 15
}
},
tooltip: {
backgroundColor: '#1E293B',
titleColor: '#F1F5F9',
bodyColor: '#F1F5F9',
borderColor: '#334155',
borderWidth: 1,
titleFont: { family: 'Sarabun', size: 12, weight: 'bold' },
bodyFont: { family: 'JetBrains Mono', size: 11 }
}
},
scales: {
x: {
grid: { color: gridColor },
ticks: { color: '#94A3B8', font: fontConfig }
},
y: {
grid: { color: gridColor },
ticks: { color: '#94A3B8', font: fontConfig, stepSize: 1 }
}
}
}
});
// Data Prep for Chart 2: Stacked Bar Chart for Faculty BottleNecks
const deptMap = {};
filteredDatasetGlobal.forEach(row => {
const dept = row['ผู้ขอเบิก'] || 'ไม่ระบุ';
if (!deptMap[dept]) {
deptMap[dept] = { success: 0, returned: 0, pending: 0 };
}
if (row['Status'] === 'เบิกจ่ายสำเร็จ') {
deptMap[dept].success++;
} else if (row['Status'] === 'ตีกลับแก้ไข') {
deptMap[dept].returned++;
} else {
deptMap[dept].pending++;
}
});
const labelsDept = Object.keys(deptMap);
const deptSuccess = labelsDept.map(d => deptMap[d].success);
const deptReturned = labelsDept.map(d => deptMap[d].returned);
const deptPending = labelsDept.map(d => deptMap[d].pending);
if (deptChartInstance) {
deptChartInstance.destroy();
}
const ctxDept = document.getElementById('stackedBarChart').getContext('2d');
deptChartInstance = new Chart(ctxDept, {
type: 'bar',
data: {
labels: labelsDept.length > 0 ? labelsDept : ["ไม่มีข้อมูลคณะ"],
datasets: [
{
label: 'เบิกจ่ายสำเร็จ (ชุด)',
data: deptSuccess,
backgroundColor: '#34D399', // Emerald-400
borderRadius: 4
},
{
label: 'ตีกลับแก้ไข (ชุด)',
data: deptReturned,
backgroundColor: '#F87171', // Red-400
borderRadius: 4
},
{
label: 'รอดำเนินการ (ชุด)',
data: deptPending,
backgroundColor: '#FBBF24', // Amber-400
borderRadius: 4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#94A3B8',
font: fontConfig,
padding: 15
}
},
tooltip: {
backgroundColor: '#1E293B',
titleColor: '#F1F5F9',
bodyColor: '#F1F5F9',
borderColor: '#334155',
borderWidth: 1,
titleFont: { family: 'Sarabun', size: 12, weight: 'bold' },
bodyFont: { family: 'JetBrains Mono', size: 11 }
}
},
scales: {
x: {
stacked: true,
grid: { color: gridColor },
ticks: {
color: '#94A3B8',
font: fontConfig,
maxRotation: 45,
minRotation: 45
}
},
y: {
stacked: true,
grid: { color: gridColor },
ticks: { color: '#94A3B8', font: fontConfig, stepSize: 1 }
}
}
}
});
}
// Update the visual status badge next to upload zone
function updateDataSourceBadge(text) {
document.getElementById('data-source-badge').innerText = text;
}
// Export Entire Cleaned & Prepped Dataset to Premium .xlsx sheet
function exportCleanedDataToExcel() {
if (filteredDatasetGlobal.length === 0) {
showFeedback('danger', 'ไม่มีข้อมูลสำหรับส่งออกเอกสาร Excel ในขณะนี้');
return;
}
const exportRows = filteredDatasetGlobal.map((row, idx) => {
return {
'ลำดับ': idx + 1,
'วันที่ส่งเบิก': row['วันที่'] ? `${row['วันที่'].getFullYear() + 543}-${String(row['วันที่'].getMonth() + 1).padStart(2, '0')}-${String(row['วันที่'].getDate()).padStart(2, '0')}` : '',
'ผู้ขอเบิก (คณะ)': row['ผู้ขอเบิก'],
'รายการ': row['รายการ'],
'หมวดรายจ่าย': row['หมวดรายจ่าย'],
'จำนวนเงิน (บาท)': row['จำนวนเงิน'],
'สถานะ': row['Status'],
'เลขที่เบิก': row['เลขที่เบิก'],
'เลขที่ตัดยอด': row['เลขที่ตัดยอด'],
'เลขที่ใบงบหน้า': row['เลขที่ใบงบหน้า'],
'วันที่ส่งคืน': row['วันที่ส่งคืน'] ? `${row['วันที่ส่งคืน'].getFullYear() + 543}-${String(row['วันที่ส่งคืน'].getMonth() + 1).padStart(2, '0')}-${String(row['วันที่ส่งคืน'].getDate()).padStart(2, '0')}` : '',
'รายการแก้ไข': row['รายการแก้ไข'],
'วันที่รับคืน': row['วันที่รับคืน'] ? `${row['วันที่รับคืน'].getFullYear() + 543}-${String(row['วันที่รับคืน'].getMonth() + 1).padStart(2, '0')}-${String(row['วันที่รับคืน'].getDate()).padStart(2, '0')}` : '',
'เลขที่ บย.': row['เลขที่ บย.'],
'ผู้บันทึก': row['created_by']
};
});
const worksheet = XLSX.utils.json_to_sheet(exportRows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Budget Cleaned Data");
// Format numeric values to look clean in excel
const range = XLSX.utils.decode_range(worksheet['!ref']);
for (let R = range.s.r + 1; R <= range.e.r; ++R) {
const amtCellRef = XLSX.utils.encode_cell({r: R, c: 5}); // 'จำนวนเงิน (บาท)' column
if (worksheet[amtCellRef]) {
worksheet[amtCellRef].t = 'n'; // Numeric format
worksheet[amtCellRef].z = '#,##0.00';
}
}
XLSX.writeFile(workbook, "มรพส_ทะเบียนคุมหลักฐานการเบิก_Cleaned.xlsx");
showFeedback('success', 'ดาวน์โหลดไฟล์จำแนกข้อมูลสะอาด (.xlsx) สำเร็จเรียบร้อย');
}
// Download Chart Snapshots as base64 images
function downloadChartSnapshots() {
if (!trendChartInstance || !deptChartInstance) {
showFeedback('danger', 'ไม่พบความพร้อมของระบบสร้างภาพสถิติในขณะนี้');
return;
}
try {
// Download Line Trend
const lineImgUrl = trendChartInstance.toBase64Image();
const aLine = document.createElement('a');
aLine.href = lineImgUrl;
aLine.download = 'มรพส_สถิติแนวโน้มการเบิกรายเดือน.png';
aLine.click();
// Download Dept Bar
const deptImgUrl = deptChartInstance.toBase64Image();
const aDept = document.createElement('a');
aDept.href = deptImgUrl;
aDept.download = 'มรพส_สถิติแยกตามคณะ.png';
aDept.click();
showFeedback('success', 'ดาวน์โหลดภาพวิเคราะห์แผนภูมิความละเอียดสูง (.png) ทั้งสองภาพสำเร็จ');
} catch (err) {
showFeedback('danger', 'ไม่สามารถบันทึกรูปกราฟได้เนื่องจากข้อกำหนด Security Sandbox');
}
}
// Show User Feedback Alerts
function showFeedback(type, message) {
const banner = document.getElementById('feedback-alert');
const icon = document.getElementById('feedback-icon');
const msg = document.getElementById('feedback-message');
banner.className = "px-4 py-3.5 rounded-lg border flex items-center justify-between transition-opacity duration-300";
if (type === 'success') {
banner.classList.add('bg-emerald-400/15', 'text-emerald-400', 'border-emerald-400/30');
icon.innerHTML = '<i class="fa-solid fa-circle-check"></i>';
} else if (type === 'danger') {
banner.classList.add('bg-red-400/15', 'text-red-400', 'border-red-400/30');
icon.innerHTML = '<i class="fa-solid fa-circle-exclamation"></i>';
} else {
banner.classList.add('bg-indigo-400/15', 'text-indigo-400', 'border-indigo-400/30');
icon.innerHTML = '<i class="fa-solid fa-circle-info"></i>';
}
msg.innerText = message;
banner.classList.remove('hidden');
// Autohide after 6 seconds
setTimeout(hideFeedbackAlert, 6000);
}
// Hide Feedback Alerts
function hideFeedbackAlert() {
const banner = document.getElementById('feedback-alert');
banner.classList.add('hidden');
}
</script>
</body>
</html>