KPI_dashboard1 / index.html
AmmaPP's picture
Upload index.html
fd689e2 verified
Raw
History Blame Contribute Delete
63.5 kB
<!DOCTYPE html>
<html lang="th" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ระบบติดตามตัวชี้วัดสำนักงานอธิการบดี (สนอ.) ประจำปีงบประมาณ 2568</title>
<!-- Google Fonts: Prompt & Sarabun -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;500;600;700&family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- SheetJS (xlsx.full.min.js) -->
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
dark: '#1b4332',
primary: '#2d6a4f',
medium: '#40916c',
accent: '#52b788',
light: '#74c69d',
cream: '#f4f6f0',
paper: '#f8f9fa'
}
},
fontFamily: {
sans: ['Prompt', 'Sarabun', 'sans-serif'],
body: ['Sarabun', 'sans-serif']
}
}
}
}
</script>
<style>
body {
font-family: 'Prompt', sans-serif;
background-color: #f4f6f0;
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Sticky table styling */
.table-sticky-header th {
position: sticky;
top: 0;
z-index: 10;
}
</style>
</head>
<body class="min-h-screen flex flex-col text-slate-800">
<!-- Header Navigation -->
<header class="bg-brand-dark text-white shadow-md border-b-4 border-brand-accent">
<div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center space-x-4">
<div class="p-2.5 bg-brand-primary rounded-lg border border-brand-light/30">
<i class="fa-solid fa-chart-line text-2xl text-brand-light"></i>
</div>
<div>
<h1 class="text-xl md:text-2xl font-bold tracking-tight">ระบบติดตามตัวชี้วัดสำนักงานอธิการบดี (สนอ.)</h1>
<p class="text-xs md:text-sm text-brand-light/90">ปีงบประมาณ พ.ศ. 2568 | แดชบอร์ดเปรียบเทียบผลสัมฤทธิ์ (Best Practice Finder)</p>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-brand-primary border border-brand-light/20 text-white">
<span class="w-2 h-2 mr-2 rounded-full bg-emerald-400 animate-pulse"></span>
สถานะการทำงาน: ท้องถิ่น (Local Only)
</span>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="flex-grow max-w-7xl w-full mx-auto px-4 py-6 sm:px-6 lg:px-8">
<!-- ALERT BOX (Toast Alternative) -->
<div id="toast-notification" class="hidden fixed bottom-5 right-5 z-50 max-w-md bg-white border-l-4 border-emerald-500 rounded-r-lg shadow-2xl p-4 transition-all duration-300 transform translate-y-10">
<div class="flex items-start">
<div class="flex-shrink-0">
<i id="toast-icon" class="fa-solid fa-circle-check text-emerald-500 text-lg"></i>
</div>
<div class="ml-3">
<p id="toast-title" class="text-sm font-semibold text-slate-900"></p>
<p id="toast-message" class="text-xs text-slate-600 mt-1"></p>
</div>
<button onclick="closeNotification()" class="ml-auto flex-shrink-0 text-slate-400 hover:text-slate-600 focus:outline-none" aria-label="ปิดแจ้งเตือน">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<!-- UPLOAD PLACEHOLDER STATE -->
<div id="upload-placeholder-container" class="space-y-6">
<div class="bg-white rounded-2xl border border-slate-200 p-8 text-center shadow-sm max-w-3xl mx-auto my-8">
<div class="w-20 h-20 bg-brand-cream text-brand-primary rounded-full flex items-center justify-center mx-auto mb-6">
<i class="fa-solid fa-file-excel text-4xl"></i>
</div>
<h2 class="text-2xl font-bold text-brand-dark mb-2">ยินดีต้อนรับสู่ระบบติดตามตัวชี้วัด สนอ.</h2>
<p class="text-slate-600 mb-6 max-w-lg mx-auto">
กรุณาอัปโหลดไฟล์รายงานผลตัวชี้วัด (Excel หรือ CSV) เพื่อตรวจสอบเปรียบเทียบคะแนน ค้นหากลุ่มกองงานที่มีคะแนนดีที่สุด และส่งออกรายงานที่สมบูรณ์
</p>
<!-- File Drag and Drop Box -->
<div id="drop-zone" class="border-2 border-dashed border-brand-medium/30 hover:border-brand-primary rounded-xl p-8 bg-brand-cream/30 transition duration-200 cursor-pointer mb-6" aria-label="พื้นที่วางไฟล์ข้อมูล">
<input type="file" id="file-upload" class="hidden" accept=".xlsx, .xls, .csv">
<div class="space-y-2">
<p class="text-slate-700 font-medium">ลากไฟล์มาวางที่นี่ หรือ <span class="text-brand-primary underline hover:text-brand-dark">เลือกไฟล์จากคอมพิวเตอร์</span></p>
<p class="text-xs text-slate-500">รองรับไฟล์ Excel (.xlsx, .xls) หรือ CSV ที่มีรูปแบบตารางเปรียบเทียบผลคะแนน</p>
</div>
</div>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<button id="btn-load-sample" class="px-6 py-3 bg-brand-primary hover:bg-brand-dark text-white rounded-lg shadow font-semibold transition flex items-center justify-center gap-2" aria-label="โหลดข้อมูลตัวอย่างสำหรับทดสอบระบบ">
<i class="fa-solid fa-wand-magic-sparkles"></i>
ใช้ข้อมูลตัวอย่างเพื่อทดสอบทันที
</button>
</div>
<div class="mt-8 pt-6 border-t border-slate-150 text-left">
<h3 class="text-sm font-semibold text-brand-dark mb-2"><i class="fa-solid fa-circle-info mr-1"></i> โครงสร้างข้อมูลที่ระบบต้องการ</h3>
<ul class="text-xs text-slate-500 space-y-1 list-disc pl-5">
<li>มีคอลัมน์ลำดับแรกเป็นชื่อหรือหัวข้อตัวชี้วัด</li>
<li>ตัวยุทธศาสตร์หลักหลักระบุขึ้นต้นด้วยเลขหลักเดี่ยว เช่น <code class="bg-slate-100 px-1 py-0.5 rounded">1 : ...</code> หรือ <code class="bg-slate-100 px-1 py-0.5 rounded">2 : ...</code></li>
<li>ตัวชี้วัดย่อยระบุเป็นทศนิยมในคอลัมน์แรก เช่น <code class="bg-slate-100 px-1 py-0.5 rounded">1.1</code>, <code class="bg-slate-100 px-1 py-0.5 rounded">1.2</code>, <code class="bg-slate-100 px-1 py-0.5 rounded">2.1</code></li>
<li>คอลัมน์ถัดไปประกอบด้วยคะแนนผลการดำเนินงานแบ่งตามกองงาน: สนอ., กก., กนผ., กบค., กบศ., กพน., กค.</li>
</ul>
</div>
</div>
</div>
<!-- ACTIVE DASHBOARD VIEW (HIDDEN BY DEFAULT) -->
<div id="dashboard-active-view" class="hidden space-y-6">
<!-- Dynamic Controls and File Reset Bar -->
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-4 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-3">
<span class="p-2 bg-emerald-100 text-emerald-800 rounded-lg">
<i class="fa-solid fa-file-circle-check"></i>
</span>
<div>
<p class="text-xs text-slate-500">ไฟล์ที่กำลังประมวลผล</p>
<p id="loaded-file-name" class="text-sm font-semibold text-slate-800">sample_data_สนอ68.xlsx</p>
</div>
</div>
<div class="flex items-center gap-2 flex-wrap justify-end">
<button id="btn-export-excel" class="px-4 py-2 bg-brand-primary hover:bg-brand-dark text-white rounded-lg text-sm font-semibold shadow transition flex items-center gap-1.5" aria-label="ส่งออกผลการวิเคราะห์เป็น Excel">
<i class="fa-solid fa-file-arrow-down"></i>
ส่งออกคลีนเนอร์ Excel
</button>
<button id="btn-reset-data" class="px-4 py-2 bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 rounded-lg text-sm font-semibold transition flex items-center gap-1.5" aria-label="อัปโหลดไฟล์ใหม่">
<i class="fa-solid fa-arrow-rotate-left"></i>
ล้างข้อมูล / อัปโหลดใหม่
</button>
</div>
</div>
<!-- SECTION A: KPI SUMMARY CARDS -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Card 1: Overall Average -->
<div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-4">
<div class="p-3 bg-emerald-50 text-brand-primary rounded-xl">
<i class="fa-solid fa-award text-3xl"></i>
</div>
<div>
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider">คะแนนเฉลี่ยภาพรวม (สนอ.)</p>
<p id="stat-overall-avg" class="text-2xl font-bold text-brand-dark mt-0.5">-</p>
<p class="text-xs text-slate-400 mt-0.5">เป้าหมายตามเกณฑ์ 4.50+</p>
</div>
</div>
<!-- Card 2: Count of KPIs -->
<div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-4">
<div class="p-3 bg-teal-50 text-teal-700 rounded-xl">
<i class="fa-solid fa-list-check text-3xl"></i>
</div>
<div>
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider">จำนวนตัวชี้วัดย่อยทั้งหมด</p>
<p id="stat-total-kpis" class="text-2xl font-bold text-teal-800 mt-0.5">-</p>
<p class="text-xs text-slate-400 mt-0.5">ตัวชี้วัดที่ผ่านการจำแนกประเภท</p>
</div>
</div>
<!-- Card 3: Count of Evaluated Units -->
<div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm flex items-center space-x-4">
<div class="p-3 bg-amber-50 text-amber-700 rounded-xl">
<i class="fa-solid fa-sitemap text-3xl"></i>
</div>
<div>
<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider">จำนวนกลุ่มงานที่ประเมิน</p>
<p id="stat-total-depts" class="text-2xl font-bold text-amber-800 mt-0.5">7</p>
<p class="text-xs text-slate-400 mt-0.5">สำนักงาน/กอง/กลุ่มงาน</p>
</div>
</div>
<!-- Card 4: Best Practice Unit -->
<div class="bg-white p-5 rounded-xl border border-emerald-200 bg-emerald-50/20 shadow-sm flex items-center space-x-4">
<div class="p-3 bg-brand-primary text-white rounded-xl">
<i class="fa-solid fa-trophy text-3xl"></i>
</div>
<div>
<p class="text-xs font-semibold text-brand-primary uppercase tracking-wider">สุดยอดหน่วยงาน (Best Practice)</p>
<p id="stat-best-unit" class="text-2xl font-bold text-brand-dark mt-0.5">-</p>
<p id="stat-best-unit-score" class="text-xs text-slate-600 mt-0.5">คะแนนเฉลี่ยสูงสุดข้ามกองงาน</p>
</div>
</div>
</div>
<!-- SECTION B: ADVANCED FILTERS & LIVE COUNTER BADGE -->
<div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div class="flex items-center space-x-2">
<i class="fa-solid fa-filter text-brand-primary"></i>
<h3 class="font-bold text-brand-dark text-base">ตัวกรองและตัวเลือกขั้นสูง</h3>
<span id="active-badge-counter" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-brand-cream text-brand-primary border border-brand-accent/20">
แสดงผล - จาก - ตัวชี้วัด
</span>
</div>
<div class="flex flex-wrap items-center gap-3 w-full md:w-auto">
<!-- Filter 1: Strategic Pillar -->
<div class="w-full sm:w-64">
<label for="filter-theme" class="sr-only">เลือกประเด็นยุทธศาสตร์หลัก</label>
<select id="filter-theme" class="w-full text-sm bg-slate-50 border border-slate-200 rounded-lg p-2.5 focus:ring-brand-primary focus:border-brand-primary outline-none">
<option value="all">เลือกประเด็นยุทธศาสตร์ทั้งหมด</option>
</select>
</div>
<!-- Filter 2: Department Focus -->
<div class="w-full sm:w-48">
<label for="filter-dept" class="sr-only">เลือกกลุ่มงานเพื่อเน้นคะแนน</label>
<select id="filter-dept" class="w-full text-sm bg-slate-50 border border-slate-200 rounded-lg p-2.5 focus:ring-brand-primary focus:border-brand-primary outline-none">
<option value="all">เปรียบเทียบทุกกองงาน</option>
</select>
</div>
</div>
</div>
</div>
<!-- SECTION C: INTERACTIVE DATA VISUALIZATIONS -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Chart 1: Horizontal Bar Chart -->
<div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm lg:col-span-5 flex flex-col justify-between">
<div>
<div class="flex justify-between items-center mb-4">
<h4 class="font-bold text-brand-dark text-sm sm:text-base flex items-center gap-1.5">
<i class="fa-solid fa-chart-bar"></i>
เปรียบเทียบคะแนนเฉลี่ยภาพรวมรายหน่วยงาน
</h4>
<button onclick="downloadChart('chart-overall-avg')" class="p-1.5 text-slate-400 hover:text-brand-primary transition" title="ดาวน์โหลดแผนภูมิเป็น PNG" aria-label="ดาวน์โหลดแผนภูมิคะแนนเฉลี่ยภาพรวม">
<i class="fa-solid fa-download"></i>
</button>
</div>
<div class="relative h-64 sm:h-72">
<canvas id="chart-overall-avg"></canvas>
</div>
</div>
<p class="text-xs text-slate-400 mt-4 italic border-t pt-2">
* คะแนนเฉลี่ยคำนวณจากตัวชี้วัดย่อยทั้งหมด (ไม่รวมประเด็นยุทธศาสตร์หลักและผลคะแนนรวม)
</p>
</div>
<!-- Chart 2: Radar Chart / Spider Chart -->
<div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm lg:col-span-7 flex flex-col justify-between">
<div>
<div class="flex justify-between items-center mb-4">
<h4 class="font-bold text-brand-dark text-sm sm:text-base flex items-center gap-1.5">
<i class="fa-solid fa-chart-pie"></i>
เรดาร์ชาร์ตเปรียบเทียบคะแนนตามประเด็นยุทธศาสตร์หลัก
</h4>
<button onclick="downloadChart('chart-themes-radar')" class="p-1.5 text-slate-400 hover:text-brand-primary transition" title="ดาวน์โหลดแผนภูมิเป็น PNG" aria-label="ดาวน์โหลดแผนภูมิเรดาร์ยุทธศาสตร์">
<i class="fa-solid fa-download"></i>
</button>
</div>
<div class="relative h-64 sm:h-72 flex justify-center items-center">
<canvas id="chart-themes-radar"></canvas>
</div>
</div>
<p class="text-xs text-slate-400 mt-4 border-t pt-2">
<i class="fa-solid fa-info-circle mr-1 text-slate-500"></i>
คลิกกลุ่มงานที่ Legend เพื่อเปิด/ปิดการแสดงเส้นเรดาร์ ช่วยให้วิเคราะห์ข้อมูลเทียบระหว่าง 2-3 กองงานได้ง่ายขึ้น
</p>
</div>
</div>
<!-- SECTION D: INTERACTIVE DATA TABLE -->
<div class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div class="p-5 border-b border-slate-100 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-brand-cream/10">
<div>
<h4 class="font-bold text-brand-dark text-base flex items-center gap-1.5">
<i class="fa-solid fa-table-list"></i>
ตารางแจกแจงระดับคะแนนและค้นหา Best Practice ในแต่ละตัวชี้วัด
</h4>
<p class="text-xs text-slate-500 mt-1">
ไฮไลต์เซลล์ด้วยสีเขียว <span class="bg-emerald-100 text-emerald-800 px-1 rounded">🏆 Best</span> แสดงถึงผู้ทำผลงานคะแนนได้สูงสุดในตัวชี้วัดนั้นๆ
</p>
</div>
<!-- Color scale legend -->
<div class="flex flex-wrap gap-2 text-xs">
<span class="flex items-center gap-1">
<span class="w-3.5 h-3.5 bg-[#d1fae5] border border-emerald-300 rounded"></span>
ดีเยี่ยม (>= 4.50)
</span>
<span class="flex items-center gap-1">
<span class="w-3.5 h-3.5 bg-[#fef3c7] border border-amber-300 rounded"></span>
ปานกลาง (3.50 - 4.49)
</span>
<span class="flex items-center gap-1">
<span class="w-3.5 h-3.5 bg-[#ffe4e6] border border-rose-300 rounded"></span>
ต้องปรับปรุง (&lt; 3.50)
</span>
</div>
</div>
<!-- Table Container -->
<div class="overflow-x-auto max-h-[500px] custom-scrollbar">
<table class="w-full text-left border-collapse table-sticky-header">
<thead>
<tr class="bg-brand-dark text-white text-xs sm:text-sm font-semibold uppercase">
<th class="p-3 bg-brand-dark left-0 z-20 min-w-[280px]">ตัวชี้วัด</th>
<th class="p-3 text-center bg-brand-primary min-w-[80px]">สนอ. (หลัก)</th>
<th class="p-3 text-center min-w-[80px]">กก.</th>
<th class="p-3 text-center min-w-[80px]">กนผ.</th>
<th class="p-3 text-center min-w-[80px]">กบค.</th>
<th class="p-3 text-center min-w-[80px]">กบศ.</th>
<th class="p-3 text-center min-w-[80px]">กพน.</th>
<th class="p-3 text-center min-w-[80px]">กค.</th>
</tr>
</thead>
<tbody id="kpi-table-body" class="divide-y divide-slate-100 text-xs sm:text-sm">
<!-- Dynamic Table Rows Injection -->
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Footer Area -->
<footer class="bg-brand-dark text-slate-300 py-6 border-t border-brand-primary/50 mt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="text-center md:text-left">
<p class="text-sm font-semibold">กองนโยบายและแผน สำนักงานอธิการบดี</p>
<p class="text-xs text-slate-400 mt-1">© 2026 สงวนลิขสิทธิ์ตามพระราชบัญญัติระบบคอมพิวเตอร์และนโยบายภายใน</p>
</div>
<div class="flex space-x-4 text-xs text-slate-400">
<a href="#" class="hover:text-brand-light transition" aria-label="คู่มือผู้ใช้">คู่มือการใช้งาน</a>
<span>|</span>
<a href="#" class="hover:text-brand-light transition" aria-label="นโยบายความเสถียรข้อมูล">นโยบายความมั่นคงปลอดภัย</a>
</div>
</div>
</footer>
<!-- App JavaScript Logic -->
<script>
// Global variables for dashboard state
const CONFIG = {
appId: typeof __app_id !== 'undefined' ? __app_id : 'kpi-tracker-68',
columns: ['ตัวชี้วัด', 'สนอ.', 'กก.', 'กนผ.', 'กบค.', 'กบศ.', 'กพน.', 'กค.'],
departments: ['สนอ.', 'กก.', 'กนผ.', 'กบค.', 'กบศ.', 'กพน.', 'กค.'],
themePattern: /^\d+\s*:/,
kpiPattern: /^\d+\.\d+/
};
const state = {
rawRows: [], // [{ type: 'theme'|'kpi', key: string, name: string, scores: { dept: float } }]
filteredRows: [], // Sliced/Diced kpi rows based on active dropdowns
themes: [], // Unique Array of String: ['1 : การวิจัย...', '2 : การสอน...']
activeThemeFilter: 'all',
activeDeptFilter: 'all',
charts: {
overallAvg: null,
themesRadar: null
},
fileName: ''
};
// On Document Loaded
document.addEventListener('DOMContentLoaded', () => {
initEventListeners();
});
// Initialize user interaction events
function initEventListeners() {
const fileUpload = document.getElementById('file-upload');
const dropZone = document.getElementById('drop-zone');
const btnLoadSample = document.getElementById('btn-load-sample');
const btnResetData = document.getElementById('btn-reset-data');
const btnExportExcel = document.getElementById('btn-export-excel');
const filterTheme = document.getElementById('filter-theme');
const filterDept = document.getElementById('filter-dept');
// Drag and drop events
dropZone.addEventListener('click', () => fileUpload.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-brand-primary', 'bg-brand-cream/50');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-brand-primary', 'bg-brand-cream/50');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-brand-primary', 'bg-brand-cream/50');
if (e.dataTransfer.files.length) {
processUploadedFile(e.dataTransfer.files[0]);
}
});
fileUpload.addEventListener('change', (e) => {
if (e.target.files.length) {
processUploadedFile(e.target.files[0]);
}
});
// Sample data load click
btnLoadSample.addEventListener('click', loadSampleDataset);
// Filters logic
filterTheme.addEventListener('change', (e) => {
state.activeThemeFilter = e.target.value;
applyFilters();
});
filterDept.addEventListener('change', (e) => {
state.activeDeptFilter = e.target.value;
applyFilters();
});
// Action Buttons
btnResetData.addEventListener('click', resetDashboard);
btnExportExcel.addEventListener('click', exportCleanedDataToExcel);
}
// Display Custom Notification (Replaces native alerts)
function showNotification(title, message, isSuccess = true) {
const toast = document.getElementById('toast-notification');
const icon = document.getElementById('toast-icon');
const titleEl = document.getElementById('toast-title');
const msgEl = document.getElementById('toast-message');
titleEl.textContent = title;
msgEl.textContent = message;
if (isSuccess) {
toast.classList.remove('border-rose-500');
toast.classList.add('border-emerald-500');
icon.className = "fa-solid fa-circle-check text-emerald-500 text-lg";
} else {
toast.classList.remove('border-emerald-500');
toast.classList.add('border-rose-500');
icon.className = "fa-solid fa-triangle-exclamation text-rose-500 text-lg";
}
toast.classList.remove('hidden', 'translate-y-10');
toast.classList.add('translate-y-0');
// Autohide after 5 seconds
setTimeout(() => {
closeNotification();
}, 5000);
}
function closeNotification() {
const toast = document.getElementById('toast-notification');
toast.classList.add('hidden', 'translate-y-10');
}
// Reset Dashboard State
function resetDashboard() {
state.rawRows = [];
state.filteredRows = [];
state.themes = [];
state.activeThemeFilter = 'all';
state.activeDeptFilter = 'all';
state.fileName = '';
// Destroy charts instances
if (state.charts.overallAvg) {
state.charts.overallAvg.destroy();
state.charts.overallAvg = null;
}
if (state.charts.themesRadar) {
state.charts.themesRadar.destroy();
state.charts.themesRadar = null;
}
// Reset UI components
document.getElementById('file-upload').value = '';
document.getElementById('filter-theme').innerHTML = '<option value="all">เลือกประเด็นยุทธศาสตร์ทั้งหมด</option>';
document.getElementById('filter-dept').selectedIndex = 0;
document.getElementById('dashboard-active-view').classList.add('hidden');
document.getElementById('upload-placeholder-container').classList.remove('hidden');
showNotification('รีเซ็ตข้อมูลแล้ว', 'คุณสามารถอัปโหลดไฟล์ชุดใหม่เพื่อวิเคราะห์ข้อมูลได้ทันที', true);
}
// Download Chart Canvas as PNG Image
function downloadChart(canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
// Generate an image from canvas with White background fallback to prevent transparency issues
const canvasImg = document.createElement('canvas');
canvasImg.width = canvas.width;
canvasImg.height = canvas.height;
const ctx = canvasImg.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvasImg.width, canvasImg.height);
ctx.drawImage(canvas, 0, 0);
try {
const dataURL = canvasImg.toDataURL('image/png');
const link = document.createElement('a');
link.download = `${canvasId}_export.png`;
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showNotification('ส่งออกสำเร็จ', `ดาวน์โหลดแผนภูมิ ${canvasId} เป็นไฟล์ PNG เรียบร้อยแล้ว`, true);
} catch (err) {
showNotification('เกิดข้อผิดพลาด', 'ไม่สามารถบันทึกแผนภูมิเป็นไฟล์ภาพได้สำเร็จในเวลานี้', false);
}
}
// Extract and process Excel / CSV data file
function processUploadedFile(file) {
state.fileName = file.name;
document.getElementById('loaded-file-name').textContent = file.name;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[firstSheetName];
const rawJson = XLSX.utils.sheet_to_json(sheet, { header: 1 });
processMatrixJson(rawJson);
} catch (err) {
showNotification('เกิดข้อผิดพลาดในการเปิดไฟล์', 'โปรดแน่ใจว่าได้เลือกไฟล์ Excel หรือ CSV ที่มีรูปแบบถูกต้อง', false);
console.error(err);
}
};
reader.readAsArrayBuffer(file);
}
// Process Matrix representation parsed from SheetJS
function processMatrixJson(matrix) {
if (!matrix || matrix.length < 2) {
showNotification('โครงสร้างตารางไม่เพียงพอ', 'ไม่พบข้อมูลที่ต้องการวิเคราะห์หรือไฟล์มีโครงสร้างว่างเปล่า', false);
return;
}
const cleanRows = [];
let activeTheme = "อื่นๆ (ไม่ระบุประเด็นยุทธศาสตร์)";
const detectedThemes = new Set();
for (let i = 0; i < matrix.length; i++) {
const row = matrix[i];
if (!row || row.length === 0) continue;
const firstCell = String(row[0] || '').trim();
if (!firstCell) continue;
// 1. Check if Strategic Theme Category Row
if (CONFIG.themePattern.test(firstCell)) {
activeTheme = firstCell;
detectedThemes.add(activeTheme);
cleanRows.push({
type: 'theme',
name: firstCell,
originalRowIndex: i
});
continue;
}
// 2. Check if Average Summary Rows (Omit from core raw metrics calculation to prevent duplicates)
if (firstCell.includes("คะแนนเฉลี่ย")) {
cleanRows.push({
type: 'average_summary',
name: firstCell,
originalRowIndex: i
});
continue;
}
// 3. Check if KPI Sub-Indicator Row
if (CONFIG.kpiPattern.test(firstCell)) {
// Extract scores mapped with CONFIG.columns ['ตัวชี้วัด', 'สนอ.', 'กก.', 'กนผ.', 'กบค.', 'กบศ.', 'กพน.', 'กค.']
const scores = {};
CONFIG.departments.forEach((dept, index) => {
const cellVal = row[index + 1];
const parsedScore = parseFloat(cellVal);
scores[dept] = !isNaN(parsedScore) ? parsedScore : 0.0;
});
cleanRows.push({
type: 'kpi',
key: firstCell, // e.g. "1.1"
name: firstCell, // Display name
themeParent: activeTheme,
scores: scores,
originalRowIndex: i
});
}
}
// Verify cleanRows has KPIs
const hasKpis = cleanRows.some(r => r.type === 'kpi');
if (!hasKpis) {
showNotification(
'ไม่พบตัวชี้วัดที่สมบูรณ์',
'กรุณาตรวจสอบว่าชื่อตัวชี้วัดย่อยขึ้นต้นด้วยหลักทศนิยม (เช่น 1.1, 1.2) และแผนงานสอดคล้องตามที่กำหนด',
false
);
return;
}
// Save state
state.rawRows = cleanRows;
state.themes = Array.from(detectedThemes);
// Render Select Options dynamically
populateFilters();
// Toggle view to dashboard
document.getElementById('upload-placeholder-container').classList.add('hidden');
document.getElementById('dashboard-active-view').classList.remove('hidden');
// Apply default display and metrics
applyFilters();
showNotification('การนำเข้าไฟล์ข้อมูลสำเร็จ', 'ระบบวิเคราะห์และตรวจสอบ Best Practice ทันที เรียบร้อยแล้ว', true);
}
// Dynamically Populate Theme and Department Dropdowns
function populateFilters() {
const filterTheme = document.getElementById('filter-theme');
const filterDept = document.getElementById('filter-dept');
// 1. Populate Themes
let themeOptionsHTML = '<option value="all">เลือกประเด็นยุทธศาสตร์ทั้งหมด</option>';
state.themes.forEach(theme => {
themeOptionsHTML += `<option value="${theme}">${theme}</option>`;
});
filterTheme.innerHTML = themeOptionsHTML;
// 2. Populate Departments
let deptOptionsHTML = '<option value="all">เปรียบเทียบทุกกองงาน</option>';
CONFIG.departments.forEach(dept => {
deptOptionsHTML += `<option value="${dept}">${dept}</option>`;
});
filterDept.innerHTML = deptOptionsHTML;
}
// Master Filtering Logic
function applyFilters() {
let filtered = state.rawRows.filter(row => row.type === 'kpi');
// Filter 1: Theme parent match
if (state.activeThemeFilter !== 'all') {
filtered = filtered.filter(row => row.themeParent === state.activeThemeFilter);
}
state.filteredRows = filtered;
// Update Counter Badge
const totalKPIs = state.rawRows.filter(row => row.type === 'kpi').length;
const badge = document.getElementById('active-badge-counter');
badge.textContent = `แสดงผล ${filtered.length} จาก ${totalKPIs} ตัวชี้วัดย่อย`;
// Render components
calculateAndRenderStats();
renderKPITable();
renderCharts();
}
// SECTION A: Core KPI Calculations & Render Summary Cards
function calculateAndRenderStats() {
const activeKpis = state.filteredRows;
// 1. Total KPI Count
document.getElementById('stat-total-kpis').textContent = activeKpis.length;
if (activeKpis.length === 0) {
document.getElementById('stat-overall-avg').textContent = '0.00';
document.getElementById('stat-best-unit').textContent = 'ไม่มีข้อมูล';
document.getElementById('stat-best-unit-score').textContent = 'ไม่สามารถคำนวณคะแนนได้';
return;
}
// 2. Calculated Overall Average (สนอ. Primary target)
let sumPrimary = 0;
activeKpis.forEach(r => {
sumPrimary += r.scores['สนอ.'] || 0;
});
const avgPrimary = (sumPrimary / activeKpis.length).toFixed(2);
document.getElementById('stat-overall-avg').textContent = avgPrimary;
// 3. Find Best Practice Unit across all 7 departments (highest average score overall)
const deptAverages = {};
CONFIG.departments.forEach(dept => {
let deptSum = 0;
activeKpis.forEach(r => {
deptSum += r.scores[dept] || 0;
});
deptAverages[dept] = activeKpis.length > 0 ? (deptSum / activeKpis.length) : 0;
});
let bestDeptName = 'สนอ.';
let maxAverageScore = -1;
CONFIG.departments.forEach(dept => {
if (deptAverages[dept] > maxAverageScore) {
maxAverageScore = deptAverages[dept];
bestDeptName = dept;
}
});
document.getElementById('stat-best-unit').textContent = bestDeptName;
document.getElementById('stat-best-unit-score').textContent = `ค่าเฉลี่ยสะสมดีที่สุด: ${maxAverageScore.toFixed(2)} คะแนน`;
}
// SECTION D: Render beautiful sticky HTML table highlighting "Best Practice" per row
function renderKPITable() {
const tbody = document.getElementById('kpi-table-body');
tbody.innerHTML = '';
if (state.filteredRows.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="p-8 text-center text-slate-400">
<i class="fa-solid fa-folder-open text-3xl mb-2 block"></i>
ไม่พบข้อมูลตัวชี้วัดสอดคล้องตามเกณฑ์ตัวกรองนี้
</td>
</tr>
`;
return;
}
// To render a well-organized hierarchy, we loop over raw rows but filter rendering
let lastRenderedTheme = "";
state.rawRows.forEach(row => {
// If it is a Theme Category, we display a beautiful divider group row
if (row.type === 'theme') {
// Check if there are any active filtered KPIs under this theme
const hasActiveKpisInTheme = state.filteredRows.some(k => k.themeParent === row.name);
if (hasActiveKpisInTheme && state.activeThemeFilter === 'all') {
const themeTr = document.createElement('tr');
themeTr.className = "bg-brand-cream/50 border-y border-brand-accent/20";
themeTr.innerHTML = `
<td colspan="8" class="p-3 font-semibold text-brand-dark text-xs sm:text-sm">
<i class="fa-solid fa-folder-open mr-1.5 text-brand-medium"></i>
${row.name}
</td>
`;
tbody.appendChild(themeTr);
}
return;
}
// If it's a KPI row, check if it exists in filtered lists
if (row.type === 'kpi') {
const isFiltered = state.filteredRows.some(k => k.originalRowIndex === row.originalRowIndex);
if (!isFiltered) return;
// Locate Maximum score in this specific Row for "Best Practice Highlight"
let maxScore = -1;
CONFIG.departments.forEach(dept => {
const score = row.scores[dept] || 0;
if (score > maxScore) maxScore = score;
});
const tr = document.createElement('tr');
tr.className = "hover:bg-brand-cream/10 transition";
// KPI Title Cell
let kpiCellHTML = `
<td class="p-3 border-r border-slate-100 left-0 bg-white/95 backdrop-blur-sm z-10 max-w-[320px]">
<div class="flex items-start space-x-2">
<span class="inline-flex items-center justify-center px-1.5 py-0.5 rounded text-xs font-bold bg-brand-cream text-brand-primary border border-brand-accent/10">
${row.key}
</span>
<span class="text-xs sm:text-sm text-slate-700 font-body block">${row.name}</span>
</div>
</td>
`;
// Generate Cells for each Department
let deptCellsHTML = '';
CONFIG.departments.forEach(dept => {
const score = row.scores[dept] || 0;
const formattedScore = score.toFixed(2);
const isMax = score === maxScore && maxScore > 0;
// Score alert scales
let colorClass = "bg-slate-50 text-slate-700";
if (score >= 4.50) {
colorClass = "bg-[#d1fae5] text-[#065f46] border-[#a7f3d0]"; // Soft green
} else if (score >= 3.50) {
colorClass = "bg-[#fef3c7] text-[#92400e] border-[#fde68a]"; // Soft Orange
} else if (score > 0) {
colorClass = "bg-[#ffe4e6] text-[#9f1239] border-[#fecdd3]"; // Soft Red
}
// Determine if specific focus filter is applied to this department
const isFocusedDept = state.activeDeptFilter !== 'all' && state.activeDeptFilter === dept;
const focusBorder = isFocusedDept ? 'ring-2 ring-brand-primary ring-inset font-bold' : '';
// If cell is the absolute Best Practice
let badgeHTML = '';
if (isMax) {
badgeHTML = `<span class="block text-[10px] uppercase tracking-wider font-extrabold text-amber-600 mt-1"><i class="fa-solid fa-trophy mr-0.5 text-amber-500"></i> Best</span>`;
}
deptCellsHTML += `
<td class="p-3 text-center border-r border-slate-100 ${isMax ? 'bg-emerald-50/70' : ''} ${focusBorder}">
<div class="inline-flex flex-col items-center justify-center w-full min-h-[44px]">
<span class="inline-block px-2.5 py-1 text-xs font-semibold rounded-md border ${colorClass}">
${formattedScore}
</span>
${badgeHTML}
</div>
</td>
`;
});
tr.innerHTML = kpiCellHTML + deptCellsHTML;
tbody.appendChild(tr);
}
});
}
// SECTION C: Interactive charts logic
function renderCharts() {
const activeKpis = state.filteredRows;
// Compute overall averages
const deptAverages = {};
CONFIG.departments.forEach(dept => {
let sum = 0;
activeKpis.forEach(r => {
sum += r.scores[dept] || 0;
});
deptAverages[dept] = activeKpis.length > 0 ? (sum / activeKpis.length) : 0;
});
// CHART 1: Horizontal Bar Chart
const ctxBar = document.getElementById('chart-overall-avg').getContext('2d');
// Destroy existing instance to prevent visual overlapping on update
if (state.charts.overallAvg) {
state.charts.overallAvg.destroy();
}
const barData = CONFIG.departments.map(dept => deptAverages[dept].toFixed(2));
const barColors = CONFIG.departments.map(dept =>
dept === 'สนอ.' ? '#1b4332' : '#40916c'
);
state.charts.overallAvg = new Chart(ctxBar, {
type: 'bar',
data: {
labels: CONFIG.departments,
datasets: [{
label: 'คะแนนเฉลี่ยรวมทุกตัวชี้วัด',
data: barData,
backgroundColor: barColors,
borderRadius: 6,
maxBarThickness: 32
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `คะแนนเฉลี่ย: ${context.parsed.x} คะแนน`;
}
}
}
},
scales: {
x: {
min: 0,
max: 5,
ticks: {
stepSize: 1
},
grid: {
color: '#e2e8f0'
}
},
y: {
grid: {
display: false
}
}
}
}
});
// CHART 2: Radar Chart representing Theme Performance
const ctxRadar = document.getElementById('chart-themes-radar').getContext('2d');
if (state.charts.themesRadar) {
state.charts.themesRadar.destroy();
}
// Group average scores per Theme
const radarLabels = state.themes.map(theme => {
// Shorten name to fit into layout nicely
const split = theme.split(':');
const text = split[1] ? split[1].trim() : theme;
return text.length > 25 ? text.substring(0, 25) + '...' : text;
});
// Compute theme breakdown per department
const datasets = CONFIG.departments.map((dept, index) => {
const dataPoints = state.themes.map(theme => {
const themeKpis = state.rawRows.filter(r => r.type === 'kpi' && r.themeParent === theme);
let sum = 0;
themeKpis.forEach(k => {
sum += k.scores[dept] || 0;
});
return themeKpis.length > 0 ? parseFloat((sum / themeKpis.length).toFixed(2)) : 0;
});
// Generate different border/background tones based on palette
const paletteColors = [
'rgba(27, 67, 50, 1)', // สนอ.
'rgba(45, 106, 79, 0.7)', // กก.
'rgba(64, 145, 108, 0.7)', // กนผ.
'rgba(82, 183, 136, 0.7)', // กบค.
'rgba(116, 198, 157, 0.7)',// กบศ.
'rgba(149, 213, 178, 0.7)',// กพน.
'rgba(183, 228, 199, 0.7)' // กค.
];
return {
label: dept,
data: dataPoints,
borderColor: paletteColors[index % paletteColors.length],
backgroundColor: paletteColors[index % paletteColors.length].replace('1)', '0.04)').replace('0.7)', '0.04)'),
borderWidth: dept === 'สนอ.' ? 3 : 1.5,
pointRadius: dept === 'สนอ.' ? 4 : 2,
hidden: dept !== 'สนอ.' && state.activeDeptFilter !== 'all' && state.activeDeptFilter !== dept
};
});
state.charts.themesRadar = new Chart(ctxRadar, {
type: 'radar',
data: {
labels: radarLabels.length > 0 ? radarLabels : ['ไม่มีข้อมูลตัวชี้วัดหลัก'],
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12,
padding: 10,
font: {
size: 11
}
}
}
},
scales: {
r: {
angleLines: {
color: '#e2e8f0'
},
grid: {
color: '#cbd5e1'
},
pointLabels: {
font: {
size: 10,
family: 'Prompt'
}
},
min: 0,
max: 5,
ticks: {
stepSize: 1,
showLabelBackdrop: false
}
}
}
}
});
}
// Action: Clean raw data and download as standard XLSX
function exportCleanedDataToExcel() {
if (state.rawRows.length === 0) {
showNotification('ไม่มีข้อมูลส่งออก', 'ไม่พบประวัติข้อมูลเพื่อนำออกกรุณาทำการเลือกไฟล์ก่อน', false);
return;
}
// Standardize output rows structure
const excelRows = [];
state.rawRows.forEach(row => {
if (row.type === 'theme') {
excelRows.push({
'ตัวชี้วัด / หัวข้อยุทธศาสตร์': row.name,
'สนอ.': '', 'กก.': '', 'กนผ.': '', 'กบค.': '', 'กบศ.': '', 'กพน.': '', 'กค.': '',
'ประเภท': 'ประเด็นยุทธศาสตร์หลัก'
});
} else if (row.type === 'kpi') {
excelRows.push({
'ตัวชี้วัด / หัวข้อยุทธศาสตร์': `${row.key} ${row.name}`,
'สนอ.': row.scores['สนอ.'],
'กก.': row.scores['กก.'],
'กนผ.': row.scores['กนผ.'],
'กบค.': row.scores['กบค.'],
'กบศ.': row.scores['กบศ.'],
'กพน.': row.scores['กพน.'],
'กค.': row.scores['กค.'],
'ประเภท': 'ตัวชี้วัดย่อย'
});
}
});
try {
const worksheet = XLSX.utils.json_to_sheet(excelRows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "KPI Cleaned");
// Write and trigger browser download file action
const outputName = `cleaned_report_${state.fileName || 'สนอ68'}.xlsx`;
XLSX.writeFile(workbook, outputName);
showNotification('ส่งออกตารางสำเร็จ', `ไฟล์ของคุณได้รับการคลีนเนอร์และบันทึกเป็นชื่อ ${outputName}`, true);
} catch (err) {
showNotification('ส่งออกล้มเหลว', 'เกิดความผิดพลาดทางเทคนิคระหว่างเตรียมสตรีมไฟล์เพื่อบันทึก', false);
console.error(err);
}
}
// Action: Simulated Mock Data Injector
function loadSampleDataset() {
const mockMatrix = [
["ตัวชี้วัด", "สนอ.", "กก.", "กนผ.", "กบค.", "กบศ.", "กพน.", "กค."],
["1 : การวิจัยและการบริการวิชาการเพื่อยกระดับสังคมและเพิ่มมูลค่าเศรษฐกิจ"],
["1.1 ระดับความสำเร็จในการพัฒนาระบบนิเวศวิจัยเชิงนวัตกรรม", 4.80, 4.20, 4.90, 3.80, 4.50, 4.00, 4.60],
["1.2 ร้อยละผลงานวิจัยที่ถูกนำไปใช้ประโยชน์ในเชิงพื้นที่หรือพาณิชย์", 4.10, 4.70, 4.30, 4.90, 4.10, 4.50, 4.20],
["1.3 จำนวนทรัพย์สินทางปัญญาที่ได้รับความคุ้มครองจดทะเบียนสิทธิบัตร", 3.20, 4.10, 3.50, 4.50, 4.80, 4.70, 4.90],
["คะแนนเฉลี่ยประเด็นยุทธศาสตร์ที่ 1", 4.03, 4.33, 4.23, 4.40, 4.47, 4.40, 4.57],
["2 : การพัฒนาคุณภาพและการบริหารจัดการหลักสูตรการศึกษาในศตวรรษที่ 21"],
["2.1 ระดับความสำเร็จของการบูรณาการเทคโนโลยีดิจิทัลในแผนวิชาการ", 4.60, 4.80, 4.20, 4.10, 4.30, 4.90, 4.50],
["2.2 ร้อยละการคงอยู่ของนิสิตนักศึกษาระดับปริญญาตรีและบัณฑิตศึกษา", 4.90, 4.40, 4.50, 4.70, 4.20, 4.30, 4.60],
["2.3 อัตราความพึงพอใจของนายจ้างที่มีต่อคุณภาพผู้สำเร็จการศึกษา", 4.50, 4.50, 4.80, 4.20, 4.00, 4.40, 4.80],
["คะแนนเฉลี่ยประเด็นยุทธศาสตร์ที่ 2", 4.67, 4.57, 4.50, 4.33, 4.17, 4.53, 4.63],
["3 : การพัฒนาความเลิศด้านสมรรถนะองค์กรดิจิทัลและการพัฒนาบุคลากรวิชาชีพ"],
["3.1 ระดับคะแนนเฉลี่ยความสุขในการปฏิบัติงานของบุคลากรภายในสำนักงาน", 4.30, 4.20, 4.60, 4.50, 4.90, 4.10, 4.40],
["3.2 ร้อยละบุคลากรที่ผ่านเกณฑ์การพัฒนาสมรรถนะและประเมินระดับความรู้", 4.70, 4.60, 4.80, 4.40, 4.50, 4.80, 4.70],
["3.3 ร้อยละความสำเร็จในการใช้จ่ายงบประมาณแผ่นดินและการจำกัดความสูญเปล่า", 3.40, 3.90, 4.10, 4.80, 3.60, 4.40, 4.50],
["คะแนนเฉลี่ยประเด็นยุทธศาสตร์ที่ 3", 4.13, 4.23, 4.50, 4.57, 4.33, 4.43, 4.53]
];
state.fileName = "ตัวอย่าง_ตัวชี้วัด_สนอ68.xlsx";
document.getElementById('loaded-file-name').textContent = state.fileName;
processMatrixJson(mockMatrix);
}
</script>
</body>
</html>