Test / index.html
manatwee's picture
ปรับขนาดของจำนวนในตาราง
5a89e6c verified
Raw History Blame Contribute Delete
61.1 kB
<!DOCTYPE html>
<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 !important; /* rounded-2xl */
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
position: relative;
}
/* ฟอร์มตัวเลือกดรอปดาวน์ */
.custom-select {
border-radius: 0.75rem !important; /* 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) !important;
box-shadow: 0 0 0 4px var(--brand-100) !important;
}
/* ตกแต่งส่วนเน้นการ์ดสรุป */
.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 !important;
}
.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 !important;
min-width: 60px !important; /* กำหนดความกว้างขั้นต่ำเท่ากันเพื่อให้ป้ายตรงแนวระนาบสวยงาม */
text-align: center !important;
padding: 0.25rem 0.5rem !important; /* คุมความสูงและ Padding ของป้ายเท่ากันทุกระดับชั้น */
}
.badge-applied {
background-color: #eff6ff !important; /* Soft Blue */
color: #1e40af !important; /* Dark Blue Text */
border: 1px solid #bfdbfe !important;
font-weight: 700 !important;
}
.badge-paid {
background-color: #e0e7ff !important; /* Soft Indigo */
color: #3730a3 !important; /* Dark Indigo Text */
border: 1px solid #c7d2fe !important;
font-weight: 700 !important;
}
.badge-eligible {
background-color: #ecfeff !important; /* Soft Cyan */
color: #0e7490 !important; /* Dark Cyan Text */
border: 1px solid #a5f3fc !important;
font-weight: 700 !important;
}
.badge-registered {
background-color: var(--brand-50) !important; /* Soft Brand Teal */
color: var(--brand-800) !important; /* Dark Brand Teal Text */
border: 1px solid var(--brand-200) !important;
font-weight: 700 !important;
}
/* สไตล์ 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) !important; }
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">สถิติเจาะลึกแบบลำดับขั้น (จังหวัด &gt; สถานศึกษาเดิม &gt; คณะ &gt; สาขาวิชา)</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>