Spaces:
Running
Running
| <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> | |
| ต้องปรับปรุง (< 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> |