Spaces:
Running
Running
| <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> |