PAli / index.html
Pali05's picture
Upload index.html
bf7ac94 verified
Raw
History Blame Contribute Delete
75.3 kB
<!DOCTYPE html>
<html lang="th" class="h-full bg-[#0B0F19] text-[#F1F5F9]">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ระบบติดตามและประเมินตัวชี้วัด สนอ. มรพส. (ปีงบประมาณ 2568)</title>
<!-- Google Fonts -->
<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=Noto+Sans+Thai:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
cyber: {
black: '#0B0F19',
navy: '#161F30',
charcoal: '#1F2937',
emerald: '#10B981',
mint: '#34D399',
slate: '#F1F5F9',
muted: '#64748B'
}
},
fontFamily: {
sans: ['Noto Sans Thai', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace']
}
}
}
}
</script>
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- SheetJS CDN for Excel parsing -->
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<!-- Lucide Icons CDN -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* Custom scrollbar styling for a high-tech dashboard look */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0B0F19;
}
::-webkit-scrollbar-thumb {
background: #1F2937;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #10B981;
}
.glow-emerald {
box-shadow: 0 0 20px rgba(16, 185, 129, 0.15);
}
.text-glow {
text-shadow: 0 0 10px rgba(52, 211, 153, 0.3);
}
/* Custom classes to prevent tabular numbers bouncing */
.font-mono {
font-variant-numeric: tabular-nums;
}
</style>
</head>
<body class="h-full font-sans antialiased text-cyber-slate flex flex-col md:flex-row">
<!-- Mobile Top Navigation Header -->
<div class="md:hidden w-full bg-cyber-navy border-b border-cyber-charcoal p-4 flex items-center justify-between sticky top-0 z-50">
<div class="flex items-center space-x-3">
<div class="p-2 bg-cyber-emerald/10 text-cyber-mint rounded-lg">
<i data-lucide="shield-check" class="w-6 h-6"></i>
</div>
<div>
<h1 class="text-sm font-bold tracking-tight text-white">ระบบตัวชี้วัด สนอ. มรพส.</h1>
<p class="text-[10px] text-cyber-emerald">ปีงบประมาณ 2568</p>
</div>
</div>
<button id="mobileMenuBtn" class="p-2 rounded-lg text-cyber-slate hover:bg-cyber-charcoal focus:outline-none">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
</div>
<!-- Left Sidebar for Navigation, Filters, & File Config -->
<aside id="sidebar" class="hidden md:flex flex-col w-full md:w-80 lg:w-90 bg-cyber-navy border-r border-cyber-charcoal flex-shrink-0 h-screen overflow-y-auto sticky top-0 z-40 transition-all duration-300">
<!-- Logo Header -->
<div class="p-6 border-b border-cyber-charcoal flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="p-2.5 bg-cyber-emerald/15 text-cyber-mint rounded-xl border border-cyber-emerald/20 glow-emerald">
<i data-lucide="trending-up" class="w-6 h-6"></i>
</div>
<div>
<h2 class="text-base font-bold text-white tracking-wide">สนอ. มรพส. 2568</h2>
<p class="text-xs text-cyber-mint font-mono font-semibold">KPI EVALUATION</p>
</div>
</div>
<!-- Desktop Collapse Button -->
<button id="collapseSidebarBtn" class="hidden md:flex p-1.5 rounded-lg text-cyber-muted hover:text-cyber-mint hover:bg-cyber-charcoal transition" title="ซ่อนแถบเมนู">
<i data-lucide="chevron-left" class="w-5 h-5"></i>
</button>
</div>
<!-- File Control & Configuration -->
<div class="p-5 border-b border-cyber-charcoal bg-cyber-black/20">
<div class="flex items-center justify-between mb-3">
<span class="text-xs font-semibold text-cyber-muted tracking-wider uppercase">แหล่งข้อมูล</span>
<span id="dataStatusBadge" class="px-2 py-0.5 rounded-full text-[10px] font-medium bg-red-500/10 text-red-400 border border-red-500/20">
ยังไม่มีข้อมูล
</span>
</div>
<!-- Small File Upload Utility inside Sidebar -->
<div id="sidebarFileZone" class="border border-dashed border-cyber-charcoal rounded-lg p-3 hover:border-cyber-emerald/50 transition cursor-pointer flex flex-col items-center justify-center text-center bg-cyber-black/40">
<input type="file" id="sidebarFileIn" accept=".xlsx,.xls" class="hidden">
<i data-lucide="upload-cloud" class="w-6 h-6 text-cyber-muted mb-1"></i>
<span class="text-xs text-cyber-slate font-medium">เปลี่ยนไฟล์ Excel</span>
<span class="text-[9px] text-cyber-muted mt-0.5">ลากไฟล์มาที่นี่เพื่ออัปเดต</span>
</div>
<button id="loadDemoBtn" class="w-full mt-2.5 py-1.5 px-3 bg-cyber-emerald/10 hover:bg-cyber-emerald/20 text-cyber-mint border border-cyber-emerald/30 rounded-lg text-xs font-medium transition flex items-center justify-center space-x-1">
<i data-lucide="database" class="w-3.5 h-3.5"></i>
<span>โหลดข้อมูลสาธิต (Demo)</span>
</button>
</div>
<!-- Real-Time Interactive Filters -->
<div class="p-6 flex-grow space-y-6">
<div class="flex items-center justify-between">
<h3 class="text-xs font-bold text-cyber-muted tracking-wider uppercase">ตัวกรองข้อมูลแบบมีปฏิสัมพันธ์</h3>
<button id="resetFiltersBtn" class="text-xs text-cyber-mint hover:text-white flex items-center space-x-1 font-semibold transition group">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5 group-hover:rotate-180 transition-transform duration-500"></i>
<span>รีเซ็ต</span>
</button>
</div>
<!-- Strategy Filter -->
<div class="space-y-2">
<label for="filterStrategy" class="block text-xs font-semibold text-cyber-slate">ประเด็นยุทธศาสตร์</label>
<div class="relative">
<select id="filterStrategy" class="w-full bg-cyber-black text-xs text-cyber-slate border border-cyber-charcoal rounded-lg py-2.5 pl-3 pr-8 focus:outline-none focus:border-cyber-emerald focus:ring-1 focus:ring-cyber-emerald appearance-none cursor-pointer">
<option value="all">-- ทั้งหมด --</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none text-cyber-muted">
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</div>
</div>
</div>
<!-- Department Filter -->
<div class="space-y-2">
<label for="filterDept" class="block text-xs font-semibold text-cyber-slate">กองงาน / หน่วยงานย่อย</label>
<div class="relative">
<select id="filterDept" class="w-full bg-cyber-black text-xs text-cyber-slate border border-cyber-charcoal rounded-lg py-2.5 pl-3 pr-8 focus:outline-none focus:border-cyber-emerald focus:ring-1 focus:ring-cyber-emerald appearance-none cursor-pointer">
<option value="all">-- ทั้งหมด --</option>
<option value="สนอ.">สนอ. (ภาพรวม)</option>
<option value="กก.">กก. (กองกลาง)</option>
<option value="กนผ.">กนผ. (กองนโยบายและแผน)</option>
<option value="กบค.">กบค. (กองบริหารงานบุคคล)</option>
<option value="กบศ.">กบศ. (กองบริการการศึกษา)</option>
<option value="กพน.">กพน. (กองพัฒนานักศึกษา)</option>
<option value="กค.">กค. (กองคลัง)</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none text-cyber-muted">
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</div>
</div>
</div>
<!-- Score Threshold Filter Slider -->
<div class="space-y-2">
<div class="flex justify-between items-center">
<label class="block text-xs font-semibold text-cyber-slate">ตัวกรองคะแนนเป้าหมาย</label>
<span id="scoreThresholdVal" class="text-xs font-mono text-cyber-mint font-bold bg-cyber-emerald/10 px-2 py-0.5 rounded border border-cyber-emerald/20">≥ 1.0</span>
</div>
<input id="scoreThresholdSlider" type="range" min="1" max="5" step="0.5" value="1" class="w-full h-1 bg-cyber-charcoal rounded-lg appearance-none cursor-pointer accent-cyber-emerald">
<div class="flex justify-between text-[10px] text-cyber-muted font-mono">
<span>1.0</span>
<span>2.0</span>
<span>3.0</span>
<span>4.0</span>
<span>5.0</span>
</div>
</div>
</div>
<!-- Sidebar Footer Status info -->
<div class="p-6 border-t border-cyber-charcoal mt-auto bg-cyber-black/10">
<div class="flex items-center space-x-2 text-[11px] text-cyber-muted mb-1">
<span class="w-1.5 h-1.5 rounded-full bg-cyber-emerald animate-pulse"></span>
<span>พอร์ทัลผู้บริหารระดับสูง (Executive Console)</span>
</div>
<div class="text-[10px] text-cyber-muted font-mono leading-relaxed">
เวอร์ชันระบบ v1.2 (ปีงบประมาณ 2568)<br>
ประมวลผลภายในบราวเซอร์ 100%
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-grow flex flex-col min-h-screen overflow-x-hidden">
<!-- Splash Welcome / Drag and Drop Upload screen (Shown when no data exists) -->
<section id="uploadScreen" class="flex-grow flex flex-col items-center justify-center p-6 md:p-12 lg:p-16 max-w-5xl mx-auto w-full">
<div class="text-center mb-10">
<span class="px-3 py-1 bg-cyber-emerald/10 border border-cyber-emerald/30 text-cyber-mint rounded-full text-xs font-mono font-bold tracking-wider uppercase mb-3 inline-block">
มหาวิทยาลัยราชภัฏพิบูลสงคราม
</span>
<h1 class="text-3xl md:text-4xl font-extrabold tracking-tight text-white mb-4 mt-2">
ระบบติดตามและประเมินตัวชี้วัด <span class="text-transparent bg-clip-text bg-gradient-to-r from-cyber-emerald to-cyber-mint">สนอ. มรพส.</span>
</h1>
<p class="text-cyber-muted max-w-xl mx-auto text-sm md:text-base">
แพลตฟอร์มวิเคราะห์และแสดงผลผลงานเชิงกลยุทธ์ สำหรับผู้บริหารระดับสูง สำนักงานอธิการบดี เพื่อตรวจจับและค้นหากลุ่มงานที่มีผลสัมฤทธิ์สูงสุด (Top Performer) ได้ทันที
</p>
</div>
<!-- Big Sleek Dropzone -->
<div id="dropzone" class="w-full max-w-2xl border-2 border-dashed border-cyber-charcoal rounded-2xl p-10 md:p-14 hover:border-cyber-emerald bg-cyber-navy/40 transition duration-300 flex flex-col items-center justify-center text-center group cursor-pointer shadow-xl relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-cyber-emerald/5 to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition duration-300"></div>
<div class="p-4 bg-cyber-navy border border-cyber-charcoal rounded-2xl group-hover:scale-110 group-hover:border-cyber-emerald/40 transition duration-300 mb-6 relative">
<i data-lucide="file-spreadsheet" class="w-12 h-12 text-cyber-mint"></i>
</div>
<h3 class="text-lg font-bold text-white mb-2">ลากและวางไฟล์ผลการประเมินประเด็นยุทธศาสตร์ (.xlsx)</h3>
<p class="text-xs text-cyber-muted max-w-md mb-6 leading-relaxed">
รองรับไฟล์ประเมินตัวชี้วัดของสำนักงานอธิการบดี (สนอ.) ปีงบประมาณ 2568 ทุกกองงาน ระบบจะทำการคลี่ตารางและล้างค่าสถิติซ้ำซ้อนโดยอัตโนมัติ
</p>
<input type="file" id="mainFileIn" accept=".xlsx,.xls" class="hidden">
<button class="px-6 py-2.5 bg-cyber-emerald hover:bg-cyber-emerald/85 text-cyber-black font-bold rounded-lg text-sm transition duration-150 flex items-center space-x-2 shadow-lg hover:shadow-cyber-emerald/20">
<i data-lucide="search" class="w-4 h-4"></i>
<span>ค้นหาและเลือกไฟล์จากเครื่อง</span>
</button>
</div>
<!-- Quick Demo Guide & File Path Note -->
<div class="mt-8 flex flex-col items-center text-center">
<div class="text-xs text-cyber-muted max-w-md">
<span class="text-cyber-mint font-semibold">หมายเหตุ:</span> ระบบพยายามตรวจเช็คฐานข้อมูลที่ <code class="font-mono bg-cyber-navy py-0.5 px-1.5 rounded text-cyber-slate">./datasource/ตัวชี้วัด สนอ.68.xlsx</code> เรียลไทม์ หากทำงานแบบออฟไลน์ คุณสามารถอัปโหลดไฟล์ตรงนี้ หรือคลิกปุ่มด้านล่างเพื่อประมวลผลด้วยข้อมูลจำลองทันที:
</div>
<button id="quickDemoBtn" class="mt-4 px-5 py-2 bg-cyber-navy border border-cyber-charcoal hover:border-cyber-emerald hover:text-cyber-mint text-cyber-slate font-medium text-xs rounded-lg transition duration-200 flex items-center space-x-2">
<i data-lucide="zap" class="w-4 h-4 text-cyber-mint animate-bounce"></i>
<span>เริ่มต้นทดลองใช้งานด่วน (โหลดข้อมูลสาธิต)</span>
</button>
</div>
</section>
<!-- Executive Dashboard Contents (Initially Hidden) -->
<section id="dashboardContent" class="hidden flex-grow p-4 md:p-6 lg:p-8 space-y-6">
<!-- Dashboard Header / Overview Panel -->
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between border-b border-cyber-charcoal pb-6 space-y-4 lg:space-y-0">
<div class="flex items-start">
<!-- Desktop Expand Menu Button (Hidden when sidebar is visible) -->
<button id="expandSidebarBtn" class="hidden p-2.5 bg-cyber-navy border border-cyber-charcoal rounded-xl text-cyber-mint hover:bg-cyber-charcoal transition mr-4 shadow-lg" title="แสดงแถบเมนู">
<i data-lucide="menu" class="w-5 h-5"></i>
</button>
<div>
<div class="flex items-center space-x-2.5">
<span id="activeFiltersBadge" class="hidden px-2 py-0.5 bg-yellow-500/15 border border-yellow-500/30 text-yellow-400 rounded-full text-[10px] font-mono">
เปิดใช้งานตัวกรองอยู่
</span>
<span id="dataSourceBadge" class="px-2 py-0.5 bg-cyber-emerald/10 border border-cyber-emerald/30 text-cyber-mint rounded-full text-[10px] font-mono">
กำลังแสดงผลข้อมูลสาธิต
</span>
</div>
<h1 class="text-2xl md:text-3xl font-extrabold tracking-tight text-white mt-1">
ระบบติดตามและประเมินตัวชี้วัด สนอ. มรพส.
</h1>
<p class="text-xs text-cyber-muted mt-1 leading-relaxed">
แผงประเมินผลสำหรับผู้บริหารระดับสูง สำนักงานอธิการบดี มหาวิทยาลัยราชภัฏพิบูลสงคราม ประจำปีงบประมาณ 2568
</p>
</div>
</div>
<!-- Export Utilities & Reload Options -->
<div class="flex flex-wrap items-center gap-2">
<button id="exportRawExcelBtn" class="px-4 py-2 bg-cyber-navy border border-cyber-charcoal hover:border-cyber-emerald/50 text-cyber-slate hover:text-cyber-mint text-xs font-semibold rounded-lg transition flex items-center space-x-2">
<i data-lucide="download" class="w-4 h-4"></i>
<span>ส่งออกข้อมูลคลี่ตาราง (.xlsx)</span>
</button>
<button id="changeFileTriggerBtn" class="px-4 py-2 bg-cyber-black hover:bg-cyber-charcoal border border-cyber-charcoal text-cyber-slate text-xs font-semibold rounded-lg transition flex items-center space-x-2">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
<span>เปลี่ยนไฟล์ชุดข้อมูล</span>
</button>
</div>
</div>
<!-- 1. Executive Analytics & KPI Cards (Top Section) -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
<!-- Card 1: Overall Average score 'สนอ.' -->
<div class="bg-cyber-navy border border-cyber-charcoal rounded-xl p-6 relative overflow-hidden group hover:border-cyber-emerald/30 transition duration-300 flex flex-col justify-between">
<div class="absolute -right-6 -bottom-6 text-cyber-emerald/5 group-hover:scale-110 transition-transform duration-500 pointer-events-none">
<i data-lucide="award" class="w-36 h-36"></i>
</div>
<div class="flex justify-between items-start z-10">
<div>
<p class="text-xs font-semibold text-cyber-muted tracking-wide uppercase">คะแนนเฉลี่ยภาพรวม สนอ.</p>
<span class="text-4xl md:text-5xl font-bold font-mono text-cyber-emerald tracking-tight block mt-2 text-glow" id="kpiAvgScore">-</span>
</div>
<span class="p-2 bg-cyber-emerald/10 text-cyber-mint rounded-lg">
<i data-lucide="gauge" class="w-5 h-5"></i>
</span>
</div>
<div class="mt-4 pt-4 border-t border-cyber-charcoal/50 flex items-center justify-between text-xs text-cyber-muted z-10">
<span>จากทุกตัวชี้วัดที่ประเมินแล้ว</span>
<span class="font-mono font-medium text-cyber-slate">เต็ม 5.00 คะแนน</span>
</div>
</div>
<!-- Card 2: TOP PERFORMING Department -->
<div class="bg-cyber-navy border border-cyber-charcoal rounded-xl p-6 relative overflow-hidden group hover:border-cyber-emerald/30 transition duration-300 flex flex-col justify-between">
<div class="absolute -right-6 -bottom-6 text-cyber-mint/5 group-hover:scale-110 transition-transform duration-500 pointer-events-none">
<i data-lucide="trophy" class="w-36 h-36"></i>
</div>
<div class="flex justify-between items-start z-10">
<div>
<p class="text-xs font-semibold text-cyber-muted tracking-wide uppercase">กองงานดาวรุ่งยอดเยี่ยม (Top Performer)</p>
<span class="text-4xl md:text-5xl font-bold font-mono text-cyan-400 tracking-tight block mt-2 animate-pulse" id="kpiTopPerformer">-</span>
</div>
<span class="p-2 bg-cyan-500/10 text-cyan-400 rounded-lg">
<i data-lucide="crown" class="w-5 h-5"></i>
</span>
</div>
<div class="mt-4 pt-4 border-t border-cyber-charcoal/50 flex items-center justify-between text-xs text-cyber-muted z-10">
<span>หน่วยงานย่อยผลงานสูงสุดเฉลี่ย</span>
<span class="font-mono text-cyan-400 font-bold" id="kpiTopPerformerScore">-</span>
</div>
</div>
<!-- Card 3: Total Active KPI items (Parsed and cleaned) -->
<div class="bg-cyber-navy border border-cyber-charcoal rounded-xl p-6 relative overflow-hidden group hover:border-cyber-emerald/30 transition duration-300 flex flex-col justify-between">
<div class="absolute -right-4 -bottom-4 text-purple-500/5 group-hover:scale-110 transition-transform duration-500 pointer-events-none">
<i data-lucide="check-square" class="w-32 h-32"></i>
</div>
<div class="flex justify-between items-start z-10">
<div>
<p class="text-xs font-semibold text-cyber-muted tracking-wide uppercase">ตัวชี้วัดที่เปิดใช้งานจริง</p>
<span class="text-4xl md:text-5xl font-bold font-mono text-purple-400 tracking-tight block mt-2" id="kpiActiveCount">-</span>
</div>
<span class="p-2 bg-purple-500/10 text-purple-400 rounded-lg">
<i data-lucide="list-checks" class="w-5 h-5"></i>
</span>
</div>
<div class="mt-4 pt-4 border-t border-cyber-charcoal/50 flex items-center justify-between text-xs text-cyber-muted z-10">
<span>คัดกรองแถวค่าเฉลี่ยสถิติออกแล้ว</span>
<span class="font-mono text-purple-400 font-semibold" id="kpiExcludedSummaryCount">0 แถว</span>
</div>
</div>
</div>
<!-- 2. Charts Grid Section -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- Chart 1 (Left 5 columns): Horizontal Bar Ranking Departments -->
<div class="bg-cyber-navy border border-cyber-charcoal rounded-xl p-5 lg:col-span-5 flex flex-col justify-between">
<div class="flex items-center justify-between border-b border-cyber-charcoal/50 pb-3 mb-4">
<div class="flex items-center space-x-2">
<i data-lucide="bar-chart-3" class="w-4 h-4 text-cyber-emerald"></i>
<h3 class="text-sm font-bold text-white">อันดับคะแนนเฉลี่ยรายหน่วยงาน</h3>
</div>
<button onclick="downloadChartPng('rankingChart')" class="p-1.5 hover:bg-cyber-charcoal rounded text-cyber-muted hover:text-cyber-slate transition" title="ดาวน์โหลดแผนภาพเป็นรูปภาพ">
<i data-lucide="image" class="w-4 h-4"></i>
</button>
</div>
<div class="relative flex-grow min-h-[300px] flex items-center justify-center">
<canvas id="rankingChart"></canvas>
</div>
</div>
<!-- Chart 2 (Right 7 columns): Strategy Themes Comparison -->
<div class="bg-cyber-navy border border-cyber-charcoal rounded-xl p-5 lg:col-span-7 flex flex-col justify-between">
<div class="flex items-center justify-between border-b border-cyber-charcoal/50 pb-3 mb-4">
<div class="flex items-center space-x-2">
<i data-lucide="radar" class="w-4 h-4 text-cyber-mint"></i>
<h3 class="text-sm font-bold text-white">เปรียบเทียบผลงานใน 5 มิติตามประเด็นยุทธศาสตร์</h3>
</div>
<button onclick="downloadChartPng('strategyChart')" class="p-1.5 hover:bg-cyber-charcoal rounded text-cyber-muted hover:text-cyber-slate transition" title="ดาวน์โหลดแผนภาพเป็นรูปภาพ">
<i data-lucide="image" class="w-4 h-4"></i>
</button>
</div>
<div class="relative flex-grow min-h-[300px] flex items-center justify-center">
<canvas id="strategyChart"></canvas>
</div>
</div>
</div>
<!-- 3. Interactive Data Table Panel -->
<div class="bg-cyber-navy border border-cyber-charcoal rounded-xl p-5">
<!-- Table Header and Search Bar -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pb-5 mb-4 border-b border-cyber-charcoal/50">
<div class="flex items-center space-x-2">
<i data-lucide="table" class="w-4 h-4 text-cyber-emerald"></i>
<h3 class="text-sm font-bold text-white">รายละเอียดชุดคะแนนประเมินตัวชี้วัดรายข้อ</h3>
</div>
<div class="flex items-center space-x-2 max-w-md w-full sm:w-auto">
<div class="relative w-full">
<input id="tableSearchIn" type="text" placeholder="พิมพ์คำเพื่อค้นหาตัวชี้วัด..." class="w-full bg-cyber-black text-xs text-cyber-slate border border-cyber-charcoal rounded-lg pl-8 pr-3 py-2 focus:outline-none focus:border-cyber-emerald">
<div class="absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none text-cyber-muted">
<i data-lucide="search" class="w-3.5 h-3.5"></i>
</div>
</div>
</div>
</div>
<!-- Actual Data Table Wrapper -->
<div class="overflow-x-auto rounded-lg border border-cyber-charcoal max-h-[500px] overflow-y-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-cyber-black/70 border-b border-cyber-charcoal sticky top-0 z-10 backdrop-blur-md">
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase max-w-xs">ประเด็นยุทธศาสตร์</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase w-20">รหัส</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase min-w-[250px]">ชื่อตัวชี้วัด</th>
<th class="p-3 text-[11px] font-bold text-cyber-mint tracking-wide uppercase text-right font-mono font-bold w-16">สนอ.</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase text-right font-mono w-16">กก.</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase text-right font-mono w-16">กนผ.</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase text-right font-mono w-16">กบค.</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase text-right font-mono w-16">กบศ.</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase text-right font-mono w-16">กพน.</th>
<th class="p-3 text-[11px] font-bold text-cyber-slate tracking-wide uppercase text-right font-mono w-16">กค.</th>
</tr>
</thead>
<tbody id="indicatorsTableBody" class="divide-y divide-cyber-charcoal/40">
<!-- Rows will be injected dynamically -->
</tbody>
</table>
</div>
<!-- Table Footer Pagination / Summary -->
<div class="mt-4 flex flex-col sm:flex-row items-center justify-between text-xs text-cyber-muted gap-2">
<span id="tablePageStats">แสดงผล 0 จากทั้งหมด 0 แถว</span>
<span class="flex items-center space-x-1">
<i data-lucide="help-circle" class="w-3.5 h-3.5 text-cyber-muted"></i>
<span>เลื่อนเมาส์ชี้เหนือนามย่อกองเพื่อดูชื่อหน่วยงานเต็ม</span>
</span>
</div>
</div>
<!-- 4. Extra Analytics Explainer For Executives -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Data Dictionary / Strategy Info Card -->
<div class="bg-cyber-navy/50 border border-cyber-charcoal rounded-xl p-5 space-y-3">
<h4 class="text-xs font-bold text-cyber-mint tracking-wider uppercase flex items-center space-x-1.5">
<i data-lucide="bookmark-check" class="w-4 h-4"></i>
<span>คู่มือรหัสหน่วยงานย่อย (Department Index)</span>
</h4>
<div class="grid grid-cols-2 gap-y-1 gap-x-4 text-xs leading-relaxed text-cyber-muted">
<div><strong class="text-cyber-slate font-mono">สนอ.</strong> : สำนักงานอธิการบดี (ภาพรวม)</div>
<div><strong class="text-cyber-slate font-mono">กก.</strong> : กองกลาง</div>
<div><strong class="text-cyber-slate font-mono">กนผ.</strong> : กองนโยบายและแผน</div>
<div><strong class="text-cyber-slate font-mono">กบค.</strong> : กองบริหารงานบุคคล</div>
<div><strong class="text-cyber-slate font-mono">กบศ.</strong> : กองบริการการศึกษา</div>
<div><strong class="text-cyber-slate font-mono">กพน.</strong> : กองพัฒนานักศึกษา</div>
<div><strong class="text-cyber-slate font-mono">กค.</strong> : กองคลัง</div>
</div>
</div>
<!-- Cleaning Logic Log Window -->
<div class="bg-cyber-navy/50 border border-cyber-charcoal rounded-xl p-5 space-y-2 flex flex-col justify-between">
<h4 class="text-xs font-bold text-purple-400 tracking-wider uppercase flex items-center space-x-1.5">
<i data-lucide="terminal" class="w-4 h-4"></i>
<span>ความสอดคล้องและการล้างข้อมูลในบราวเซอร์ (Data Cleaning Logs)</span>
</h4>
<p class="text-[11px] text-cyber-muted leading-relaxed">
ระบบทำการตรวจจับมิติข้อมูลแบบ Cross-Tab แล้วคลี่โครงสร้างออกเป็นตารางเชิงสัมพันธ์ (Relational Rows) พร้อมทั้งตรวจจับข้อความและกรองเอา "คะแนนเฉลี่ยทุกประเด็นการประเมิน", "เฉลี่ยสะสม", และค่าร้อยละเฉพาะส่วนที่ซ้ำซ้อนออกทั้งหมดเพื่อรักษาความถูกต้องและปราศจากการคำนวณเบิ้ลสองเท่า (Double-counting Error)
</p>
<div class="text-[10px] text-cyber-mint font-mono mt-2 pt-2 border-t border-cyber-charcoal/40 flex justify-between">
<span>สถานะระบบวิเคราะห์: ปกติ</span>
<span>รหัสคำนวณ: COMPLIANT-OK</span>
</div>
</div>
</div>
</section>
</main>
<!-- Global Application Logic Scripts -->
<script>
// Store raw flattened indicators and current filters state
let rawIndicatorsData = [];
let cleanFilteredData = [];
let allUniqueStrategies = [];
// References to visual charts (ChartJS objects)
let chartInstanceRanking = null;
let chartInstanceStrategy = null;
// Target Departments structure in consistency with requirements
const DEPARTMENTS = ['สนอ.', 'กก.', 'กนผ.', 'กบค.', 'กบศ.', 'กพน.', 'กค.'];
// Fully Compliant Mock Dataset that represents precisely the multi-strategy matrix
const demoExcelRawMatrix = {
SheetNames: ["ตัวชี้วัดปี68"],
Sheets: {
"ตัวชี้วัดปี68": {
"!ref": "A1:H19",
"A1": { v: "ตัวชี้วัด" }, "B1": { v: "สนอ." }, "C1": { v: "กก." }, "D1": { v: "กนผ." }, "E1": { v: "กบค." }, "F1": { v: "กบศ." }, "G1": { v: "กพน." }, "H1": { v: "กค." },
// Strategy 1
"A2": { v: "1 : พัฒนาระบบบริหารจัดการด้วยเทคโนโลยีและหลักธรรมาภิบาล" },
"A3": { v: "1.1 ร้อยละความสำเร็จในการพัฒนาระบบประเมินผลออนไลน์" }, "B3": { v: 4.5 }, "C3": { v: 4.2 }, "D3": { v: 4.8 }, "E3": { v: 4.0 }, "F3": { v: 4.3 }, "G3": { v: 3.9 }, "H3": { v: 4.1 },
"A4": { v: "1.2 ระดับความพึงพอใจต่อการดำเนินงานตามหลักธรรมาภิบาล" }, "B4": { v: 3.8 }, "C4": { v: 4.0 }, "D4": { v: 3.5 }, "E4": { v: 4.5 }, "F4": { v: 3.7 }, "G4": { v: null }, "H4": { v: 4.0 },
"A5": { v: "คะแนนเฉลี่ย 1" }, "B5": { v: 4.15 }, "C5": { v: 4.1 }, "D5": { v: 4.15 }, "E5": { v: 4.25 }, "F5": { v: 4.0 }, "G5": { v: 3.9 }, "H5": { v: 4.05 }, // Ignore row
// Strategy 2
"A6": { v: "2 : ยกระดับและเพิ่มขีดความสามารถการบริการวิชาการแก่ชุมชนท้องถิ่น" },
"A7": { v: "2.1 จำนวนโครงการวิจัยเพื่อพัฒนาท้องถิ่นที่สัมฤทธิ์ผลตามเป้าหมาย" }, "B7": { v: 4.2 }, "C7": { v: 4.0 }, "D7": { v: 4.5 }, "E7": { v: null }, "F7": { v: 4.6 }, "G7": { v: 4.7 }, "H7": { v: null },
"A8": { v: "2.2 ระดับการนำองค์ความรู้ไปใช้ประโยชน์ในชุมชนเป้าหมาย" }, "B8": { v: 4.0 }, "C8": { v: 3.8 }, "D8": { v: 4.2 }, "E8": { v: 4.0 }, "F8": { v: 4.1 }, "G8": { v: 4.5 }, "H8": { v: 3.5 },
"A9": { v: "คะแนนเฉลี่ย 2" }, "B9": { v: 4.1 }, "C9": { v: 3.9 }, "D9": { v: 4.35 }, "E9": { v: 4.0 }, "F9": { v: 4.35 }, "G9": { v: 4.6 }, "H9": { v: 3.5 }, // Ignore row
// Strategy 3
"A10": { v: "3 : พัฒนาคุณภาพการศึกษาและขีดความสามารถการเรียนรู้ของนักศึกษา" },
"A11": { v: "3.1 ร้อยละของบัณฑิตที่มีสมรรถนะการทำงานในระดับพึงพอใจสูง" }, "B11": { v: 4.8 }, "C11": { v: null }, "D11": { v: 4.6 }, "E11": { v: 4.5 }, "F11": { v: 4.9 }, "G11": { v: 4.7 }, "H11": { v: 4.2 },
"A12": { v: "3.2 คะแนนรวมเฉลี่ยผลสัมฤทธิ์ทางการเรียนและการพัฒนาทักษะอาชีพ" }, "B12": { v: 4.1 }, "C12": { v: 3.9 }, "D12": { v: 4.0 }, "E12": { v: 4.2 }, "F12": { v: 4.3 }, "G12": { v: 4.1 }, "H12": { v: 3.8 },
"A13": { v: "คะแนนเฉลี่ย 3" }, "B13": { v: 4.45 }, "C13": { v: 3.9 }, "D13": { v: 4.3 }, "E13": { v: 4.35 }, "F13": { v: 4.6 }, "G13": { v: 4.4 }, "H13": { v: 4.0 }, // Ignore row
// Strategy 4
"A14": { v: "4 : เสริมสร้างวัฒนธรรมองค์กร ค่านิยม และอนุรักษ์ศิลปวัฒนธรรมท้องถิ่น" },
"A15": { v: "4.1 ดัชนีความพึงพอใจของบุคลากรต่อการสืบสานทำนุบำรุงศิลปวัฒนธรรม" }, "B15": { v: 3.5 }, "C15": { v: 3.7 }, "D15": { v: 3.4 }, "E15": { v: 3.8 }, "F15": { v: 3.6 }, "G15": { v: 4.0 }, "H15": { v: 3.5 },
"A16": { v: "คะแนนเฉลี่ย 4" }, "B16": { v: 3.5 }, "C16": { v: 3.7 }, "D16": { v: 3.4 }, "E16": { v: 3.8 }, "F16": { v: 3.6 }, "G16": { v: 4.0 }, "H16": { v: 3.5 }, // Ignore row
// Strategy 5
"A17": { v: "5 : เพิ่มประสิทธิภาพการบริหารสินทรัพย์และการสร้างรายได้เพิ่มขึ้น" },
"A18": { v: "5.1 ร้อยละการเพิ่มขึ้นของรายได้จากสินทรัพย์และการบริการวิชาการ" }, "B18": { v: 4.2 }, "C18": { v: 3.9 }, "D18": { v: 4.1 }, "E18": { v: 3.8 }, "F18": { v: 4.0 }, "G18": { v: 4.3 }, "H18": { v: 4.5 },
"A19": { v: "คะแนนเฉลี่ย 5" }, "B19": { v: 4.2 }, "C19": { v: 3.9 }, "D19": { v: 4.1 }, "E19": { v: 3.8 }, "F19": { v: 4.0 }, "G19": { v: 4.3 }, "H19": { v: 4.5 }, // Ignore row
}
}
};
// Window load event handling
window.onload = function () {
lucide.createIcons();
setupDragAndDrop();
setupInteractiveFilters();
setupSidebarToggle();
tryAutoFetchDefaultExcel();
};
// Sidebar Toggle Configuration (Desktop & Mobile)
function setupSidebarToggle() {
const sidebar = document.getElementById('sidebar');
const collapseSidebarBtn = document.getElementById('collapseSidebarBtn');
const expandSidebarBtn = document.getElementById('expandSidebarBtn');
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
function hideSidebar() {
sidebar.classList.add('hidden');
sidebar.classList.remove('md:flex');
expandSidebarBtn.classList.remove('hidden');
expandSidebarBtn.classList.add('flex');
// Redraw charts smoothly
setTimeout(() => {
if (chartInstanceRanking) chartInstanceRanking.resize();
if (chartInstanceStrategy) chartInstanceStrategy.resize();
}, 100);
}
function showSidebar() {
sidebar.classList.remove('hidden');
sidebar.classList.add('md:flex');
expandSidebarBtn.classList.add('hidden');
expandSidebarBtn.classList.remove('flex');
// Redraw charts smoothly
setTimeout(() => {
if (chartInstanceRanking) chartInstanceRanking.resize();
if (chartInstanceStrategy) chartInstanceStrategy.resize();
}, 100);
}
collapseSidebarBtn.addEventListener('click', hideSidebar);
expandSidebarBtn.addEventListener('click', showSidebar);
// Mobile menu overlay handler
mobileMenuBtn.addEventListener('click', () => {
if (sidebar.classList.contains('hidden')) {
sidebar.classList.remove('hidden');
sidebar.classList.add('fixed', 'w-full', 'h-full');
} else {
sidebar.classList.add('hidden');
sidebar.classList.remove('fixed', 'w-full', 'h-full');
}
});
}
// Try auto-loading file from ./datasource/ตัวชี้วัด สนอ.68.xlsx
async function tryAutoFetchDefaultExcel() {
try {
const response = await fetch('./datasource/ตัวชี้วัด สนอ.68.xlsx');
if (response.ok) {
const arrayBuffer = await response.arrayBuffer();
const data = new Uint8Array(arrayBuffer);
const workbook = XLSX.read(data, { type: 'array' });
processWorkbook(workbook, "ไฟล์อัตโนมัติ (ตัวชี้วัด สนอ.68.xlsx)");
} else {
console.log("Could not auto-load default Excel file from datasource path. Awaiting manual upload or click to demo.");
}
} catch (error) {
console.log("Auto-fetch skipped or failed. Run on local web-server or upload manually. Details: ", error.message);
}
}
// Initialize with high-quality Demo Data
document.getElementById('loadDemoBtn').addEventListener('click', () => loadDemoDataset());
document.getElementById('quickDemoBtn').addEventListener('click', () => loadDemoDataset());
function loadDemoDataset() {
processWorkbook(demoExcelRawMatrix, "ข้อมูลจำลองระบบประเมิน (Demo Mode)");
}
// Configure upload area behaviors
function setupDragAndDrop() {
const dropzone = document.getElementById('dropzone');
const mainFileIn = document.getElementById('mainFileIn');
const sidebarFileIn = document.getElementById('sidebarFileIn');
const sidebarFileZone = document.getElementById('sidebarFileZone');
const changeFileTriggerBtn = document.getElementById('changeFileTriggerBtn');
// Set triggers
dropzone.addEventListener('click', () => mainFileIn.click());
sidebarFileZone.addEventListener('click', () => sidebarFileIn.click());
changeFileTriggerBtn.addEventListener('click', () => mainFileIn.click());
mainFileIn.addEventListener('change', handleFileInput);
sidebarFileIn.addEventListener('change', handleFileInput);
// Drag Events
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
dropzone.classList.add('border-cyber-emerald', 'bg-cyber-emerald/10');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
dropzone.classList.remove('border-cyber-emerald', 'bg-cyber-emerald/10');
}, false);
});
dropzone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
processUploadedFile(files[0]);
}
});
}
function handleFileInput(e) {
const files = e.target.files;
if (files.length > 0) {
processUploadedFile(files[0]);
}
}
function processUploadedFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
const data = new Uint8Array(e.target.result);
try {
const workbook = XLSX.read(data, { type: 'array' });
processWorkbook(workbook, file.name);
} catch (error) {
alert("เกิดข้อผิดพลาดในการเปิดและแปลงข้อมูลไฟล์ Excel นี้ กรุณาตรวจสอบรูปแบบไฟล์ของคุณ!");
console.error(error);
}
};
reader.readAsArrayBuffer(file);
}
// Clean & Flatten Matrix Structure
function processWorkbook(workbook, sourceName) {
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
// Format sheet range to array matrix
const rawRows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null });
if (rawRows.length < 2) {
alert("ข้อมูลในตารางไม่ครบถ้วนหรือไม่ถูกต้องตามรูปแบบ");
return;
}
// Attempt to locate departments columns dynamically in the first 3 rows
const departmentColIndices = {};
let headerFound = false;
for (let r = 0; r < Math.min(rawRows.length, 3); r++) {
const currentRow = rawRows[r];
if (!currentRow) continue;
for (let c = 0; c < currentRow.length; c++) {
const cellVal = currentRow[c] ? String(currentRow[c]).trim() : '';
if (DEPARTMENTS.includes(cellVal)) {
departmentColIndices[cellVal] = c;
headerFound = true;
}
}
if (headerFound) break;
}
// If headers were not dynamically discovered, use manual mapping
if (Object.keys(departmentColIndices).length === 0) {
DEPARTMENTS.forEach((dept, index) => {
departmentColIndices[dept] = index + 1; // Default to column offsets next to KPI text
});
}
const parsedIndicators = [];
let currentStrategyTheme = "ยังไม่ระบุประเด็นยุทธศาสตร์";
let excludedAveragesCount = 0;
// Iterate matrix starting beyond basic headers
for (let r = 0; r < rawRows.length; r++) {
const row = rawRows[r];
if (!row || row.length === 0) continue;
const firstCell = row[0] ? String(row[0]).trim() : '';
if (!firstCell) continue;
// 1. Detect Strategy Header Row (Regex e.g., "1 : ..." or "1. ...")
const strategyPattern = /^(\d+)\s*[:.]\s*(.*)$/;
if (strategyPattern.test(firstCell)) {
// Check to verify it isn't an ignore target
if (!firstCell.includes("เฉลี่ย") && !firstCell.includes("ร้อยละ")) {
currentStrategyTheme = firstCell;
continue; // Proceed to sub-indicators
}
}
// 2. Filter out rows with statistics, summary stats, percentages, etc.
const testLower = firstCell.toLowerCase();
if (
testLower.includes("เฉลี่ย") ||
testLower.includes("คะแนนเฉลี่ย") ||
testLower.includes("เฉลี่ยทุกประเด็น") ||
testLower.includes("ร้อยละ") && (testLower.includes("เฉลี่ย") || testLower.includes("ผลรวม") || testLower.includes("ภาพรวม"))
) {
excludedAveragesCount++;
continue; // Skip calculating averages to avoid double counts
}
// Parse valid sub-KPI details
let kpiCode = "N/A";
let kpiName = firstCell;
// Strip indicator codes if present
const codeMatch = firstCell.match(/^(\d+(\.\d+)+)\s*(.*)$/);
if (codeMatch) {
kpiCode = codeMatch[1];
kpiName = codeMatch[3].trim();
}
// Extract evaluations and unpivot columns
DEPARTMENTS.forEach(dept => {
const colIndex = departmentColIndices[dept];
let scoreValue = null;
if (colIndex !== undefined && colIndex < row.length) {
const rawCellScore = row[colIndex];
if (rawCellScore !== null && rawCellScore !== undefined && rawCellScore !== '') {
const parsedNum = Number(rawCellScore);
if (!isNaN(parsedNum)) {
scoreValue = parsedNum;
}
}
}
parsedIndicators.push({
strategy: currentStrategyTheme,
kpi_code: kpiCode,
kpi_name: kpiName,
department: dept,
score: scoreValue // Retain null when not evaluated
});
});
}
// Save clean arrays
rawIndicatorsData = parsedIndicators;
// Build list of unique strategies
const strategiesSet = new Set(rawIndicatorsData.map(item => item.strategy));
allUniqueStrategies = Array.from(strategiesSet);
// Populate strategic options to dropdown
const filterStrategyEl = document.getElementById('filterStrategy');
filterStrategyEl.innerHTML = '<option value="all">-- ทั้งหมด --</option>';
allUniqueStrategies.forEach(strat => {
const opt = document.createElement('option');
opt.value = strat;
opt.textContent = strat.length > 55 ? strat.substring(0, 55) + '...' : strat;
filterStrategyEl.appendChild(opt);
});
// Update UI
document.getElementById('dataStatusBadge').textContent = 'โหลดเสร็จสิ้น';
document.getElementById('dataStatusBadge').className = 'px-2 py-0.5 rounded-full text-[10px] font-medium bg-cyber-emerald/10 text-cyber-mint border border-cyber-emerald/20';
document.getElementById('dataSourceBadge').textContent = `ฐานข้อมูล: ${sourceName}`;
document.getElementById('kpiExcludedSummaryCount').textContent = `${excludedAveragesCount} แถวสถิติ`;
// Transition screens
document.getElementById('uploadScreen').classList.add('hidden');
document.getElementById('dashboardContent').classList.remove('hidden');
// Apply filter computation
applyFilters();
}
// Configure change event interactions
function setupInteractiveFilters() {
document.getElementById('filterStrategy').addEventListener('change', applyFilters);
document.getElementById('filterDept').addEventListener('change', applyFilters);
const thresholdSlider = document.getElementById('scoreThresholdSlider');
const thresholdVal = document.getElementById('scoreThresholdVal');
thresholdSlider.addEventListener('input', (e) => {
const val = parseFloat(e.target.value).toFixed(1);
thresholdVal.textContent = `≥ ${val}`;
applyFilters();
});
document.getElementById('resetFiltersBtn').addEventListener('click', () => {
document.getElementById('filterStrategy').value = 'all';
document.getElementById('filterDept').value = 'all';
document.getElementById('scoreThresholdSlider').value = '1.0';
document.getElementById('scoreThresholdVal').textContent = '≥ 1.0';
document.getElementById('tableSearchIn').value = '';
applyFilters();
});
document.getElementById('tableSearchIn').addEventListener('input', applyFilters);
document.getElementById('exportRawExcelBtn').addEventListener('click', exportCleanedExcel);
}
// Filter and compile metrics in real-time
function applyFilters() {
const selectedStrategy = document.getElementById('filterStrategy').value;
const selectedDept = document.getElementById('filterDept').value;
const thresholdVal = parseFloat(document.getElementById('scoreThresholdSlider').value);
const searchText = document.getElementById('tableSearchIn').value.trim().toLowerCase();
// Track if filters are active
const filtersActive = selectedStrategy !== 'all' || selectedDept !== 'all' || thresholdVal > 1.0 || searchText !== '';
document.getElementById('activeFiltersBadge').classList.toggle('hidden', !filtersActive);
// Step 1: Filter raw tabular values
cleanFilteredData = rawIndicatorsData.filter(item => {
if (selectedStrategy !== 'all' && item.strategy !== selectedStrategy) return false;
if (selectedDept !== 'all' && item.department !== selectedDept) return false;
if (item.score !== null && item.score < thresholdVal) return false;
if (searchText) {
const matchName = item.kpi_name.toLowerCase().includes(searchText);
const matchCode = item.kpi_code.toLowerCase().includes(searchText);
const matchStrategy = item.strategy.toLowerCase().includes(searchText);
if (!matchName && !matchCode && !matchStrategy) return false;
}
return true;
});
// Step 2: Compute core executive indices
renderExecutiveKPICards();
// Step 3: Draw charts
renderRankingChart();
renderStrategyRadarChart();
// Step 4: Populate Data Table
renderDataTable();
}
// Calculate and Render Executive KPI values
function renderExecutiveKPICards() {
// Find overall สนอ average score from filtered indicators
const overallScoreElements = cleanFilteredData.filter(item => item.department === 'สนอ.' && item.score !== null);
let avgScoreText = "ไม่มีข้อมูล";
if (overallScoreElements.length > 0) {
const total = overallScoreElements.reduce((acc, curr) => acc + curr.score, 0);
avgScoreText = (total / overallScoreElements.length).toFixed(2);
}
document.getElementById('kpiAvgScore').textContent = avgScoreText;
// Dynamically calculate top performer
// Compute averages for each department excluding 'สนอ.' (overall) to find sub-units
const deptAverages = {};
DEPARTMENTS.filter(d => d !== 'สนอ.').forEach(dept => {
const deptScores = cleanFilteredData.filter(item => item.department === dept && item.score !== null);
if (deptScores.length > 0) {
const sum = deptScores.reduce((acc, c) => acc + c.score, 0);
deptAverages[dept] = sum / deptScores.length;
}
});
let topPerformer = "N/A";
let topScore = 0;
Object.keys(deptAverages).forEach(dept => {
if (deptAverages[dept] > topScore) {
topScore = deptAverages[dept];
topPerformer = dept;
}
});
// Unit Map Helper
const fullNameMap = {
'กก.': 'กก. (กองกลาง)',
'กนผ.': 'กนผ. (นโยบายและแผน)',
'กบค.': 'กบค. (บริหารงานบุคคล)',
'กบศ.': 'กบศ. (บริการการศึกษา)',
'กพน.': 'กพน. (พัฒนานักศึกษา)',
'กค.': 'กค. (กองคลัง)'
};
document.getElementById('kpiTopPerformer').textContent = topPerformer;
document.getElementById('kpiTopPerformerScore').textContent = topScore > 0 ? `เฉลี่ย ${topScore.toFixed(2)} คะแนน` : '-';
// Total Unique active KPI items (Each unique KPI code / name is evaluated across multiple depts)
const uniqueKPIs = new Set(cleanFilteredData.map(item => item.kpi_code + '-' + item.kpi_name));
document.getElementById('kpiActiveCount').textContent = uniqueKPIs.size;
}
// Horizontal Ranking Chart of Departments
function renderRankingChart() {
const ctx = document.getElementById('rankingChart').getContext('2d');
// Compute values for departments
const labels = DEPARTMENTS;
const values = DEPARTMENTS.map(dept => {
const deptData = cleanFilteredData.filter(item => item.department === dept && item.score !== null);
if (deptData.length === 0) return 0;
return parseFloat((deptData.reduce((acc, c) => acc + c.score, 0) / deptData.length).toFixed(2));
});
if (chartInstanceRanking) {
chartInstanceRanking.destroy();
}
// Create gradient for the charts
const gradient = ctx.createLinearGradient(0, 0, 400, 0);
gradient.addColorStop(0, '#10B981'); // Emerald Green
gradient.addColorStop(1, '#34D399'); // Mint Green
chartInstanceRanking = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'คะแนนเฉลี่ยสะสม',
data: values,
backgroundColor: gradient,
borderRadius: 6,
borderWidth: 0,
barThickness: 16
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#161F30',
titleColor: '#F1F5F9',
bodyColor: '#34D399',
bodyFont: { family: 'JetBrains Mono', size: 12 },
titleFont: { family: 'Noto Sans Thai', size: 12 },
borderColor: '#1F2937',
borderWidth: 1
}
},
scales: {
x: {
min: 0,
max: 5,
grid: { color: 'rgba(16, 185, 129, 0.05)' },
ticks: { color: '#64748B', font: { family: 'JetBrains Mono', size: 11 } }
},
y: {
grid: { display: false },
ticks: { color: '#F1F5F9', font: { family: 'Noto Sans Thai', size: 12, weight: 'bold' } }
}
}
}
});
}
// Radar/Theme Line comparison of strategies
function renderStrategyRadarChart() {
const ctx = document.getElementById('strategyChart').getContext('2d');
// Build strategy abbreviations for clean rendering
const shortStrategyLabels = allUniqueStrategies.map((s, idx) => `ยุทธศาสตร์ที่ ${idx + 1}`);
// Calculate score for each strategy across selected departments
const datasets = [];
const colors = [
'#34D399', // Mint (สนอ.)
'#60A5FA', // Blue (กก.)
'#F59E0B', // Amber (กนผ.)
'#EC4899', // Pink (กบค.)
'#8B5CF6', // Purple (กบศ.)
'#EF4444', // Red (กพน.)
'#10B981' // Emerald (กค.)
];
// Display Top 3 departments to avoid visualization clutter (Overall 'สนอ.' + 2 Top Performing Departments)
const selectedDept = document.getElementById('filterDept').value;
let targetDeptRenderList = [];
if (selectedDept !== 'all') {
targetDeptRenderList = [selectedDept];
} else {
targetDeptRenderList = ['สนอ.', 'กก.', 'กนผ.', 'กบค.']; // Standard comparison group
}
targetDeptRenderList.forEach((dept, idx) => {
const deptScoresByStrategy = allUniqueStrategies.map(strat => {
const subset = cleanFilteredData.filter(item => item.department === dept && item.strategy === strat && item.score !== null);
if (subset.length === 0) return 0;
return parseFloat((subset.reduce((acc, c) => acc + c.score, 0) / subset.length).toFixed(2));
});
datasets.push({
label: dept === 'สนอ.' ? 'สนอ. (ภาพรวม)' : `กองงาน ${dept}`,
data: deptScoresByStrategy,
borderColor: colors[idx % colors.length],
backgroundColor: colors[idx % colors.length] + '1A', // transparent fill
borderWidth: 2,
pointBackgroundColor: colors[idx % colors.length],
tension: 0.1
});
});
if (chartInstanceStrategy) {
chartInstanceStrategy.destroy();
}
// Radar structure requires multi-axis labels
chartInstanceStrategy = new Chart(ctx, {
type: 'radar',
data: {
labels: shortStrategyLabels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#F1F5F9', font: { family: 'Noto Sans Thai', size: 11 } }
},
tooltip: {
backgroundColor: '#161F30',
bodyFont: { family: 'JetBrains Mono', size: 12 },
borderColor: '#1F2937',
borderWidth: 1
}
},
scales: {
r: {
grid: { color: 'rgba(16, 185, 129, 0.1)' },
angleLines: { color: 'rgba(16, 185, 129, 0.1)' },
ticks: {
display: false,
stepSize: 1
},
pointLabels: {
color: '#F1F5F9',
font: { family: 'Noto Sans Thai', size: 11 }
},
suggestedMin: 1,
suggestedMax: 5
}
}
}
});
}
// Build Multi-Level Cross-tab matrix for data-table
function renderDataTable() {
const tableBody = document.getElementById('indicatorsTableBody');
tableBody.innerHTML = '';
// Group filtered data by unique Indicator Item (kpi_code & kpi_name)
const groupedMap = new Map();
cleanFilteredData.forEach(item => {
const key = `${item.kpi_code}-${item.kpi_name}`;
if (!groupedMap.has(key)) {
groupedMap.set(key, {
strategy: item.strategy,
code: item.kpi_code,
name: item.kpi_name,
scores: {}
});
}
groupedMap.get(key).scores[item.department] = item.score;
});
const uniqueRows = Array.from(groupedMap.values());
document.getElementById('tablePageStats').textContent = `แสดงผล ${uniqueRows.length} จากทั้งหมด ${uniqueRows.length} รายการ`;
if (uniqueRows.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="10" class="text-center p-8 text-cyber-muted text-xs">
<div class="flex flex-col items-center justify-center space-y-2">
<i data-lucide="info" class="w-8 h-8 text-cyber-muted"></i>
<span>ไม่พบข้อมูลตัวชี้วัดที่สอดคล้องกับเงื่อนไขการกรองและการค้นหา</span>
</div>
</td>
</tr>
`;
return;
}
// Loop and draw rows
uniqueRows.forEach(row => {
const tr = document.createElement('tr');
tr.className = 'hover:bg-cyber-black/40 transition duration-150';
// Format Strategy name
const shortStrategy = row.strategy.length > 40 ? row.strategy.substring(0, 40) + '...' : row.strategy;
// Build department cells
let deptCellsHtml = '';
DEPARTMENTS.forEach((dept, index) => {
const score = row.scores[dept];
let displayScore = '-';
let textClass = 'text-cyber-muted';
if (score !== undefined && score !== null) {
displayScore = score.toFixed(1);
if (score >= 4.5) {
textClass = 'text-cyber-mint font-bold text-glow';
} else if (score >= 3.5) {
textClass = 'text-cyber-slate font-medium';
} else {
textClass = 'text-amber-400 font-medium';
}
}
// Bold the overall average score (สนอ.)
if (dept === 'สนอ.') {
deptCellsHtml += `<td class="p-3 text-right font-mono font-bold text-sm ${textClass} border-r border-cyber-charcoal/40 bg-cyber-emerald/5">${displayScore}</td>`;
} else {
deptCellsHtml += `<td class="p-3 text-right font-mono text-xs ${textClass}">${displayScore}</td>`;
}
});
tr.innerHTML = `
<td class="p-3 text-xs text-cyber-muted max-w-xs truncate" title="${row.strategy}">${shortStrategy}</td>
<td class="p-3 text-xs font-mono font-bold text-cyber-mint">${row.code}</td>
<td class="p-3 text-xs text-white font-medium max-w-md truncate" title="${row.name}">${row.name}</td>
${deptCellsHtml}
`;
tableBody.appendChild(tr);
});
}
// Export data to a perfectly flattened standard format using SheetJS
function exportCleanedExcel() {
if (rawIndicatorsData.length === 0) return;
const structuredExportData = cleanFilteredData.map(item => ({
"ประเด็นยุทธศาสตร์": item.strategy,
"รหัสตัวชี้วัด": item.kpi_code,
"ชื่อตัวชี้วัดย่อย": item.kpi_name,
"กองงาน": item.department,
"คะแนนผลการประเมิน": item.score !== null ? item.score : "ยังไม่ได้ประเมิน"
}));
const worksheet = XLSX.utils.json_to_sheet(structuredExportData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "ข้อมูลตัวชี้วัดแบบคลี่แล้ว");
// Adjust column widths automatically
const max_len = structuredExportData.reduce((w, r) => Math.max(w, String(r["ชื่อตัวชี้วัดย่อย"]).length), 10);
worksheet['!cols'] = [ { wch: 35 }, { wch: 12 }, { wch: max_len }, { wch: 12 }, { wch: 18 } ];
XLSX.writeFile(workbook, "ตัวชี้วัด_สนอ_มรพส_68_คลี่ตารางล้างข้อมูลแล้ว.xlsx");
}
// Trigger visual diagram export to PNG
function downloadChartPng(chartId) {
let activeChart = null;
if (chartId === 'rankingChart') activeChart = chartInstanceRanking;
if (chartId === 'strategyChart') activeChart = chartInstanceStrategy;
if (activeChart) {
const imgUrl = activeChart.toBase64Image();
const a = document.createElement('a');
a.href = imgUrl;
a.download = `${chartId}_สนอ_มรพส_68.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}
</script>
</body>
</html>