Download index.html from manatwee/Test: direct link, hf CLI and curl.
- Browser
- Download file 61.1 kB
-
https://huggingface.co/spaces/manatwee/Test/resolve/main/index.html
- Command line
-
hf download hf://spaces/manatwee/Test/index.html
-
curl -L -o index.html https://huggingface.co/spaces/manatwee/Test/resolve/main/index.html
61.1 kB
| <html lang="th"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dashboard สถิติจำนวนรับเข้านักศึกษาใหม่ (Teal Premium & Admission Funnel)</title> | |
| <!-- Bootstrap 5 CSS --> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <!-- Font Awesome --> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <!-- SweetAlert2 CSS --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"> | |
| <!-- Leaflet CSS --> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> | |
| <!-- Google Fonts: Sarabun --> | |
| <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* กำหนดสีหลัก Teal พรีเมียม */ | |
| :root { | |
| --brand-50: #f0fdfa; | |
| --brand-100: #ccfbf1; | |
| --brand-200: #99f6e4; | |
| --brand-500: #14b8a6; | |
| --brand-600: #0d9488; /* สี Teal หลัก */ | |
| --brand-700: #0f766e; | |
| --brand-800: #115e59; | |
| --brand-900: #134e4a; | |
| } | |
| body { | |
| background-color: #f8fafc; | |
| font-family: 'Sarabun', sans-serif; | |
| color: #334155; | |
| } | |
| /* ดีไซน์ Sticky Header Navbar */ | |
| .header-navbar { | |
| background-color: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(8px); | |
| -webkit-backdrop-filter: blur(8px); | |
| border-bottom: 1px solid #f1f5f9; | |
| } | |
| /* ดีไซน์แผ่นการ์ดโมเดิร์น */ | |
| .custom-card { | |
| background: #ffffff; | |
| border: 1px solid #f1f5f9; | |
| border-radius: 1rem ; /* rounded-2xl */ | |
| box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); | |
| position: relative; | |
| } | |
| /* ฟอร์มตัวเลือกดรอปดาวน์ */ | |
| .custom-select { | |
| border-radius: 0.75rem ; /* rounded-xl */ | |
| border: 1px solid #e2e8f0; | |
| padding: 0.5rem 1rem; | |
| font-size: 0.875rem; | |
| color: #475569; | |
| outline: none; | |
| transition: all 0.2s ease; | |
| } | |
| .custom-select:focus { | |
| border-color: var(--brand-500) ; | |
| box-shadow: 0 0 0 4px var(--brand-100) ; | |
| } | |
| /* ตกแต่งส่วนเน้นการ์ดสรุป */ | |
| .stat-card-accent { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 4px; | |
| width: 100%; | |
| border-radius: 1rem 1rem 0 0; | |
| } | |
| .stat-applied { background-color: #3b82f6; } /* Blue */ | |
| .stat-paid { background-color: #6366f1; } /* Indigo */ | |
| .stat-eligible { background-color: #06b6d4; } /* Cyan */ | |
| .stat-registered { background-color: #0d9488; }/* Teal (Brand Primary) */ | |
| .icon-wrapper { | |
| width: 3rem; | |
| height: 3rem; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 0.75rem; | |
| } | |
| /* ปรับแต่งไอคอนสรุปสถิติ 4 ขั้นตอนเพื่อความสวยงามพรีเมียม */ | |
| .icon-applied { background-color: #eff6ff; color: #3b82f6; } | |
| .icon-paid { background-color: #f5f3ff; color: #6366f1; } | |
| .icon-eligible { background-color: #ecfeff; color: #06b6d4; } | |
| .icon-registered { background-color: var(--brand-50); color: var(--brand-600); } | |
| /* แผนที่แบบ Interactive ด้วย Leaflet */ | |
| .map-wrapper { | |
| height: 480px; | |
| width: 100%; | |
| position: relative; | |
| background-color: #ffffff; | |
| border-radius: 0 0 1rem 1rem; | |
| overflow: hidden; | |
| border-top: 1px solid #f1f5f9; | |
| } | |
| #map { | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| /* ตกแต่งหน้าต่าง Popup ของแผนที่ */ | |
| .leaflet-popup-content-wrapper { | |
| border-radius: 0.75rem; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| border: 1px solid #f1f5f9; | |
| } | |
| .leaflet-popup-content { | |
| font-family: 'Sarabun', sans-serif; | |
| color: #475569; | |
| margin: 12px 15px; | |
| line-height: 1.5; | |
| } | |
| /* ตกแต่งตารางสไตล์ Drill-down หลายคอลัมน์ (แก้ปัญหาเส้นตารางเบี้ยว) */ | |
| .table-custom { | |
| border-collapse: collapse; | |
| font-size: 0.875rem; | |
| } | |
| .table-custom thead th { | |
| background-color: #f8fafc; | |
| color: #64748b; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| font-size: 0.75rem; | |
| letter-spacing: 0.05em; | |
| padding: 1rem 1.25rem; | |
| border-bottom: 1px solid #e2e8f0; | |
| } | |
| .clickable-row { | |
| cursor: pointer; | |
| user-select: none; | |
| transition: background-color 0.15s ease; | |
| } | |
| .clickable-row:hover { | |
| background-color: #f8fafc ; | |
| } | |
| .toggle-icon { | |
| width: 20px; | |
| text-align: center; | |
| transition: transform 0.2s; | |
| } | |
| /* สไตล์ไอคอนแถวในตารางย่อย */ | |
| .nested-icon-prov { | |
| width: 1.75rem; | |
| height: 1.75rem; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 0.5rem; | |
| background-color: var(--brand-50); | |
| color: var(--brand-600); | |
| } | |
| .nested-icon-school { | |
| width: 1.5rem; | |
| height: 1.5rem; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 0.375rem; | |
| background-color: #fffbeb; | |
| color: #d97706; | |
| } | |
| .nested-icon-faculty { | |
| width: 1.25rem; | |
| height: 1.25rem; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 0.375rem; | |
| background-color: var(--brand-50); | |
| color: var(--brand-600); | |
| } | |
| /* ==================================================== */ | |
| /* แก้ไขและล็อกขนาด/คู่สีของป้าย Badge ในตารางให้สอดคล้องกันและคมชัด */ | |
| /* ==================================================== */ | |
| .table-custom .badge { | |
| display: inline-block ; | |
| min-width: 60px ; /* กำหนดความกว้างขั้นต่ำเท่ากันเพื่อให้ป้ายตรงแนวระนาบสวยงาม */ | |
| text-align: center ; | |
| padding: 0.25rem 0.5rem ; /* คุมความสูงและ Padding ของป้ายเท่ากันทุกระดับชั้น */ | |
| } | |
| .badge-applied { | |
| background-color: #eff6ff ; /* Soft Blue */ | |
| color: #1e40af ; /* Dark Blue Text */ | |
| border: 1px solid #bfdbfe ; | |
| font-weight: 700 ; | |
| } | |
| .badge-paid { | |
| background-color: #e0e7ff ; /* Soft Indigo */ | |
| color: #3730a3 ; /* Dark Indigo Text */ | |
| border: 1px solid #c7d2fe ; | |
| font-weight: 700 ; | |
| } | |
| .badge-eligible { | |
| background-color: #ecfeff ; /* Soft Cyan */ | |
| color: #0e7490 ; /* Dark Cyan Text */ | |
| border: 1px solid #a5f3fc ; | |
| font-weight: 700 ; | |
| } | |
| .badge-registered { | |
| background-color: var(--brand-50) ; /* Soft Brand Teal */ | |
| color: var(--brand-800) ; /* Dark Brand Teal Text */ | |
| border: 1px solid var(--brand-200) ; | |
| font-weight: 700 ; | |
| } | |
| /* สไตล์ Progress Bar ในช่องจัดอันดับสถิติสูง */ | |
| .custom-progress { | |
| height: 6px; | |
| border-radius: 999px; | |
| background-color: #f1f5f9; | |
| overflow: hidden; | |
| } | |
| .custom-progress-bar { | |
| height: 100%; | |
| background-color: var(--brand-500); | |
| border-radius: 999px; | |
| transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* เอฟเฟคกะพริบไฮไลท์แถว */ | |
| .highlight-row { | |
| animation: highlight-effect 2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| @keyframes highlight-effect { | |
| 0% { background-color: rgba(20, 184, 166, 0.25) ; } | |
| 100% { background-color: transparent; } | |
| } | |
| </style> | |
| </head> | |
| <body class="antialiased"> | |
| <!-- Premium Header Navbar --> | |
| <header class="header-navbar sticky-top shadow-sm"> | |
| <div class="container-xxl py-3 px-4 d-flex align-items-center justify-content-between"> | |
| <div class="d-flex align-items-center"> | |
| <div class="icon-wrapper bg-brand-50 text-brand-600 me-3"> | |
| <i class="fa-solid fa-graduation-cap fs-4"></i> | |
| </div> | |
| <div> | |
| <span class="text-uppercase text-secondary tracking-wider font-semibold" style="font-size: 0.65rem; letter-spacing: 0.05em;">ระบบวิเคราะห์ขั้นตอนสารสนเทศ</span> | |
| <h1 class="h6 mb-0 fw-bold text-dark">ระบบวิเคราะห์ข้อมูลสถิติผู้สมัครและสัดส่วนรับเข้าศึกษา</h1> | |
| </div> | |
| </div> | |
| <button onclick="showInfo()" class="btn btn-white border border-secondary-subtle d-flex align-items-center gap-2 px-3 py-1.5 rounded-3 text-secondary font-medium shadow-sm transition"> | |
| <i class="fa-solid fa-circle-info text-brand-600"></i> | |
| <span class="small">รายละเอียดระบบ</span> | |
| </button> | |
| </div> | |
| </header> | |
| <main class="container-xxl px-4 py-4" style="max-width: 80rem;"> | |
| <!-- Filter Card --> | |
| <section class="custom-card p-4 mb-4"> | |
| <div class="row align-items-center g-3"> | |
| <div class="col-xl-2 col-md-12"> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="d-inline-flex align-items-center justify-content-center" style="width: 2rem; height: 2rem; background-color: #f1f5f9; border-radius: 0.5rem;"> | |
| <i class="fa-solid fa-filter text-secondary"></i> | |
| </span> | |
| <h2 class="h6 mb-0 fw-bold text-secondary">ตัวกรองระบบวิเคราะห์:</h2> | |
| </div> | |
| </div> | |
| <div class="col-xl-10 col-md-12"> | |
| <div class="row g-3"> | |
| <div class="col-md-3"> | |
| <!-- แผนก/ระดับการศึกษา (ค่าเริ่มต้น: ภาคปกติ) --> | |
| <select id="programFilter" onchange="applyFilters()" class="form-select custom-select w-full"> | |
| <option value="">-- แผนก/ระดับการศึกษาทั้งหมด --</option> | |
| <option value="ภาคปกติ" selected>ภาคปกติ</option> | |
| <option value="ภาคพิเศษ">ภาคพิเศษ</option> | |
| <option value="ระดับบัณฑิตศึกษา">ระดับบัณฑิตศึกษา</option> | |
| </select> | |
| </div> | |
| <div class="col-md-3"> | |
| <!-- รอบการรับสมัคร --> | |
| <select id="roundFilter" onchange="applyFilters()" class="form-select custom-select w-full"> | |
| <option value="">-- รอบการสมัครทั้งหมด --</option> | |
| </select> | |
| </div> | |
| <div class="col-md-3"> | |
| <!-- คณะ --> | |
| <select id="facultyFilter" onchange="onFacultyChange()" class="form-select custom-select w-full"> | |
| <option value="">-- คณะทั้งหมด --</option> | |
| </select> | |
| </div> | |
| <div class="col-md-3"> | |
| <!-- สาขาวิชา --> | |
| <select id="majorFilter" onchange="applyFilters()" class="form-select custom-select w-full"> | |
| <option value="">-- สาขาวิชาทั้งหมด --</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Summary Cards Funnel Grid --> | |
| <section class="row g-4 mb-4"> | |
| <!-- ขั้นที่ 1: ผู้สมัครสะสม --> | |
| <div class="col-lg-3 col-sm-6"> | |
| <div class="custom-card p-4 h-100 shadow-sm"> | |
| <div class="stat-card-accent stat-applied"></div> | |
| <div class="d-flex align-items-center justify-content-between"> | |
| <div> | |
| <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ผู้สมัครสะสม</p> | |
| <h3 id="statApplied" class="h3 fw-extrabold text-dark mb-0">0</h3> | |
| </div> | |
| <div class="icon-wrapper icon-applied"> | |
| <i class="fa-solid fa-file-signature fs-5"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ขั้นที่ 2: ชำระค่าสมัครแล้ว --> | |
| <div class="col-lg-3 col-sm-6"> | |
| <div class="custom-card p-4 h-100 shadow-sm"> | |
| <div class="stat-card-accent stat-paid"></div> | |
| <div class="d-flex align-items-center justify-content-between"> | |
| <div> | |
| <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ชำระค่าสมัครแล้ว</p> | |
| <h3 id="statPaid" class="h3 fw-extrabold text-dark mb-0">0</h3> | |
| </div> | |
| <div class="icon-wrapper icon-paid"> | |
| <i class="fa-solid fa-receipt fs-5"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ขั้นที่ 3: ผู้มีสิทธิ์เข้าศึกษา --> | |
| <div class="col-lg-3 col-sm-6"> | |
| <div class="custom-card p-4 h-100 shadow-sm"> | |
| <div class="stat-card-accent stat-eligible"></div> | |
| <div class="d-flex align-items-center justify-content-between"> | |
| <div> | |
| <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ผู้มีสิทธิ์เข้าศึกษา</p> | |
| <h3 id="statEligible" class="h3 fw-extrabold text-dark mb-0">0</h3> | |
| </div> | |
| <div class="icon-wrapper icon-eligible"> | |
| <i class="fa-solid fa-user-check fs-5"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ขั้นที่ 4: ผู้ชำระเงินรายงานตัว --> | |
| <div class="col-lg-3 col-sm-6"> | |
| <div class="custom-card p-4 h-100 shadow-sm"> | |
| <div class="stat-card-accent stat-registered"></div> | |
| <div class="d-flex align-items-center justify-content-between"> | |
| <div> | |
| <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ผู้ชำระเงินรายงานตัว</p> | |
| <h3 id="statRegistered" class="h3 fw-extrabold text-dark mb-0">0</h3> | |
| </div> | |
| <div class="icon-wrapper icon-registered"> | |
| <i class="fa-solid fa-id-card fs-5"></i> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Top 5 Analysis Panel --> | |
| <section class="row g-4 mb-4"> | |
| <!-- Top 5 Provinces --> | |
| <div class="col-lg-6"> | |
| <div class="custom-card p-4 shadow-sm h-100"> | |
| <div class="d-flex align-items-center justify-content-between mb-4 border-bottom pb-2"> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="text-brand-600"><i class="fa-solid fa-trophy text-brand-600"></i></span> | |
| <h4 class="h6 mb-0 fw-bold text-dark">Top 5 จังหวัดรับเข้าสูงที่สุด</h4> | |
| </div> | |
| <span class="badge bg-slate-100 text-slate-500 font-medium small" style="font-size: 0.7rem; padding: 0.25rem 0.5rem;">นับตามผู้ชำระเงินรายงานตัว</span> | |
| </div> | |
| <div id="topProvincesList" class="d-flex flex-column gap-3"> | |
| <!-- รายการจัดอันดับจังหวัดจะถูกแสดงด้วย JavaScript --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Top 5 Feeder Schools --> | |
| <div class="col-lg-6"> | |
| <div class="custom-card p-4 shadow-sm h-100"> | |
| <div class="d-flex align-items-center justify-content-between mb-4 border-bottom pb-2"> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="text-warning"><i class="fa-solid fa-school-flag text-warning"></i></span> | |
| <h4 class="h6 mb-0 fw-bold text-dark">Top 5 สถานศึกษาป้อนผู้เรียนสูงที่สุด</h4> | |
| </div> | |
| <span class="badge bg-slate-100 text-slate-500 font-medium small" style="font-size: 0.7rem; padding: 0.25rem 0.5rem;">นับตามผู้ชำระเงินรายงานตัว</span> | |
| </div> | |
| <div id="topSchoolsList" class="d-flex flex-column gap-3"> | |
| <!-- รายการจัดอันดับโรงเรียนจะถูกแสดงด้วย JavaScript --> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Map Module --> | |
| <section class="custom-card overflow-hidden mb-4 shadow-sm"> | |
| <div class="d-flex align-items-center justify-content-between bg-light bg-opacity-50 px-4 py-3 border-bottom"> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="text-brand-600"><i class="fa-solid fa-map"></i></span> | |
| <h3 class="h6 mb-0 fw-bold text-dark">แผนที่วิเคราะห์การกระจายตัวผู้สมัคร (คลิกวงกลมสีเพื่อจำแนกสถิติ Funnel รายจังหวัด)</h3> | |
| </div> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="d-none d-sm-inline-flex align-items-center text-secondary me-2" style="font-size: 0.75rem;"> | |
| <i class="fa-solid fa-circle-nodes me-1 text-brand-600"></i> ซูมเข้า-ออกและขยายแผนที่ได้อย่างอิสระ | |
| </span> | |
| <button class="btn btn-xs btn-outline-secondary px-2.5 py-1 text-xs rounded-2" onclick="resetMapView()"> | |
| <i class="fa-solid fa-compress me-1"></i>คืนค่ามุมมอง | |
| </button> | |
| </div> | |
| </div> | |
| <div class="map-wrapper"> | |
| <!-- หน้าจอโหลด --> | |
| <div id="map-loading" class="position-absolute w-100 h-100 d-flex flex-column align-items-center justify-content-center bg-white" style="z-index: 1000; transition: opacity 0.3s;"> | |
| <div class="spinner-border text-teal mb-2" role="status" style="color: var(--brand-600);"></div> | |
| <span class="text-secondary small font-medium">กำลังจัดทำแผนที่สารสนเทศ...</span> | |
| </div> | |
| <div id="map"></div> | |
| </div> | |
| </section> | |
| <!-- Data Table Container --> | |
| <section class="custom-card p-4 shadow-sm"> | |
| <div class="d-flex align-items-center gap-2 mb-3"> | |
| <span class="text-brand-600"><i class="fa-solid fa-table-list"></i></span> | |
| <h3 class="h6 mb-0 fw-bold text-dark">สถิติเจาะลึกแบบลำดับขั้น (จังหวัด > สถานศึกษาเดิม > คณะ > สาขาวิชา)</h3> | |
| </div> | |
| <div class="table-responsive rounded-3 border border-light"> | |
| <table class="table table-custom table-hover align-middle mb-0"> | |
| <thead> | |
| <tr class="text-center"> | |
| <th scope="col" class="text-start" style="width: 40%">โครงสร้างหน่วยและสายการรับสมัคร</th> | |
| <th scope="col" style="width: 15%">ผู้สมัคร (คน)</th> | |
| <th scope="col" style="width: 15%">ชำระค่าสมัคร (คน)</th> | |
| <th scope="col" style="width: 15%">ผู้มีสิทธิ์ (คน)</th> | |
| <th scope="col" style="width: 15%">รายงานตัว (คน)</th> | |
| </tr> | |
| </thead> | |
| <tbody id="dataTableBody" class="border-top-0"> | |
| <!-- Filled by JavaScript --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Scripts --> | |
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <script> | |
| // ข้อมูลจำลองสถิติผู้สมัครและสัดส่วนรับสมัครในแต่ละ Funnel | |
| const rawData = [ | |
| { id: 1, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 250, paid: 240, eligible: 90, registered: 80 }, | |
| { id: 2, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", programType: "ภาคพิเศษ", applied: 150, paid: 140, eligible: 50, registered: 45 }, | |
| { id: 3, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมโยธา", round: "รอบที่ 2 Quota", programType: "ภาคปกติ", applied: 110, paid: 105, eligible: 20, registered: 15 }, | |
| { id: 4, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "บริหารธุรกิจ", major: "การตลาด", round: "รอบที่ 3 Admission", programType: "ภาคปกติ", applied: 80, paid: 75, eligible: 20, registered: 20 }, | |
| { id: 5, name: "โรงเรียนมหิดลวิทยานุสรณ์", province: "นครปฐม", faculty: "วิทยาศาสตร์", major: "ชีววิทยา", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 130, paid: 125, eligible: 60, registered: 55 }, | |
| { id: 6, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "บริหารธุรกิจ", major: "การบัญชี", round: "รอบที่ 2 Quota", programType: "ภาคปกติ", applied: 290, paid: 280, eligible: 105, registered: 100 }, | |
| { id: 7, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "มนุษยศาสตร์", major: "ภาษาอังกฤษ", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 220, paid: 210, eligible: 80, registered: 70 }, | |
| { id: 8, name: "โรงเรียนขอนแก่นวิทยายน", province: "ขอนแก่น", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 2 Quota", programType: "ภาคปกติ", applied: 250, paid: 240, eligible: 90, registered: 85 }, | |
| { id: 9, name: "โรงเรียนหาดใหญ่วิทยาลัย", province: "สงขลา", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 190, paid: 180, eligible: 70, registered: 65 }, | |
| { id: 10, name: "โรงเรียนภูเก็ตวิทยาลัย", province: "ภูเก็ต", faculty: "บริหารธุรกิจ", major: "ธุรกิจระหว่างประเทศ", round: "รอบที่ 3 Admission", programType: "ภาคปกติ", applied: 160, paid: 150, eligible: 65, registered: 60 } | |
| ]; | |
| // พิกัดและชุดสีจำแนกพื้นที่รายจังหวัดบนแผนที่ | |
| const provinceConfigs = { | |
| "กรุงเทพมหานคร": { coords: [13.7563, 100.5018], color: "#0d9488", fillColor: "#14b8a6" }, | |
| "นครปฐม": { coords: [13.8140, 100.0373], color: "#0284c7", fillColor: "#38bdf8" }, | |
| "พิษณุโลก": { coords: [16.8211, 100.2659], color: "#7c3aed", fillColor: "#a78bfa" }, | |
| "ขอนแก่น": { coords: [16.4322, 102.8335], color: "#d97706", fillColor: "#fbbf24" }, | |
| "สงขลา": { coords: [7.1898, 100.5954], color: "#db2777", fillColor: "#f472b6" }, | |
| "ภูเก็ต": { coords: [7.8804, 98.3923], color: "#ea580c", fillColor: "#fb923c" } | |
| }; | |
| let activeFilteredData = []; | |
| let map = null; | |
| let markerLayerGroup = null; | |
| const defaultMapCenter = [13.0, 101.0]; | |
| const defaultMapZoom = 6; | |
| // ฟังก์ชันช่วยจัดการเนื้อหาข้อความ DOM อย่างปลอดภัย | |
| function setInnerTextSafe(id, value) { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| el.innerText = value; | |
| } | |
| } | |
| // จัดการเริ่มต้นกระบวนการเมื่อ DOM พร้อม | |
| function initDashboard() { | |
| initDropdowns(); | |
| initLeafletMap(); | |
| applyFilters(); | |
| } | |
| if (document.readyState === 'complete' || document.readyState === 'interactive') { | |
| initDashboard(); | |
| } else { | |
| document.addEventListener('DOMContentLoaded', initDashboard); | |
| } | |
| function initDropdowns() { | |
| const facultyFilter = document.getElementById('facultyFilter'); | |
| if (facultyFilter) { | |
| const faculties = [...new Set(rawData.map(item => item.faculty))].sort(); | |
| faculties.forEach(fac => { | |
| const option = document.createElement('option'); | |
| option.value = fac; | |
| option.textContent = fac; | |
| facultyFilter.appendChild(option); | |
| }); | |
| } | |
| const roundFilter = document.getElementById('roundFilter'); | |
| if (roundFilter) { | |
| const rounds = [...new Set(rawData.map(item => item.round))].sort(); | |
| rounds.forEach(rnd => { | |
| const option = document.createElement('option'); | |
| option.value = rnd; | |
| option.textContent = rnd; | |
| roundFilter.appendChild(option); | |
| }); | |
| } | |
| } | |
| function initLeafletMap() { | |
| const mapContainer = document.getElementById('map'); | |
| if (!mapContainer) return; | |
| map = L.map('map', { | |
| center: defaultMapCenter, | |
| zoom: defaultMapZoom, | |
| zoomControl: true, | |
| attributionControl: false | |
| }); | |
| L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { | |
| maxZoom: 18 | |
| }).addTo(map); | |
| markerLayerGroup = L.layerGroup().addTo(map); | |
| const loader = document.getElementById('map-loading'); | |
| if (loader) { | |
| loader.style.opacity = '0'; | |
| setTimeout(() => { | |
| loader.classList.add('d-none'); | |
| }, 300); | |
| } | |
| // ปรับมุมมองพิกัดแผนที่เพื่อป้องกันการโหลดกระเบื้องสีเทาค้าง | |
| setTimeout(() => { | |
| if (map) { | |
| map.invalidateSize(); | |
| } | |
| }, 400); | |
| } | |
| function resetMapView() { | |
| if (map) { | |
| map.setView(defaultMapCenter, defaultMapZoom); | |
| } | |
| } | |
| function onFacultyChange() { | |
| const facultyEl = document.getElementById('facultyFilter'); | |
| const majorFilter = document.getElementById('majorFilter'); | |
| if (!facultyEl || !majorFilter) return; | |
| const faculty = facultyEl.value; | |
| majorFilter.innerHTML = '<option value="">-- สาขาวิชาทั้งหมด --</option>'; | |
| if (faculty) { | |
| const majors = [...new Set(rawData.filter(item => item.faculty === faculty).map(item => item.major))].sort(); | |
| majors.forEach(maj => { | |
| const option = document.createElement('option'); | |
| option.value = maj; | |
| option.textContent = maj; | |
| majorFilter.appendChild(option); | |
| }); | |
| } | |
| applyFilters(); | |
| } | |
| function applyFilters() { | |
| const facultyEl = document.getElementById('facultyFilter'); | |
| const majorEl = document.getElementById('majorFilter'); | |
| const roundEl = document.getElementById('roundFilter'); | |
| const programEl = document.getElementById('programFilter'); | |
| const faculty = facultyEl ? facultyEl.value : ""; | |
| const major = majorEl ? majorEl.value : ""; | |
| const round = roundEl ? roundEl.value : ""; | |
| const program = programEl ? programEl.value : "ภาคปกติ"; | |
| activeFilteredData = rawData.filter(item => { | |
| return (faculty === "" || item.faculty === faculty) && | |
| (major === "" || item.major === major) && | |
| (round === "" || item.round === round) && | |
| (program === "" || item.programType === program); | |
| }); | |
| updateStatsAndRankings(activeFilteredData); | |
| renderTable(activeFilteredData); | |
| updateLeafletMapMarkers(activeFilteredData); | |
| } | |
| function updateStatsAndRankings(data) { | |
| let totalApplied = 0; | |
| let totalPaid = 0; | |
| let totalEligible = 0; | |
| let totalRegistered = 0; | |
| let provinceCounts = {}; | |
| let schoolCounts = {}; | |
| data.forEach(item => { | |
| totalApplied += item.applied; | |
| totalPaid += item.paid; | |
| totalEligible += item.eligible; | |
| totalRegistered += item.registered; | |
| // รวมข้อมูลรายจังหวัด | |
| if (!provinceCounts[item.province]) { | |
| provinceCounts[item.province] = 0; | |
| } | |
| provinceCounts[item.province] += item.registered; | |
| // รวมข้อมูลรายโรงเรียน | |
| if (!schoolCounts[item.name]) { | |
| schoolCounts[item.name] = 0; | |
| } | |
| schoolCounts[item.name] += item.registered; | |
| }); | |
| // อัปเดตการ์ดสถิติ 4 ขั้นตอนอย่างปลอดภัย | |
| setInnerTextSafe('statApplied', totalApplied.toLocaleString()); | |
| setInnerTextSafe('statPaid', totalPaid.toLocaleString()); | |
| setInnerTextSafe('statEligible', totalEligible.toLocaleString()); | |
| setInnerTextSafe('statRegistered', totalRegistered.toLocaleString()); | |
| // ประมวลผลและแสดงผล Top 5 จังหวัด | |
| const sortedProvinces = Object.entries(provinceCounts) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 5); | |
| const topProvsContainer = document.getElementById('topProvincesList'); | |
| if (topProvsContainer) { | |
| topProvsContainer.innerHTML = ''; | |
| const maxProvVal = sortedProvinces[0] ? sortedProvinces[0][1] : 1; | |
| if (sortedProvinces.length === 0) { | |
| topProvsContainer.innerHTML = '<div class="text-center text-muted py-4 small">ไม่มีข้อมูลจัดอันดับในส่วนนี้</div>'; | |
| } else { | |
| sortedProvinces.forEach(([province, count], idx) => { | |
| const percent = (count / maxProvVal) * 100; | |
| const itemHTML = ` | |
| <div> | |
| <div class="d-flex justify-content-between align-items-center mb-1"> | |
| <span class="small fw-bold text-slate-700">${idx + 1}. จังหวัด${province}</span> | |
| <span class="small fw-extrabold text-brand-700">${count.toLocaleString()} คน</span> | |
| </div> | |
| <div class="custom-progress"> | |
| <div class="custom-progress-bar" style="width: ${percent}%"></div> | |
| </div> | |
| </div> | |
| `; | |
| topProvsContainer.insertAdjacentHTML('beforeend', itemHTML); | |
| }); | |
| } | |
| } | |
| // ประมวลผลและแสดงผล Top 5 โรงเรียน | |
| const sortedSchools = Object.entries(schoolCounts) | |
| .sort((a, b) => b[1] - a[1]) | |
| .slice(0, 5); | |
| const topSchoolsContainer = document.getElementById('topSchoolsList'); | |
| if (topSchoolsContainer) { | |
| topSchoolsContainer.innerHTML = ''; | |
| const maxSchoolVal = sortedSchools[0] ? sortedSchools[0][1] : 1; | |
| if (sortedSchools.length === 0) { | |
| topSchoolsContainer.innerHTML = '<div class="text-center text-muted py-4 small">ไม่มีข้อมูลจัดอันดับในส่วนนี้</div>'; | |
| } else { | |
| sortedSchools.forEach(([school, count], idx) => { | |
| const percent = (count / maxSchoolVal) * 100; | |
| const itemHTML = ` | |
| <div> | |
| <div class="d-flex justify-content-between align-items-center mb-1"> | |
| <span class="small fw-bold text-slate-700">${idx + 1}. ${school}</span> | |
| <span class="small fw-extrabold text-amber-700">${count.toLocaleString()} คน</span> | |
| </div> | |
| <div class="custom-progress"> | |
| <div class="custom-progress-bar bg-warning" style="width: ${percent}%"></div> | |
| </div> | |
| </div> | |
| `; | |
| topSchoolsContainer.insertAdjacentHTML('beforeend', itemHTML); | |
| }); | |
| } | |
| } | |
| } | |
| function updateLeafletMapMarkers(data) { | |
| if (!markerLayerGroup) return; | |
| markerLayerGroup.clearLayers(); | |
| const provinceSummary = {}; | |
| data.forEach(item => { | |
| if (!provinceSummary[item.province]) { | |
| provinceSummary[item.province] = { applied: 0, paid: 0, eligible: 0, registered: 0, schools: {} }; | |
| } | |
| provinceSummary[item.province].applied += item.applied; | |
| provinceSummary[item.province].paid += item.paid; | |
| provinceSummary[item.province].eligible += item.eligible; | |
| provinceSummary[item.province].registered += item.registered; | |
| provinceSummary[item.province].schools[item.name] = (provinceSummary[item.province].schools[item.name] || 0) + item.registered; | |
| }); | |
| for (const [provinceName, stats] of Object.entries(provinceSummary)) { | |
| const config = provinceConfigs[provinceName]; | |
| if (config && stats.applied > 0) { | |
| const radius = Math.max(12, Math.min(32, Math.sqrt(stats.registered) * 2.5)); | |
| const circle = L.circleMarker(config.coords, { | |
| radius: radius, | |
| color: config.color, | |
| fillColor: config.fillColor, | |
| fillOpacity: 0.75, | |
| weight: 2 | |
| }).addTo(markerLayerGroup); | |
| let schoolListHTML = Object.entries(stats.schools) | |
| .sort((a,b) => b[1] - a[1]) | |
| .map(([sch, c]) => `<div class="d-flex justify-content-between my-1 small"><span><i class="fa-solid fa-school text-warning me-1.5"></i>${sch}</span><span class="fw-bold text-dark">${c} คน</span></div>`) | |
| .join(''); | |
| const popupHTML = ` | |
| <div style="font-family:'Sarabun', sans-serif; min-width: 250px;"> | |
| <div class="d-flex align-items-center mb-2 pb-2 border-bottom"> | |
| <span class="nested-icon-prov me-2" style="background-color: ${config.fillColor}33; color: ${config.color};"> | |
| <i class="fa-solid fa-map-location-dot"></i> | |
| </span> | |
| <h6 class="fw-bold text-dark mb-0">จังหวัด${provinceName}</h6> | |
| </div> | |
| <div class="row g-2 mb-2 pb-2 border-bottom text-center"> | |
| <div class="col-6"> | |
| <span class="small text-muted d-block" style="font-size: 0.7rem;">ผู้สมัครสะสม</span> | |
| <span class="fw-bold text-primary">${stats.applied}</span> | |
| </div> | |
| <div class="col-6"> | |
| <span class="small text-muted d-block" style="font-size: 0.7rem;">ชำระค่าสมัคร</span> | |
| <span class="fw-bold text-indigo">${stats.paid}</span> | |
| </div> | |
| <div class="col-6 mt-1"> | |
| <span class="small text-muted d-block" style="font-size: 0.7rem;">ผู้มีสิทธิ์เข้าศึกษา</span> | |
| <span class="fw-bold text-info">${stats.eligible}</span> | |
| </div> | |
| <div class="col-6 mt-1"> | |
| <span class="small text-muted d-block" style="font-size: 0.7rem;">ชำระรายงานตัว</span> | |
| <span class="fw-extrabold text-success">${stats.registered}</span> | |
| </div> | |
| </div> | |
| <div> | |
| <span class="small text-muted d-block mb-1 font-semibold">รายชื่อสถานศึกษาเดิม (รายงานตัวแล้ว):</span> | |
| <div style="max-height: 80px; overflow-y: auto;" class="pe-1"> | |
| ${schoolListHTML} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| circle.bindPopup(popupHTML); | |
| } | |
| } | |
| } | |
| function focusOnProvinceOnMap(provinceName) { | |
| const config = provinceConfigs[provinceName]; | |
| if (config && map) { | |
| map.flyTo(config.coords, 10, { duration: 1.2 }); | |
| } | |
| } | |
| function renderTable(data) { | |
| const tbody = document.getElementById('dataTableBody'); | |
| if (!tbody) return; | |
| tbody.innerHTML = ''; | |
| if(data.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger py-5 fw-medium">ไม่พบข้อมูลที่ตรงกับเงื่อนไขตัวกรองในขณะนี้</td></tr>'; | |
| return; | |
| } | |
| const grouped = {}; | |
| data.forEach(item => { | |
| if (!grouped[item.province]) grouped[item.province] = { applied: 0, paid: 0, eligible: 0, registered: 0, schools: {} }; | |
| grouped[item.province].applied += item.applied; | |
| grouped[item.province].paid += item.paid; | |
| grouped[item.province].eligible += item.eligible; | |
| grouped[item.province].registered += item.registered; | |
| if (!grouped[item.province].schools[item.name]) { | |
| grouped[item.province].schools[item.name] = { applied: 0, paid: 0, eligible: 0, registered: 0, faculties: {} }; | |
| } | |
| grouped[item.province].schools[item.name].applied += item.applied; | |
| grouped[item.province].schools[item.name].paid += item.paid; | |
| grouped[item.province].schools[item.name].eligible += item.eligible; | |
| grouped[item.province].schools[item.name].registered += item.registered; | |
| if (!grouped[item.province].schools[item.name].faculties[item.faculty]) { | |
| grouped[item.province].schools[item.name].faculties[item.faculty] = { applied: 0, paid: 0, eligible: 0, registered: 0, majors: {} }; | |
| } | |
| grouped[item.province].schools[item.name].faculties[item.faculty].applied += item.applied; | |
| grouped[item.province].schools[item.name].faculties[item.faculty].paid += item.paid; | |
| grouped[item.province].schools[item.name].faculties[item.faculty].eligible += item.eligible; | |
| grouped[item.province].schools[item.name].faculties[item.faculty].registered += item.registered; | |
| if (!grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major]) { | |
| grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major] = { applied: 0, paid: 0, eligible: 0, registered: 0 }; | |
| } | |
| grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].applied += item.applied; | |
| grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].paid += item.paid; | |
| grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].eligible += item.eligible; | |
| grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].registered += item.registered; | |
| }); | |
| const sortedProvinces = Object.keys(grouped).sort((a, b) => grouped[b].registered - grouped[a].registered); | |
| let provIndex = 1; | |
| sortedProvinces.forEach(prov => { | |
| const provId = `prov-${provIndex}`; | |
| const provData = grouped[prov]; | |
| // --- แถวระดับที่ 1: จังหวัด --- | |
| const trProv = document.createElement('tr'); | |
| trProv.className = 'clickable-row border-bottom border-light'; | |
| trProv.onclick = () => { | |
| toggleRow(provId); | |
| focusOnProvinceOnMap(prov); | |
| }; | |
| trProv.innerHTML = ` | |
| <td class="px-4 py-3 text-start fw-bold text-dark"> | |
| <div class="d-flex align-items-center"> | |
| <i class="fa-solid fa-chevron-right text-secondary toggle-icon me-2" id="icon-${provId}"></i> | |
| <span class="nested-icon-prov me-2.5"> | |
| <i class="fa-solid fa-map-location-dot small"></i> | |
| </span> | |
| <span>จังหวัด${prov}</span> | |
| </div> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${provData.applied.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${provData.paid.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${provData.eligible.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8">${provData.registered.toLocaleString()}</span> | |
| </td> | |
| `; | |
| tbody.appendChild(trProv); | |
| const sortedSchools = Object.keys(provData.schools).sort((a, b) => provData.schools[b].registered - provData.schools[a].registered); | |
| let schIndex = 1; | |
| sortedSchools.forEach(sch => { | |
| const schId = `${provId}-sch-${schIndex}`; | |
| const schData = provData.schools[sch]; | |
| // --- แถวระดับที่ 2: สถานศึกษา --- | |
| const trSch = document.createElement('tr'); | |
| trSch.className = `child-${provId} clickable-row bg-white border-bottom border-light`; | |
| trSch.style.display = 'none'; | |
| trSch.onclick = () => toggleRow(schId); | |
| trSch.innerHTML = ` | |
| <td class="px-4 py-3 text-start fw-semibold text-secondary" style="padding-left: 2.75rem !important;"> | |
| <div class="d-flex align-items-center"> | |
| <i class="fa-solid fa-chevron-right text-secondary toggle-icon me-2" id="icon-${schId}"></i> | |
| <span class="nested-icon-school me-2"> | |
| <i class="fa-solid fa-school" style="font-size: 0.7rem;"></i> | |
| </span> | |
| <span>${sch}</span> | |
| </div> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${schData.applied.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${schData.paid.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${schData.eligible.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8">${schData.registered.toLocaleString()}</span> | |
| </td> | |
| `; | |
| tbody.appendChild(trSch); | |
| const sortedFaculties = Object.keys(schData.faculties).sort((a, b) => schData.faculties[b].registered - schData.faculties[a].registered); | |
| let facIndex = 1; | |
| sortedFaculties.forEach(fac => { | |
| const facId = `${schId}-fac-${facIndex}`; | |
| const facData = schData.faculties[fac]; | |
| // --- แถวระดับที่ 3: คณะ --- | |
| const trFac = document.createElement('tr'); | |
| trFac.className = `child-${schId} parent-${provId} clickable-row bg-white border-bottom border-light`; | |
| trFac.style.display = 'none'; | |
| trFac.onclick = () => toggleRow(facId); | |
| trFac.innerHTML = ` | |
| <td class="px-4 py-3 text-start fw-medium text-secondary" style="padding-left: 4.75rem !important;"> | |
| <div class="d-flex align-items-center"> | |
| <i class="fa-solid fa-chevron-right text-secondary toggle-icon me-2" id="icon-${facId}"></i> | |
| <span class="nested-icon-faculty me-2"> | |
| <i class="fa-solid fa-building-columns" style="font-size: 0.6rem;"></i> | |
| </span> | |
| <span>คณะ${fac}</span> | |
| </div> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${facData.applied.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${facData.paid.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${facData.eligible.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-3 text-center"> | |
| <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8">${facData.registered.toLocaleString()}</span> | |
| </td> | |
| `; | |
| tbody.appendChild(trFac); | |
| const sortedMajors = Object.keys(facData.majors).sort((a, b) => facData.majors[b].registered - facData.majors[a].registered); | |
| // --- แถวระดับที่ 4: สาขาวิชา --- | |
| sortedMajors.forEach(majorName => { | |
| const majorStats = facData.majors[majorName]; | |
| const trDet = document.createElement('tr'); | |
| trDet.className = `child-${facId} parent-${schId} parent-${provId} bg-light bg-opacity-25`; | |
| trDet.style.display = 'none'; | |
| trDet.innerHTML = ` | |
| <td class="px-4 py-2.5 text-start text-secondary" style="padding-left: 6.75rem !important;"> | |
| <div class="d-flex align-items-center"> | |
| <i class="fa-solid fa-turn-up fa-rotate-90 text-secondary-subtle me-3"></i> | |
| <span class="me-2"><i class="fa-solid fa-book-open text-secondary-subtle small me-1.5"></i>${majorName}</span> | |
| </div> | |
| </td> | |
| <td class="px-3 py-2.5 text-center"> | |
| <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${majorStats.applied.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-2.5 text-center"> | |
| <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${majorStats.paid.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-2.5 text-center"> | |
| <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${majorStats.eligible.toLocaleString()}</span> | |
| </td> | |
| <td class="px-3 py-2.5 text-center"> | |
| <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8"> | |
| ${majorStats.registered.toLocaleString()} | |
| </span> | |
| </td> | |
| `; | |
| tbody.appendChild(trDet); | |
| }); | |
| facIndex++; | |
| }); | |
| schIndex++; | |
| }); | |
| provIndex++; | |
| }); | |
| } | |
| function toggleRow(targetId) { | |
| const icon = document.getElementById(`icon-${targetId}`); | |
| if (!icon) return; | |
| const isExpanded = icon.classList.contains('fa-chevron-down'); | |
| if (isExpanded) { | |
| icon.classList.replace('fa-chevron-down', 'fa-chevron-right'); | |
| const children = document.querySelectorAll(`.child-${targetId}`); | |
| children.forEach(el => el.style.display = 'none'); | |
| const descendants = document.querySelectorAll(`.parent-${targetId}`); | |
| descendants.forEach(el => el.style.display = 'none'); | |
| children.forEach(row => { | |
| const subIcon = row.querySelector('.toggle-icon'); | |
| if (subIcon && subIcon.classList.contains('fa-chevron-down')) { | |
| subIcon.classList.replace('fa-chevron-down', 'fa-chevron-right'); | |
| } | |
| }); | |
| descendants.forEach(row => { | |
| const subIcon = row.querySelector('.toggle-icon'); | |
| if (subIcon && subIcon.classList.contains('fa-chevron-down')) { | |
| subIcon.classList.replace('fa-chevron-down', 'fa-chevron-right'); | |
| } | |
| }); | |
| } else { | |
| icon.classList.replace('fa-chevron-right', 'fa-chevron-down'); | |
| const children = document.querySelectorAll(`.child-${targetId}`); | |
| children.forEach(el => el.style.display = 'table-row'); | |
| } | |
| } | |
| function showInfo() { | |
| Swal.fire({ | |
| title: 'ข้อมูลระบบรายงานสถิติใหม่', | |
| html: ` | |
| <div class="text-start mt-2" style="font-size: 0.875rem; color: #475569;"> | |
| <p class="font-bold text-teal-700 mb-2">โครงสร้างรายงานวิเคราะห์ระดับชั้นข้อมูลรับสมัครเรียน</p> | |
| <ul class="ps-3 mb-0" style="list-style-type: disc;"> | |
| <li class="mb-2"><b>จัดเรียงสีกราฟแนวตั้ง (Vertical Alignment):</b> ล็อกคู่สีประจำแต่ละดัชนีชี้วัดในการรับสมัคร ทำให้ตารางมีความคมชัด มีระเบียบ และวิเคราะห์ง่ายในแนวตั้ง</li> | |
| <li class="mb-2"><b>แก้ปัญหาเส้นตารางเบี้ยว (Render Layout Fix):</b> ยกเลิกการใช้ Flexbox บนเซลล์แท็ก TD โดยตรง และย้ายเข้าไปทำงานใน DIV ครอบดีไซน์ย่อยแทน เพื่อฟื้นฟูระนาบเส้นขอบตารางให้ตรงกันอย่างเนี๊ยบสวยงาม</li> | |
| <li class="mb-2"><b>แผนที่ Leaflet แสนเสรี:</b> ซูมส่องพิกัดพร้อมกล่องข้อมูลสรุปรายจังหวัดได้อย่างราบรื่น</li> | |
| </ul> | |
| </div> | |
| `, | |
| icon: 'info', | |
| confirmButtonText: 'ตกลง', | |
| confirmButtonColor: '#0d9488', | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |