manatwee commited on
Commit
6464422
·
verified ·
1 Parent(s): 862be38

ปรับแผนที่และการ์ดแสดงภาพรวม

Browse files
Files changed (1) hide show
  1. index.html +777 -346
index.html CHANGED
@@ -3,191 +3,454 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Dashboard จำนวนรับเข้านักศึกษาใหม่ (Teal Theme)</title>
7
 
8
- <!-- Tailwind CSS (สำหรับโครงสร้างการจัดวางแบบพรีเมียมและยืดหยุ่น) -->
9
- <script src="https://cdn.tailwindcss.com"></script>
10
  <!-- Font Awesome -->
11
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
  <!-- SweetAlert2 CSS -->
13
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
 
 
14
  <!-- Google Fonts: Sarabun -->
15
  <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
16
 
17
- <!-- Configure Tailwind with custom brand colors -->
18
- <script>
19
- tailwind.config = {
20
- theme: {
21
- extend: {
22
- fontFamily: {
23
- sans: ['Sarabun', 'sans-serif'],
24
- },
25
- colors: {
26
- brand: {
27
- 50: '#f0fdfa',
28
- 100: '#ccfbf1',
29
- 200: '#99f6e4',
30
- 500: '#14b8a6',
31
- 600: '#0d9488', // Main Teal
32
- 700: '#0f766e',
33
- 800: '#115e59',
34
- 900: '#134e4a',
35
- }
36
- }
37
- }
38
- }
39
  }
40
- </script>
41
 
42
- <style>
43
- body { background-color: #f8fafc; } /* Soft Slate Background */
44
-
45
- /* สไตล์ตารางสไตล์ Drill-down */
46
- .clickable-row { cursor: pointer; user-select: none; transition: background-color 0.15s ease; }
47
- .toggle-icon { width: 20px; text-align: center; transition: transform 0.2s; }
48
-
49
- /* เอฟเฟคเมื่อคลิกเลือกจังหวัดจากแผนที่และส่งมาโฟกัสตาราง */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  .highlight-row {
51
  animation: highlight-effect 2s cubic-bezier(0.4, 0, 0.2, 1);
52
  }
53
  @keyframes highlight-effect {
54
- 0% { background-color: rgba(20, 184, 166, 0.25); } /* Light Teal Highlight */
55
  100% { background-color: transparent; }
56
  }
57
-
58
- /* แฟนซีพิกัดแผนภูมิประเทศไทย */
59
- .map-container { position: relative; height: 480px; width: 100%; }
60
- #regions_div { width: 100%; height: 100%; }
61
  </style>
62
  </head>
63
- <body class="font-sans text-slate-800 antialiased">
64
-
65
- <!-- Premium White Navbar with Teal Top Accent -->
66
- <header class="sticky top-0 z-50 border-b border-slate-100 bg-white/95 shadow-sm backdrop-blur">
67
- <div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
68
- <div class="flex items-center space-x-3">
69
- <div class="flex h-10 w-10 items-center justify-center rounded-xl bg-brand-50 text-brand-600">
70
- <i class="fa-solid fa-graduation-cap text-xl"></i>
71
  </div>
72
  <div>
73
- <span class="text-xs font-semibold tracking-wider text-slate-400">ระบบรายงานสารสนเทศ</span>
74
- <h1 class="text-lg font-bold text-slate-800">สถิติการรับเข้านักศึกษาใหม่</h1>
75
  </div>
76
  </div>
77
- <button onclick="showInfo()" class="flex items-center space-x-2 rounded-lg border border-slate-200 bg-white px-3.5 py-1.5 text-sm font-medium text-slate-600 shadow-sm transition hover:bg-slate-50">
78
  <i class="fa-solid fa-circle-info text-brand-600"></i>
79
- <span>ข้อมูลระบบ</span>
80
  </button>
81
  </div>
82
  </header>
83
 
84
- <main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
85
 
86
  <!-- Filter Card -->
87
- <section class="mb-6 rounded-2xl border border-slate-100 bg-white p-5 shadow-sm">
88
- <div class="flex flex-col space-y-4 lg:flex-row lg:items-center lg:justify-between lg:space-y-0">
89
- <div class="flex items-center space-x-2.5">
90
- <span class="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 text-slate-500">
91
- <i class="fa-solid fa-filter text-sm"></i>
92
- </span>
93
- <h2 class="text-sm font-bold text-slate-700">ตัวกรองวิเคราะห์ข้อมูล:</h2>
 
 
94
  </div>
95
- <div class="grid flex-1 gap-4 lg:ml-6 lg:grid-cols-3">
96
- <select id="facultyFilter" onchange="onFacultyChange()" class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-600 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100">
97
- <option value="">-- คณะทั้งหมด --</option>
98
- </select>
99
- <select id="majorFilter" onchange="applyFilters()" class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-600 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100">
100
- <option value="">-- สาขาวิชาทั้งหมด --</option>
101
- </select>
102
- <select id="roundFilter" onchange="applyFilters()" class="w-full rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-600 outline-none transition focus:border-brand-500 focus:ring-2 focus:ring-brand-100">
103
- <option value="">-- รอบการสมัครทั้งหมด --</option>
104
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  </div>
106
  </div>
107
  </section>
108
 
109
- <!-- Summary Cards Grid -->
110
- <section class="mb-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
111
- <!-- Stat 1 -->
112
- <div class="relative overflow-hidden rounded-2xl border border-slate-100 bg-white p-6 shadow-sm transition hover:shadow-md">
113
- <div class="absolute top-0 left-0 h-1 w-full bg-brand-500"></div>
114
- <div class="flex items-center justify-between">
115
- <div>
116
- <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">นักศึกษาผ่านการกรอง</p>
117
- <h3 id="totalStudents" class="mt-2 text-3xl font-extrabold text-slate-800">0</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </div>
119
- <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-brand-50 text-brand-600">
120
- <i class="fa-solid fa-users text-lg"></i>
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  </div>
122
  </div>
123
  </div>
 
124
 
125
- <!-- Stat 2 -->
126
- <div class="relative overflow-hidden rounded-2xl border border-slate-100 bg-white p-6 shadow-sm transition hover:shadow-md">
127
- <div class="absolute top-0 left-0 h-1 w-full bg-brand-500"></div>
128
- <div class="flex items-center justify-between">
129
- <div>
130
- <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">จังหวัดที่รับเข้ามากที่สุด</p>
131
- <h3 id="topProvince" class="mt-2 text-lg font-bold text-slate-700">-</h3>
 
 
 
 
132
  </div>
133
- <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-teal-50 text-teal-600">
134
- <i class="fa-solid fa-map-location-dot text-lg"></i>
135
  </div>
136
  </div>
137
  </div>
138
 
139
- <!-- Stat 3 -->
140
- <div class="relative overflow-hidden rounded-2xl border border-slate-100 bg-white p-6 shadow-sm transition hover:shadow-md sm:col-span-2 lg:col-span-1">
141
- <div class="absolute top-0 left-0 h-1 w-full bg-brand-500"></div>
142
- <div class="flex items-center justify-between">
143
- <div>
144
- <p class="text-xs font-semibold uppercase tracking-wider text-slate-400">สถานศึกษาที่ส่งเข้ามากที่สุด</p>
145
- <h3 id="topSchool" class="mt-2 text-base font-bold text-slate-700">-</h3>
 
 
146
  </div>
147
- <div class="flex h-12 w-12 items-center justify-center rounded-xl bg-amber-50 text-amber-600">
148
- <i class="fa-solid fa-school text-lg"></i>
149
  </div>
150
  </div>
151
  </div>
152
  </section>
153
 
154
  <!-- Map Module -->
155
- <section class="mb-8 rounded-2xl border border-slate-100 bg-white shadow-sm overflow-hidden">
156
- <div class="flex items-center justify-between border-b border-slate-100 bg-slate-50/50 px-6 py-4">
157
- <div class="flex items-center space-x-2">
158
  <span class="text-brand-600"><i class="fa-solid fa-map"></i></span>
159
- <h3 class="font-bold text-slate-800">แผนภูมิรูปแผนที่ประเทศ (เฉดสีจำแนกตามรายจังหวัด)</h3>
 
 
 
 
 
 
 
 
160
  </div>
161
- <span class="hidden text-xs text-slate-400 sm:inline-flex items-center">
162
- <i class="fa-solid fa-mouse-pointer mr-1.5 text-brand-500"></i> คลิกที่แต่ละจังหวัดเพื่อนำทางสู่ตารางสถิติด้านล่าง
163
- </span>
164
  </div>
165
- <div class="map-container flex items-center justify-center bg-white">
166
- <!-- Map Loader Spinner -->
167
- <div id="map-loading" class="absolute z-10 flex flex-col items-center justify-center text-slate-400">
168
- <div class="h-8 w-8 animate-spin rounded-full border-4 border-slate-200 border-t-brand-600"></div>
169
- <span class="mt-3 text-xs font-medium">กำลังเตรียมแผนที่ประเทศและคำนวณสถิติ...</span>
170
  </div>
171
- <div id="regions_div"></div>
172
  </div>
173
  </section>
174
 
175
  <!-- Data Table Container -->
176
- <section class="rounded-2xl border border-slate-100 bg-white p-6 shadow-sm">
177
- <div class="mb-4 flex items-center space-x-2">
178
  <span class="text-brand-600"><i class="fa-solid fa-table-list"></i></span>
179
- <h3 class="font-bold text-slate-800">โครงสร้างรายละเอียด (จังหวัด &gt; สถานศึกษาเดิม &gt; คณะ &gt; สาขาวิชา)</h3>
180
  </div>
181
 
182
- <div class="overflow-x-auto rounded-xl border border-slate-100">
183
- <table class="w-full border-collapse text-left text-sm text-slate-600">
184
- <thead class="bg-slate-50 text-xs font-bold uppercase tracking-wider text-slate-500">
185
- <tr class="border-b border-slate-100">
186
- <th scope="col" class="px-6 py-4" style="width: 75%">โครงสร้างหน่วยและสายการรับสมัคร</th>
187
- <th scope="col" class="px-6 py-4 text-center" style="width: 25%">จำนวนนักศึกษาใหม่ (คน)</th>
 
 
 
188
  </tr>
189
  </thead>
190
- <tbody id="dataTableBody" class="divide-y divide-slate-50">
191
  <!-- Filled by JavaScript -->
192
  </tbody>
193
  </table>
@@ -197,96 +460,132 @@
197
  </main>
198
 
199
  <!-- Scripts -->
 
200
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
201
- <script src="https://www.gstatic.com/charts/loader.js"></script>
202
 
203
  <script>
 
204
  const rawData = [
205
- { id: 1, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", count: 50 },
206
- { id: 2, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 3 Admission", count: 30 },
207
- { id: 3, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", count: 40 },
208
- { id: 4, name: "โรงเรียนเตรียมอุดม��ึกษา", province: "กรุงเทพมหานคร", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 2 Quota", count: 30 },
209
- { id: 5, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมโยธา", round: "รอบที่ 2 Quota", count: 60 },
210
- { id: 6, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมโยธา", round: "รอบที่ 3 Admission", count: 40 },
211
- { id: 7, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "บริหารธุรกิจ", major: "การตลาด", round: "รอบที่ 1 Portfolio", count: 20 },
212
- { id: 8, name: "โรงเรียนมหิดลวิทยานุสรณ์", province: "นครปฐม", faculty: "วิทยาศาสตร์", major: "ชีววิทยา", round: "รอบที่ 1 Portfolio", count: 55 },
213
- { id: 9, name: "โรงเรียนมหิดลวิทยานุสรณ์", province: "นครปฐม", faculty: "วิทยาศาสตร์", major: "ชีววิทยา", round: "รอบที่ 3 Admission", count: 30 },
214
- { id: 10, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "บริหารธุรกิจ", major: "การบัญชี", round: "รอบที่ 2 Quota", count: 70 },
215
- { id: 11, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "บริหารธุรกิจ", major: "การบัญชี", round: "รอบที่ 3 Admission", count: 40 },
216
- { id: 12, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "มนุษยศาสตร์", major: "ภาษาอังกฤษ", round: "รอบที่ 1 Portfolio", count: 100 },
217
- { id: 13, name: "โรงเรียนขอนแก่นวิทยายน", province: "ขอนแก่น", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 2 Quota", count: 85 },
218
- { id: 14, name: "โรงเรียนขอนแก่นวิทยายน", province: "ขอนแก่น", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 3 Admission", count: 50 },
219
- { id: 15, name: "โรงเรียนหาดใหญ่วิทยาลัย", province: "สงขลา", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", count: 65 },
220
- { id: 16, name: "โรงเรียนหาดใหญ่วิทยาลัย", province: "สงขลา", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 2 Quota", count: 40 },
221
- { id: 17, name: "โรงเรียนภูเก็ตวิทยาลัย", province: "ภูเก็ต", faculty: "บริหารธุรกิจ", major: "ธุรกิจระหว่างประเทศ", round: "รอบที่ 3 Admission", count: 60 }
222
  ];
223
 
224
- const provinceIsoMap = {
225
- "กรุงเทพมหานคร": "TH-10", "สมุทรปราการ": "TH-11", "นนทบุรี": "TH-12", "ปทุมธานี": "TH-13",
226
- "พระนครศรีอยุธยา": "TH-14", "อ่างทอง": "TH-15", "ลพบุรี": "TH-16", "สิงห์บุรี": "TH-17",
227
- "ชัยนาท": "TH-18", "สระบุรี": "TH-19", "ชลบุรี": "TH-20", "ระยอง": "TH-21",
228
- "จันทบุรี": "TH-22", "ตราด": "TH-23", "ฉะเชิงเทรา": "TH-24", "ปราจีนบุรี": "TH-25",
229
- "นครนายก": "TH-26", "สระแก้ว": "TH-27", "นครราชสีมา": "TH-30", "บุรีรัมย์": "TH-31",
230
- "สุรินทร์": "TH-32", "ศรีสะเกษ": "TH-33", "อุบลราชธานี": "TH-34", "ยโสธร": "TH-35",
231
- "ชัยภูมิ": "TH-36", "อำนาจเจริญ": "TH-37", "บึงกาฬ": "TH-38", "หนองบัวลำภู": "TH-39",
232
- "ขอนแก่น": "TH-40", "อุดรธานี": "TH-41", "เลย": "TH-42", "หนองคาย": "TH-43",
233
- "มหาสารคาม": "TH-44", "ร้อยเอ็ด": "TH-45", "กาฬสินธุ์": "TH-46", "สกลนคร": "TH-47",
234
- "นครพนม": "TH-48", "มุกดาหาร": "TH-49", "เชียงใหม่": "TH-50", "ลำพูน": "TH-51",
235
- "ลำปาง": "TH-52", "อุตรดิตถ์": "TH-53", "แพร่": "TH-54", "น่าน": "TH-55",
236
- "พะเยา": "TH-56", "เชียงราย": "TH-57", "แม่ฮ่องสอน": "TH-58", "นครสวรรค์": "TH-60",
237
- "อุทัยธานี": "TH-61", "กำแพงเพชร": "TH-62", "ตาก": "TH-63", "สุโขทัย": "TH-64",
238
- "พิษณุโลก": "TH-65", "พิจิตร": "TH-66", "เพชรบูรณ์": "TH-67", "ราชบุรี": "TH-70",
239
- "กาญจนบุรี": "TH-71", "สุพรรณบุรี": "TH-72", "นครปฐม": "TH-73", "สมุท���สาคร": "TH-74",
240
- "สมุทรสงคราม": "TH-75", "เพชรบุรี": "TH-76", "ประจวบคีรีขันธ์": "TH-77", "นครศรีธรรมราช": "TH-80",
241
- "กระบี่": "TH-81", "พังงา": "TH-82", "ภูเก็ต": "TH-83", "สุราษฎร์ธานี": "TH-84",
242
- "ระนอง": "TH-85", "ชุมพร": "TH-86", "สงขลา": "TH-90", "สตูล": "TH-91",
243
- "ตรัง": "TH-92", "พัทลุง": "TH-93", "ปัตตานี": "TH-94", "ยะลา": "TH-95", "นราธิวาส": "TH-96"
244
  };
245
 
246
- const isoToProvinceName = Object.fromEntries(
247
- Object.entries(provinceIsoMap).map(([province, iso]) => [iso, province])
248
- );
249
-
250
  let activeFilteredData = [];
251
- let geoChart = null;
252
- let isGoogleChartsLoaded = false;
 
 
 
253
 
254
- google.charts.load('current', { 'packages': ['geochart'] });
255
- google.charts.setOnLoadCallback(function() {
256
- isGoogleChartsLoaded = true;
257
- document.getElementById('map-loading').style.display = 'none';
258
- applyFilters();
259
- });
 
260
 
261
- document.addEventListener('DOMContentLoaded', function() {
 
262
  initDropdowns();
263
- });
 
 
 
 
 
 
 
 
264
 
265
  function initDropdowns() {
266
  const facultyFilter = document.getElementById('facultyFilter');
267
- const faculties = [...new Set(rawData.map(item => item.faculty))].sort();
268
-
269
- faculties.forEach(fac => {
270
- const option = document.createElement('option');
271
- option.value = fac;
272
- option.textContent = fac;
273
- facultyFilter.appendChild(option);
274
- });
 
275
 
276
  const roundFilter = document.getElementById('roundFilter');
277
- const rounds = [...new Set(rawData.map(item => item.round))].sort();
278
-
279
- rounds.forEach(rnd => {
280
- const option = document.createElement('option');
281
- option.value = rnd;
282
- option.textContent = rnd;
283
- roundFilter.appendChild(option);
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
 
287
  function onFacultyChange() {
288
- const faculty = document.getElementById('facultyFilter').value;
289
  const majorFilter = document.getElementById('majorFilter');
 
 
 
290
  majorFilter.innerHTML = '<option value="">-- สาขาวิชาทั้งหมด --</option>';
291
 
292
  if (faculty) {
@@ -302,158 +601,250 @@
302
  }
303
 
304
  function applyFilters() {
305
- const faculty = document.getElementById('facultyFilter').value;
306
- const major = document.getElementById('majorFilter').value;
307
- const round = document.getElementById('roundFilter').value;
 
 
 
 
 
 
308
 
309
  activeFilteredData = rawData.filter(item => {
310
  return (faculty === "" || item.faculty === faculty) &&
311
  (major === "" || item.major === major) &&
312
- (round === "" || item.round === round);
 
313
  });
314
 
315
- updateStats(activeFilteredData);
316
  renderTable(activeFilteredData);
317
-
318
- if (isGoogleChartsLoaded) {
319
- drawGeoChart(activeFilteredData);
320
- }
321
  }
322
 
323
- function updateStats(data) {
324
- let total = 0, provinceCounts = {}, schoolCounts = {};
 
 
 
325
 
326
- data.forEach(item => {
327
- total += item.count;
328
- provinceCounts[item.province] = (provinceCounts[item.province] || 0) + item.count;
329
- schoolCounts[item.name] = (schoolCounts[item.name] || 0) + item.count;
330
- });
331
 
332
- let topProv = '-', maxProv = 0, topSch = '-', maxSch = 0;
333
- for (let p in provinceCounts) { if(provinceCounts[p] > maxProv) { maxProv = provinceCounts[p]; topProv = p; } }
334
- for (let s in schoolCounts) { if(schoolCounts[s] > maxSch) { maxSch = schoolCounts[s]; topSch = s; } }
 
 
335
 
336
- document.getElementById('totalStudents').innerText = total.toLocaleString();
337
- document.getElementById('topProvince').innerText = topProv === '-' ? '-' : `${topProv} (${maxProv} คน)`;
338
- document.getElementById('topSchool').innerText = topSch === '-' ? '-' : `${topSch} (${maxSch} คน)`;
339
- }
 
340
 
341
- function drawGeoChart(data) {
342
- const provinceSummary = {};
343
- data.forEach(item => {
344
- provinceSummary[item.province] = (provinceSummary[item.province] || 0) + item.count;
 
345
  });
346
 
347
- const chartData = new google.visualization.DataTable();
348
- chartData.addColumn('string', 'Province');
349
- chartData.addColumn('number', 'จำนวนนักศึกษา (คน)');
 
 
350
 
351
- for (const [province, count] of Object.entries(provinceSummary)) {
352
- const isoCode = provinceIsoMap[province];
353
- if (isoCode) {
354
- chartData.addRow([{ v: isoCode, f: `จังหวัด${province}` }, count]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  }
356
  }
357
 
358
- // ใช้ชุดคู่สีพาสเทล Teal ที่ดูพรีเมียมและสวยงาม
359
- const options = {
360
- region: 'TH',
361
- resolution: 'provinces',
362
- colorAxis: { colors: ['#f0fdfa', '#0d9488'] }, /* Teal 50 to Teal 600 */
363
- backgroundColor: '#ffffff',
364
- datalessRegionColor: '#f8fafc',
365
- defaultColor: '#f8fafc',
366
- legend: { textStyle: { color: '#64748b', fontName: 'Sarabun', fontSize: 12 } },
367
- tooltip: { textStyle: { fontName: 'Sarabun', fontSize: 13 }, trigger: 'focus' }
368
- };
369
-
370
- const container = document.getElementById('regions_div');
371
- geoChart = new google.visualization.GeoChart(container);
372
-
373
- google.visualization.events.addListener(geoChart, 'select', function() {
374
- const selection = geoChart.getSelection();
375
- if (selection.length > 0) {
376
- const row = selection[0].row;
377
- const isoCode = chartData.getValue(row, 0);
378
- const clickedProvince = isoToProvinceName[isoCode];
379
-
380
- if (clickedProvince) {
381
- focusOnProvinceInTable(clickedProvince);
382
- }
383
- }
384
- });
385
 
386
- geoChart.draw(chartData, options);
387
- }
 
 
388
 
389
- window.addEventListener('resize', function() {
390
- if (isGoogleChartsLoaded && activeFilteredData.length > 0) {
391
- drawGeoChart(activeFilteredData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  }
393
- });
394
-
395
- function focusOnProvinceInTable(provinceName) {
396
- const rows = document.querySelectorAll('#dataTableBody tr');
397
- let targetRow = null;
398
- let targetId = null;
399
-
400
- rows.forEach(row => {
401
- if (row.textContent.includes(`จังหวัด${provinceName}`)) {
402
- targetRow = row;
403
- const iconEl = row.querySelector('.toggle-icon');
404
- if (iconEl) {
405
- targetId = iconEl.id.replace('icon-', '');
406
- }
407
  }
 
 
 
 
 
408
  });
409
 
410
- if (targetRow && targetId) {
411
- const icon = document.getElementById(`icon-${targetId}`);
412
- if (icon && icon.classList.contains('fa-chevron-right')) {
413
- toggleRow(targetId);
414
- }
 
 
 
 
 
 
 
415
 
416
- targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
 
 
 
417
 
418
- targetRow.classList.add('highlight-row');
419
- setTimeout(() => {
420
- targetRow.classList.remove('highlight-row');
421
- }, 2000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  }
423
  }
424
 
425
  function renderTable(data) {
426
  const tbody = document.getElementById('dataTableBody');
 
427
  tbody.innerHTML = '';
428
 
429
  if(data.length === 0) {
430
- tbody.innerHTML = '<tr><td colspan="2" class="text-center text-rose-500 py-8 font-medium">ไม่พบข้อมูลที่ตรงกับเงื่อนไขตัวกรองในขณะนี้</td></tr>';
431
  return;
432
  }
433
 
434
  const grouped = {};
435
  data.forEach(item => {
436
- if (!grouped[item.province]) grouped[item.province] = { count: 0, schools: {} };
437
- grouped[item.province].count += item.count;
 
 
 
438
 
439
  if (!grouped[item.province].schools[item.name]) {
440
- grouped[item.province].schools[item.name] = { count: 0, faculties: {} };
441
  }
442
- grouped[item.province].schools[item.name].count += item.count;
 
 
 
443
 
444
  if (!grouped[item.province].schools[item.name].faculties[item.faculty]) {
445
- grouped[item.province].schools[item.name].faculties[item.faculty] = { count: 0, majors: {} };
446
  }
447
- grouped[item.province].schools[item.name].faculties[item.faculty].count += item.count;
 
 
 
448
 
449
- // รวมยอดตามสาขาวิชา (ไม่แยกตามรอบการสมัครในตาราง)
450
  if (!grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major]) {
451
- grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major] = { count: 0 };
452
  }
453
- grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].count += item.count;
 
 
 
454
  });
455
 
456
- const sortedProvinces = Object.keys(grouped).sort((a, b) => grouped[b].count - grouped[a].count);
457
 
458
  let provIndex = 1;
459
  sortedProvinces.forEach(prov => {
@@ -462,25 +853,37 @@
462
 
463
  // --- แถวระดับที่ 1: จังหวัด ---
464
  const trProv = document.createElement('tr');
465
- trProv.className = 'clickable-row bg-slate-50 hover:bg-slate-100/70 border-b border-slate-100';
466
- trProv.onclick = () => toggleRow(provId);
 
 
 
467
  trProv.innerHTML = `
468
- <td class="px-6 py-3.5 text-left font-bold text-slate-800 flex items-center">
469
- <i class="fa-solid fa-chevron-right text-slate-400 toggle-icon mr-2" id="icon-${provId}"></i>
470
- <span class="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-brand-50 text-brand-600 mr-2.5">
471
- <i class="fa-solid fa-map-location-dot text-xs"></i>
472
- </span>
473
- <span>จังหวัด${prov}</span>
 
 
474
  </td>
475
- <td class="px-6 py-3.5 text-center">
476
- <span class="inline-flex items-center rounded-full bg-brand-50 px-3 py-1 text-sm font-extrabold text-brand-800 ring-1 ring-inset ring-brand-600/20">
477
- ${provData.count.toLocaleString()}
478
- </span>
 
 
 
 
 
 
 
479
  </td>
480
  `;
481
  tbody.appendChild(trProv);
482
 
483
- const sortedSchools = Object.keys(provData.schools).sort((a, b) => provData.schools[b].count - provData.schools[a].count);
484
 
485
  let schIndex = 1;
486
  sortedSchools.forEach(sch => {
@@ -489,26 +892,35 @@
489
 
490
  // --- แถวระดับที่ 2: สถานศึกษา ---
491
  const trSch = document.createElement('tr');
492
- trSch.className = `child-${provId} clickable-row bg-white hover:bg-slate-50/50 border-b border-slate-100`;
493
  trSch.style.display = 'none';
494
  trSch.onclick = () => toggleRow(schId);
495
  trSch.innerHTML = `
496
- <td class="px-6 py-3.5 text-left font-semibold text-slate-700 flex items-center" style="padding-left: 2.75rem;">
497
- <i class="fa-solid fa-chevron-right text-slate-400 toggle-icon mr-2" id="icon-${schId}"></i>
498
- <span class="inline-flex h-6 w-6 items-center justify-center rounded-md bg-amber-50 text-amber-600 mr-2">
499
- <i class="fa-solid fa-school text-xs"></i>
500
- </span>
501
- <span>${sch}</span>
 
 
502
  </td>
503
- <td class="px-6 py-3.5 text-center">
504
- <span class="inline-flex items-center rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-bold text-amber-700 ring-1 ring-inset ring-amber-600/20">
505
- ${schData.count.toLocaleString()}
506
- </span>
 
 
 
 
 
 
 
507
  </td>
508
  `;
509
  tbody.appendChild(trSch);
510
 
511
- const sortedFaculties = Object.keys(schData.faculties).sort((a, b) => schData.faculties[b].count - schData.faculties[a].count);
512
 
513
  let facIndex = 1;
514
  sortedFaculties.forEach(fac => {
@@ -517,41 +929,61 @@
517
 
518
  // --- แถวระดับที่ 3: คณะ ---
519
  const trFac = document.createElement('tr');
520
- trFac.className = `child-${schId} parent-${provId} clickable-row bg-white hover:bg-slate-50/30 border-b border-slate-100`;
521
  trFac.style.display = 'none';
522
  trFac.onclick = () => toggleRow(facId);
523
  trFac.innerHTML = `
524
- <td class="px-6 py-3 text-left font-medium text-slate-600 flex items-center" style="padding-left: 4.75rem;">
525
- <i class="fa-solid fa-chevron-right text-slate-400 toggle-icon mr-2" id="icon-${facId}"></i>
526
- <span class="inline-flex h-5 w-5 items-center justify-center rounded-md bg-brand-50 text-brand-600 mr-2">
527
- <i class="fa-solid fa-building-columns text-[10px]"></i>
528
- </span>
529
- <span>คณะ${fac}</span>
 
 
530
  </td>
531
- <td class="px-6 py-3 text-center">
532
- <span class="inline-flex items-center rounded-full bg-sky-50 px-2.5 py-0.5 text-xs font-semibold text-sky-700 ring-1 ring-inset ring-sky-600/20">
533
- ${facData.count.toLocaleString()}
534
- </span>
 
 
 
 
 
 
 
535
  </td>
536
  `;
537
  tbody.appendChild(trFac);
538
 
539
- const sortedMajors = Object.keys(facData.majors).sort((a, b) => facData.majors[b].count - facData.majors[a].count);
540
 
541
  // --- แถวระดับที่ 4: สาขาวิชา ---
542
  sortedMajors.forEach(majorName => {
543
- const majorCount = facData.majors[majorName].count;
544
  const trDet = document.createElement('tr');
545
- trDet.className = `child-${facId} parent-${schId} parent-${provId} bg-slate-50/30`;
546
  trDet.style.display = 'none';
547
  trDet.innerHTML = `
548
- <td class="px-6 py-2.5 text-left text-slate-500 flex items-center" style="padding-left: 6.75rem;">
549
- <i class="fa-solid fa-turn-up fa-rotate-90 text-slate-300 mr-3"></i>
550
- <span class="mr-2"><i class="fa-solid fa-book-open text-slate-400 text-xs mr-1.5"></i>${majorName}</span>
 
 
 
 
 
 
 
 
 
 
 
551
  </td>
552
- <td class="px-6 py-2.5 text-center">
553
- <span class="inline-flex items-center rounded-full bg-emerald-50 px-2.5 py-0.5 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20">
554
- ${majorCount.toLocaleString()}
555
  </span>
556
  </td>
557
  `;
@@ -567,9 +999,9 @@
567
  });
568
  }
569
 
570
- // ฟังก์ชันเปิด-ปิดสลับไอคอนแบบ Drill-down
571
  function toggleRow(targetId) {
572
  const icon = document.getElementById(`icon-${targetId}`);
 
573
  const isExpanded = icon.classList.contains('fa-chevron-down');
574
 
575
  if (isExpanded) {
@@ -581,7 +1013,6 @@
581
  const descendants = document.querySelectorAll(`.parent-${targetId}`);
582
  descendants.forEach(el => el.style.display = 'none');
583
 
584
- // พับไอคอนของลูกหลานทั้งหมดให้ถูกต้อง
585
  children.forEach(row => {
586
  const subIcon = row.querySelector('.toggle-icon');
587
  if (subIcon && subIcon.classList.contains('fa-chevron-down')) {
@@ -604,20 +1035,20 @@
604
 
605
  function showInfo() {
606
  Swal.fire({
607
- title: 'ข้อมูลระบบรายงานสถิติ',
608
  html: `
609
- <div class="text-left text-sm text-slate-600 space-y-3.5 mt-2">
610
- <p class="font-semibold text-brand-700">การนำเสนอรายงานสถิตินักศึกษาใหม่ระดับพรีเมียม</p>
611
- <ul class="list-disc pl-5 space-y-2">
612
- <li><b>วิเคราะห์ข้อมูลจำแนก 4 ระดับ:</b> แตกโครงสร้างอย่างโปร่งใสโดยคลิกที่ จังหวัด ➔ โรงเรียนเดิม ➔ คณะ ➔ สาขาวิชา</li>
613
- <li><b>แผนภูมิแผนที่ประเทศไทย (GeoChart):</b> แสดงความหนาแน่นของผู้เรียนแบบไล่ระดับความเข้มของสี Teal</li>
614
- <li><b>การแยกแยะสีของตัวเลขสถิติอย่างคมชัด:</b> ปรับตัวเลขยอดรวมตามระดับชั้นข้อมูลเป็นป้ายสีเฉพาะทางเพื่อให้มองเห็นความแตกต่างอย่างชัดเจน</li>
615
  </ul>
616
  </div>
617
  `,
618
  icon: 'info',
619
  confirmButtonText: 'ตกลง',
620
- confirmButtonColor: '#0d9488', // Premium Teal Buttons
621
  });
622
  }
623
  </script>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dashboard สถิติจำนวนรับเข้านักศึกษาใหม่ (Teal Premium & Admission Funnel)</title>
7
 
8
+ <!-- Bootstrap 5 CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
  <!-- Font Awesome -->
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
12
  <!-- SweetAlert2 CSS -->
13
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
14
+ <!-- Leaflet CSS -->
15
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
16
  <!-- Google Fonts: Sarabun -->
17
  <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
18
 
19
+ <style>
20
+ /* กำหนดสีหลัก Teal พรีเมียม */
21
+ :root {
22
+ --brand-50: #f0fdfa;
23
+ --brand-100: #ccfbf1;
24
+ --brand-200: #99f6e4;
25
+ --brand-500: #14b8a6;
26
+ --brand-600: #0d9488; /* สี Teal หลัก */
27
+ --brand-700: #0f766e;
28
+ --brand-800: #115e59;
29
+ --brand-900: #134e4a;
 
 
 
 
 
 
 
 
 
 
 
30
  }
 
31
 
32
+ body {
33
+ background-color: #f8fafc;
34
+ font-family: 'Sarabun', sans-serif;
35
+ color: #334155;
36
+ }
37
+
38
+ /* ดีไซน์ Sticky Header Navbar */
39
+ .header-navbar {
40
+ background-color: rgba(255, 255, 255, 0.95);
41
+ backdrop-filter: blur(8px);
42
+ -webkit-backdrop-filter: blur(8px);
43
+ border-bottom: 1px solid #f1f5f9;
44
+ }
45
+
46
+ /* ดีไซน์แผ่นการ์ดโมเดิร์น */
47
+ .custom-card {
48
+ background: #ffffff;
49
+ border: 1px solid #f1f5f9;
50
+ border-radius: 1rem !important; /* rounded-2xl */
51
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
52
+ position: relative;
53
+ }
54
+
55
+ /* ฟอร์มตัวเลือกดรอปดาวน์ */
56
+ .custom-select {
57
+ border-radius: 0.75rem !important; /* rounded-xl */
58
+ border: 1px solid #e2e8f0;
59
+ padding: 0.5rem 1rem;
60
+ font-size: 0.875rem;
61
+ color: #475569;
62
+ outline: none;
63
+ transition: all 0.2s ease;
64
+ }
65
+ .custom-select:focus {
66
+ border-color: var(--brand-500) !important;
67
+ box-shadow: 0 0 0 4px var(--brand-100) !important;
68
+ }
69
+
70
+ /* ตกแต่งส่วนเน้นการ์ดสรุป */
71
+ .stat-card-accent {
72
+ position: absolute;
73
+ top: 0;
74
+ left: 0;
75
+ height: 4px;
76
+ width: 100%;
77
+ border-radius: 1rem 1rem 0 0;
78
+ }
79
+ .stat-applied { background-color: #3b82f6; } /* Blue */
80
+ .stat-paid { background-color: #6366f1; } /* Indigo */
81
+ .stat-eligible { background-color: #06b6d4; } /* Cyan */
82
+ .stat-registered { background-color: #0d9488; }/* Teal (Brand Primary) */
83
+
84
+ .icon-wrapper {
85
+ width: 3rem;
86
+ height: 3rem;
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ border-radius: 0.75rem;
91
+ }
92
+
93
+ /* ปรับแต่งไอคอนสรุปสถิติ 4 ขั้นตอนเพื่อความสวยงามพรีเมียม */
94
+ .icon-applied { background-color: #eff6ff; color: #3b82f6; }
95
+ .icon-paid { background-color: #f5f3ff; color: #6366f1; }
96
+ .icon-eligible { background-color: #ecfeff; color: #06b6d4; }
97
+ .icon-registered { background-color: var(--brand-50); color: var(--brand-600); }
98
+
99
+ /* แผนที่แบบ Interactive ด้วย Leaflet */
100
+ .map-wrapper {
101
+ height: 480px;
102
+ width: 100%;
103
+ position: relative;
104
+ background-color: #ffffff;
105
+ border-radius: 0 0 1rem 1rem;
106
+ overflow: hidden;
107
+ border-top: 1px solid #f1f5f9;
108
+ }
109
+ #map {
110
+ width: 100%;
111
+ height: 100%;
112
+ z-index: 1;
113
+ }
114
+
115
+ /* ตกแต่งหน้าต่าง Popup ของแผนที่ */
116
+ .leaflet-popup-content-wrapper {
117
+ border-radius: 0.75rem;
118
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
119
+ border: 1px solid #f1f5f9;
120
+ }
121
+ .leaflet-popup-content {
122
+ font-family: 'Sarabun', sans-serif;
123
+ color: #475569;
124
+ margin: 12px 15px;
125
+ line-height: 1.5;
126
+ }
127
+
128
+ /* ตกแต่งตารางสไตล์ Drill-down หลายคอลัมน์ (แก้ปัญหาเส้นตารางเบี้ยว) */
129
+ .table-custom {
130
+ border-collapse: collapse;
131
+ font-size: 0.875rem;
132
+ }
133
+ .table-custom thead th {
134
+ background-color: #f8fafc;
135
+ color: #64748b;
136
+ font-weight: 700;
137
+ text-transform: uppercase;
138
+ font-size: 0.75rem;
139
+ letter-spacing: 0.05em;
140
+ padding: 1rem 1.25rem;
141
+ border-bottom: 1px solid #e2e8f0;
142
+ }
143
+ .clickable-row {
144
+ cursor: pointer;
145
+ user-select: none;
146
+ transition: background-color 0.15s ease;
147
+ }
148
+ .clickable-row:hover {
149
+ background-color: #f8fafc !important;
150
+ }
151
+ .toggle-icon {
152
+ width: 20px;
153
+ text-align: center;
154
+ transition: transform 0.2s;
155
+ }
156
+
157
+ /* สไตล์ไอคอนแถวในตารางย่อย */
158
+ .nested-icon-prov {
159
+ width: 1.75rem;
160
+ height: 1.75rem;
161
+ display: inline-flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ border-radius: 0.5rem;
165
+ background-color: var(--brand-50);
166
+ color: var(--brand-600);
167
+ }
168
+ .nested-icon-school {
169
+ width: 1.5rem;
170
+ height: 1.5rem;
171
+ display: inline-flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ border-radius: 0.375rem;
175
+ background-color: #fffbeb;
176
+ color: #d97706;
177
+ }
178
+ .nested-icon-faculty {
179
+ width: 1.25rem;
180
+ height: 1.25rem;
181
+ display: inline-flex;
182
+ align-items: center;
183
+ justify-content: center;
184
+ border-radius: 0.375rem;
185
+ background-color: var(--brand-50);
186
+ color: var(--brand-600);
187
+ }
188
+
189
+ /* คอนฟิกสีของ Pill Badges ให้คงที่เสถียรและคมชัด */
190
+ .badge-applied {
191
+ background-color: #eff6ff !important; /* Soft Blue */
192
+ color: #1e40af !important; /* Dark Blue Text */
193
+ border: 1px solid #bfdbfe !important;
194
+ font-weight: 700 !important;
195
+ }
196
+ .badge-paid {
197
+ background-color: #e0e7ff !important; /* Soft Indigo */
198
+ color: #3730a3 !important; /* Dark Indigo Text */
199
+ border: 1px solid #c7d2fe !important;
200
+ font-weight: 700 !important;
201
+ }
202
+ .badge-eligible {
203
+ background-color: #ecfeff !important; /* Soft Cyan */
204
+ color: #0e7490 !important; /* Dark Cyan Text */
205
+ border: 1px solid #a5f3fc !important;
206
+ font-weight: 700 !important;
207
+ }
208
+ .badge-registered {
209
+ background-color: var(--brand-50) !important; /* Soft Brand Teal */
210
+ color: var(--brand-800) !important; /* Dark Brand Teal Text */
211
+ border: 1px solid var(--brand-200) !important;
212
+ font-weight: 700 !important;
213
+ }
214
+
215
+ /* สไตล์ Progress Bar ในช่องจัดอันดับสถิติสูง */
216
+ .custom-progress {
217
+ height: 6px;
218
+ border-radius: 999px;
219
+ background-color: #f1f5f9;
220
+ overflow: hidden;
221
+ }
222
+ .custom-progress-bar {
223
+ height: 100%;
224
+ background-color: var(--brand-500);
225
+ border-radius: 999px;
226
+ transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
227
+ }
228
+
229
+ /* เอฟเฟคกะพริบไฮไลท์แถว */
230
  .highlight-row {
231
  animation: highlight-effect 2s cubic-bezier(0.4, 0, 0.2, 1);
232
  }
233
  @keyframes highlight-effect {
234
+ 0% { background-color: rgba(20, 184, 166, 0.25) !important; }
235
  100% { background-color: transparent; }
236
  }
 
 
 
 
237
  </style>
238
  </head>
239
+ <body class="antialiased">
240
+
241
+ <!-- Premium Header Navbar -->
242
+ <header class="header-navbar sticky-top shadow-sm">
243
+ <div class="container-xxl py-3 px-4 d-flex align-items-center justify-content-between">
244
+ <div class="d-flex align-items-center">
245
+ <div class="icon-wrapper bg-brand-50 text-brand-600 me-3">
246
+ <i class="fa-solid fa-graduation-cap fs-4"></i>
247
  </div>
248
  <div>
249
+ <span class="text-uppercase text-secondary tracking-wider font-semibold" style="font-size: 0.65rem; letter-spacing: 0.05em;">ระบบวิเคราะห์ขั้นตอนสารสนเทศ</span>
250
+ <h1 class="h6 mb-0 fw-bold text-dark">ระบบวิเคราะห์ข้อมูลสถิติผู้สมัครและสัดส่วนรับเข้าศึกษา</h1>
251
  </div>
252
  </div>
253
+ <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">
254
  <i class="fa-solid fa-circle-info text-brand-600"></i>
255
+ <span class="small">รายละเอียดระบบ</span>
256
  </button>
257
  </div>
258
  </header>
259
 
260
+ <main class="container-xxl px-4 py-4" style="max-width: 80rem;">
261
 
262
  <!-- Filter Card -->
263
+ <section class="custom-card p-4 mb-4">
264
+ <div class="row align-items-center g-3">
265
+ <div class="col-xl-2 col-md-12">
266
+ <div class="d-flex align-items-center gap-2">
267
+ <span class="d-inline-flex align-items-center justify-content-center" style="width: 2rem; height: 2rem; background-color: #f1f5f9; border-radius: 0.5rem;">
268
+ <i class="fa-solid fa-filter text-secondary"></i>
269
+ </span>
270
+ <h2 class="h6 mb-0 fw-bold text-secondary">ตัวกรองระบบวิเคราะห์:</h2>
271
+ </div>
272
  </div>
273
+ <div class="col-xl-10 col-md-12">
274
+ <div class="row g-3">
275
+ <div class="col-md-3">
276
+ <!-- แผนก/ระดับการศึกษา (ค่าเริ่มต้น: ภาคปกติ) -->
277
+ <select id="programFilter" onchange="applyFilters()" class="form-select custom-select w-full">
278
+ <option value="">-- แผนก/ระดับการศึกษาทั้งหมด --</option>
279
+ <option value="ภาคปกติ" selected>ภาคปกติ</option>
280
+ <option value="ภาคพิเศษ">ภาคพิเศษ</option>
281
+ <option value="ระดับบัณฑิตศึกษา">ระดับบัณฑิตศึกษา</option>
282
+ </select>
283
+ </div>
284
+ <div class="col-md-3">
285
+ <!-- รอบการรับสมัคร -->
286
+ <select id="roundFilter" onchange="applyFilters()" class="form-select custom-select w-full">
287
+ <option value="">-- รอบการสมัครทั้งหมด --</option>
288
+ </select>
289
+ </div>
290
+ <div class="col-md-3">
291
+ <!-- คณะ -->
292
+ <select id="facultyFilter" onchange="onFacultyChange()" class="form-select custom-select w-full">
293
+ <option value="">-- คณะทั้งหมด --</option>
294
+ </select>
295
+ </div>
296
+ <div class="col-md-3">
297
+ <!-- สาขาวิชา -->
298
+ <select id="majorFilter" onchange="applyFilters()" class="form-select custom-select w-full">
299
+ <option value="">-- สาขาวิชาทั้งหมด --</option>
300
+ </select>
301
+ </div>
302
+ </div>
303
  </div>
304
  </div>
305
  </section>
306
 
307
+ <!-- Summary Cards Funnel Grid -->
308
+ <section class="row g-4 mb-4">
309
+ <!-- ขั้นที่ 1: ผู้สมัครสะสม -->
310
+ <div class="col-lg-3 col-sm-6">
311
+ <div class="custom-card p-4 h-100 shadow-sm">
312
+ <div class="stat-card-accent stat-applied"></div>
313
+ <div class="d-flex align-items-center justify-content-between">
314
+ <div>
315
+ <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ผู้สมัครสะสม</p>
316
+ <h3 id="statApplied" class="h3 fw-extrabold text-dark mb-0">0</h3>
317
+ </div>
318
+ <div class="icon-wrapper icon-applied">
319
+ <i class="fa-solid fa-file-signature fs-5"></i>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ <!-- ขั้นที่ 2: ชำระค่าสมัครแล้ว -->
326
+ <div class="col-lg-3 col-sm-6">
327
+ <div class="custom-card p-4 h-100 shadow-sm">
328
+ <div class="stat-card-accent stat-paid"></div>
329
+ <div class="d-flex align-items-center justify-content-between">
330
+ <div>
331
+ <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ชำระค่าสมัครแล้ว</p>
332
+ <h3 id="statPaid" class="h3 fw-extrabold text-dark mb-0">0</h3>
333
+ </div>
334
+ <div class="icon-wrapper icon-paid">
335
+ <i class="fa-solid fa-receipt fs-5"></i>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <!-- ขั้นที่ 3: ผู้มีสิทธิ์เข้าศึกษา -->
342
+ <div class="col-lg-3 col-sm-6">
343
+ <div class="custom-card p-4 h-100 shadow-sm">
344
+ <div class="stat-card-accent stat-eligible"></div>
345
+ <div class="d-flex align-items-center justify-content-between">
346
+ <div>
347
+ <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ผู้มีสิทธิ์เข้าศึกษา</p>
348
+ <h3 id="statEligible" class="h3 fw-extrabold text-dark mb-0">0</h3>
349
+ </div>
350
+ <div class="icon-wrapper icon-eligible">
351
+ <i class="fa-solid fa-user-check fs-5"></i>
352
+ </div>
353
  </div>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- ขั้นที่ 4: ผู้ชำระเงินรายงานตัว -->
358
+ <div class="col-lg-3 col-sm-6">
359
+ <div class="custom-card p-4 h-100 shadow-sm">
360
+ <div class="stat-card-accent stat-registered"></div>
361
+ <div class="d-flex align-items-center justify-content-between">
362
+ <div>
363
+ <p class="text-uppercase text-secondary tracking-wider fw-semibold mb-1" style="font-size: 0.65rem; letter-spacing: 0.05em;">ผู้ชำระเงินรายงานตัว</p>
364
+ <h3 id="statRegistered" class="h3 fw-extrabold text-dark mb-0">0</h3>
365
+ </div>
366
+ <div class="icon-wrapper icon-registered">
367
+ <i class="fa-solid fa-id-card fs-5"></i>
368
+ </div>
369
  </div>
370
  </div>
371
  </div>
372
+ </section>
373
 
374
+ <!-- Top 5 Analysis Panel -->
375
+ <section class="row g-4 mb-4">
376
+ <!-- Top 5 Provinces -->
377
+ <div class="col-lg-6">
378
+ <div class="custom-card p-4 shadow-sm h-100">
379
+ <div class="d-flex align-items-center justify-content-between mb-4 border-bottom pb-2">
380
+ <div class="d-flex align-items-center gap-2">
381
+ <span class="text-brand-600"><i class="fa-solid fa-trophy text-brand-600"></i></span>
382
+ <h4 class="h6 mb-0 fw-bold text-dark">Top 5 จังหวัดรับเข้าสูงที่สุด</h4>
383
+ </div>
384
+ <span class="badge bg-slate-100 text-slate-500 font-medium small" style="font-size: 0.7rem; padding: 0.25rem 0.5rem;">นับตามผู้ชำระเงินรายงานตัว</span>
385
  </div>
386
+ <div id="topProvincesList" class="d-flex flex-column gap-3">
387
+ <!-- รายการจัดอันดับจังหวัดจะถูกแสดงด้วย JavaScript -->
388
  </div>
389
  </div>
390
  </div>
391
 
392
+ <!-- Top 5 Feeder Schools -->
393
+ <div class="col-lg-6">
394
+ <div class="custom-card p-4 shadow-sm h-100">
395
+ <div class="d-flex align-items-center justify-content-between mb-4 border-bottom pb-2">
396
+ <div class="d-flex align-items-center gap-2">
397
+ <span class="text-warning"><i class="fa-solid fa-school-flag text-warning"></i></span>
398
+ <h4 class="h6 mb-0 fw-bold text-dark">Top 5 สถานศึกษาป้อนผู้เรียนสูงที่สุด</h4>
399
+ </div>
400
+ <span class="badge bg-slate-100 text-slate-500 font-medium small" style="font-size: 0.7rem; padding: 0.25rem 0.5rem;">นับตามผู้ชำระเงินรายงานตัว</span>
401
  </div>
402
+ <div id="topSchoolsList" class="d-flex flex-column gap-3">
403
+ <!-- รายการจัดอันดับโรงเรียนจะถูกแสดงด้วย JavaScript -->
404
  </div>
405
  </div>
406
  </div>
407
  </section>
408
 
409
  <!-- Map Module -->
410
+ <section class="custom-card overflow-hidden mb-4 shadow-sm">
411
+ <div class="d-flex align-items-center justify-content-between bg-light bg-opacity-50 px-4 py-3 border-bottom">
412
+ <div class="d-flex align-items-center gap-2">
413
  <span class="text-brand-600"><i class="fa-solid fa-map"></i></span>
414
+ <h3 class="h6 mb-0 fw-bold text-dark">แผนที่วิเคราะห์การกระจายตัวผู้สมัคร (คลิกวงกลมสีเพื่อจำแนกสถิติ Funnel รายจังหวัด)</h3>
415
+ </div>
416
+ <div class="d-flex align-items-center gap-2">
417
+ <span class="d-none d-sm-inline-flex align-items-center text-secondary me-2" style="font-size: 0.75rem;">
418
+ <i class="fa-solid fa-circle-nodes me-1 text-brand-600"></i> ซูมเข้า-ออกและขยายแผนที่ได้อย่างอิสระ
419
+ </span>
420
+ <button class="btn btn-xs btn-outline-secondary px-2.5 py-1 text-xs rounded-2" onclick="resetMapView()">
421
+ <i class="fa-solid fa-compress me-1"></i>คืนค่ามุมมอง
422
+ </button>
423
  </div>
 
 
 
424
  </div>
425
+ <div class="map-wrapper">
426
+ <!-- หน้าจอโหลด -->
427
+ <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;">
428
+ <div class="spinner-border text-teal mb-2" role="status" style="color: var(--brand-600);"></div>
429
+ <span class="text-secondary small font-medium">กำลังจัดทำแผนที่สารสนเทศ...</span>
430
  </div>
431
+ <div id="map"></div>
432
  </div>
433
  </section>
434
 
435
  <!-- Data Table Container -->
436
+ <section class="custom-card p-4 shadow-sm">
437
+ <div class="d-flex align-items-center gap-2 mb-3">
438
  <span class="text-brand-600"><i class="fa-solid fa-table-list"></i></span>
439
+ <h3 class="h6 mb-0 fw-bold text-dark">สถิติเจาะลึกแบบลำดับขั้น (จังหวัด &gt; สถานศึกษาเดิม &gt; คณะ &gt; สาขาวิชา)</h3>
440
  </div>
441
 
442
+ <div class="table-responsive rounded-3 border border-light">
443
+ <table class="table table-custom table-hover align-middle mb-0">
444
+ <thead>
445
+ <tr class="text-center">
446
+ <th scope="col" class="text-start" style="width: 40%">โครงสร้างหน่วยและสายการรับสมัคร</th>
447
+ <th scope="col" style="width: 15%">ผู้สมัคร (คน)</th>
448
+ <th scope="col" style="width: 15%">ชำระค่าสมัคร (คน)</th>
449
+ <th scope="col" style="width: 15%">ผู้มีสิทธิ์ (คน)</th>
450
+ <th scope="col" style="width: 15%">รายงานตัว (คน)</th>
451
  </tr>
452
  </thead>
453
+ <tbody id="dataTableBody" class="border-top-0">
454
  <!-- Filled by JavaScript -->
455
  </tbody>
456
  </table>
 
460
  </main>
461
 
462
  <!-- Scripts -->
463
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
464
  <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
465
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
466
 
467
  <script>
468
+ // ข้อมูลจำลองสถิติผู้สมัครและสัดส่วนรับสมัครในแต่ละ Funnel
469
  const rawData = [
470
+ { id: 1, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 250, paid: 240, eligible: 90, registered: 80 },
471
+ { id: 2, name: "โรงเรียนเตรียมอุดมศึกษา", province: "กรุงเทพมหานคร", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", programType: "ภาคพิเศษ", applied: 150, paid: 140, eligible: 50, registered: 45 },
472
+ { id: 3, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมโย��า", round: "รอบที่ 2 Quota", programType: "ภาคปกติ", applied: 110, paid: 105, eligible: 20, registered: 15 },
473
+ { id: 4, name: "โรงเรียนสวนกุหลาบวิทยาลัย", province: "กรุงเทพมหานคร", faculty: "บริหารธุรกิจ", major: "การตลาด", round: "รอบที่ 3 Admission", programType: "ภาคปกติ", applied: 80, paid: 75, eligible: 20, registered: 20 },
474
+ { id: 5, name: "โรงเรียนมหิดลวิทยานุสรณ์", province: "นครปฐม", faculty: "วิทยาศาสตร์", major: "ชีววิทยา", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 130, paid: 125, eligible: 60, registered: 55 },
475
+ { id: 6, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "บริหารธุรกิจ", major: "การบัญชี", round: "รอบที่ 2 Quota", programType: "ภาคปกติ", applied: 290, paid: 280, eligible: 105, registered: 100 },
476
+ { id: 7, name: "โรงเรียนพิษณุโลกพิทยาคม", province: "พิษณุโลก", faculty: "มนุษยศาสตร์", major: "ภาษาอังกฤษ", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 220, paid: 210, eligible: 80, registered: 70 },
477
+ { id: 8, name: "โรงเรียนขอนแก่นวิทยายน", province: "ขอนแก่น", faculty: "วิศวกรรมศาสตร์", major: "วิศวกรรมคอมพิวเตอร์", round: "รอบที่ 2 Quota", programType: "ภาคปกติ", applied: 250, paid: 240, eligible: 90, registered: 85 },
478
+ { id: 9, name: "โรงเรียนหาดใหญ่วิทยาลัย", province: "สงขลา", faculty: "วิทยาศาสตร์", major: "วิทยาการคอมพิวเตอร์", round: "รอบที่ 1 Portfolio", programType: "ภาคปกติ", applied: 190, paid: 180, eligible: 70, registered: 65 },
479
+ { id: 10, name: "โรงเรียนภูเก็ตวิทยาลัย", province: "ภูเก็ต", faculty: "บริหารธุรกิจ", major: "ธุรกิจระหว่างประเทศ", round: "รอบที่ 3 Admission", programType: "ภาคปกติ", applied: 160, paid: 150, eligible: 65, registered: 60 }
 
 
 
 
 
 
 
480
  ];
481
 
482
+ // พิกัดและชุดสีจำแนกพื้นที่รายจังหวัดบนแผนที่
483
+ const provinceConfigs = {
484
+ "กรุงเทพมหานคร": { coords: [13.7563, 100.5018], color: "#0d9488", fillColor: "#14b8a6" },
485
+ "นครปฐม": { coords: [13.8140, 100.0373], color: "#0284c7", fillColor: "#38bdf8" },
486
+ "พิษณุโลก": { coords: [16.8211, 100.2659], color: "#7c3aed", fillColor: "#a78bfa" },
487
+ "ขอนแก่น": { coords: [16.4322, 102.8335], color: "#d97706", fillColor: "#fbbf24" },
488
+ "สงขลา": { coords: [7.1898, 100.5954], color: "#db2777", fillColor: "#f472b6" },
489
+ "ภูเก็ต": { coords: [7.8804, 98.3923], color: "#ea580c", fillColor: "#fb923c" }
 
 
 
 
 
 
 
 
 
 
 
 
490
  };
491
 
 
 
 
 
492
  let activeFilteredData = [];
493
+ let map = null;
494
+ let markerLayerGroup = null;
495
+
496
+ const defaultMapCenter = [13.0, 101.0];
497
+ const defaultMapZoom = 6;
498
 
499
+ // ฟังก์ชันช่วยจัดการเนื้อหาข้อความ DOM อย่างปลอดภัย
500
+ function setInnerTextSafe(id, value) {
501
+ const el = document.getElementById(id);
502
+ if (el) {
503
+ el.innerText = value;
504
+ }
505
+ }
506
 
507
+ // จัดการเริ่มต้นกระบวนการเมื่อ DOM พร้อม
508
+ function initDashboard() {
509
  initDropdowns();
510
+ initLeafletMap();
511
+ applyFilters();
512
+ }
513
+
514
+ if (document.readyState === 'complete' || document.readyState === 'interactive') {
515
+ initDashboard();
516
+ } else {
517
+ document.addEventListener('DOMContentLoaded', initDashboard);
518
+ }
519
 
520
  function initDropdowns() {
521
  const facultyFilter = document.getElementById('facultyFilter');
522
+ if (facultyFilter) {
523
+ const faculties = [...new Set(rawData.map(item => item.faculty))].sort();
524
+ faculties.forEach(fac => {
525
+ const option = document.createElement('option');
526
+ option.value = fac;
527
+ option.textContent = fac;
528
+ facultyFilter.appendChild(option);
529
+ });
530
+ }
531
 
532
  const roundFilter = document.getElementById('roundFilter');
533
+ if (roundFilter) {
534
+ const rounds = [...new Set(rawData.map(item => item.round))].sort();
535
+ rounds.forEach(rnd => {
536
+ const option = document.createElement('option');
537
+ option.value = rnd;
538
+ option.textContent = rnd;
539
+ roundFilter.appendChild(option);
540
+ });
541
+ }
542
+ }
543
+
544
+ function initLeafletMap() {
545
+ const mapContainer = document.getElementById('map');
546
+ if (!mapContainer) return;
547
+
548
+ map = L.map('map', {
549
+ center: defaultMapCenter,
550
+ zoom: defaultMapZoom,
551
+ zoomControl: true,
552
+ attributionControl: false
553
  });
554
+
555
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
556
+ maxZoom: 18
557
+ }).addTo(map);
558
+
559
+ markerLayerGroup = L.layerGroup().addTo(map);
560
+
561
+ const loader = document.getElementById('map-loading');
562
+ if (loader) {
563
+ loader.style.opacity = '0';
564
+ setTimeout(() => {
565
+ loader.classList.add('d-none');
566
+ }, 300);
567
+ }
568
+
569
+ // ปรับมุมมองพิกัดแผนที่เพื่อป้องกันการโหลดกระเบื้องสีเทาค้าง
570
+ setTimeout(() => {
571
+ if (map) {
572
+ map.invalidateSize();
573
+ }
574
+ }, 400);
575
+ }
576
+
577
+ function resetMapView() {
578
+ if (map) {
579
+ map.setView(defaultMapCenter, defaultMapZoom);
580
+ }
581
  }
582
 
583
  function onFacultyChange() {
584
+ const facultyEl = document.getElementById('facultyFilter');
585
  const majorFilter = document.getElementById('majorFilter');
586
+ if (!facultyEl || !majorFilter) return;
587
+
588
+ const faculty = facultyEl.value;
589
  majorFilter.innerHTML = '<option value="">-- สาขาวิชาทั้งหมด --</option>';
590
 
591
  if (faculty) {
 
601
  }
602
 
603
  function applyFilters() {
604
+ const facultyEl = document.getElementById('facultyFilter');
605
+ const majorEl = document.getElementById('majorFilter');
606
+ const roundEl = document.getElementById('roundFilter');
607
+ const programEl = document.getElementById('programFilter');
608
+
609
+ const faculty = facultyEl ? facultyEl.value : "";
610
+ const major = majorEl ? majorEl.value : "";
611
+ const round = roundEl ? roundEl.value : "";
612
+ const program = programEl ? programEl.value : "ภาคปกติ";
613
 
614
  activeFilteredData = rawData.filter(item => {
615
  return (faculty === "" || item.faculty === faculty) &&
616
  (major === "" || item.major === major) &&
617
+ (round === "" || item.round === round) &&
618
+ (program === "" || item.programType === program);
619
  });
620
 
621
+ updateStatsAndRankings(activeFilteredData);
622
  renderTable(activeFilteredData);
623
+ updateLeafletMapMarkers(activeFilteredData);
 
 
 
624
  }
625
 
626
+ function updateStatsAndRankings(data) {
627
+ let totalApplied = 0;
628
+ let totalPaid = 0;
629
+ let totalEligible = 0;
630
+ let totalRegistered = 0;
631
 
632
+ let provinceCounts = {};
633
+ let schoolCounts = {};
 
 
 
634
 
635
+ data.forEach(item => {
636
+ totalApplied += item.applied;
637
+ totalPaid += item.paid;
638
+ totalEligible += item.eligible;
639
+ totalRegistered += item.registered;
640
 
641
+ // รวมข้อมูลรายจังหวัด
642
+ if (!provinceCounts[item.province]) {
643
+ provinceCounts[item.province] = 0;
644
+ }
645
+ provinceCounts[item.province] += item.registered;
646
 
647
+ // รวมข้อมูลรายโรงเรียน
648
+ if (!schoolCounts[item.name]) {
649
+ schoolCounts[item.name] = 0;
650
+ }
651
+ schoolCounts[item.name] += item.registered;
652
  });
653
 
654
+ // อัปเดตการ์ดสถิติ 4 ขั้นตอนอย่างปลอดภัย
655
+ setInnerTextSafe('statApplied', totalApplied.toLocaleString());
656
+ setInnerTextSafe('statPaid', totalPaid.toLocaleString());
657
+ setInnerTextSafe('statEligible', totalEligible.toLocaleString());
658
+ setInnerTextSafe('statRegistered', totalRegistered.toLocaleString());
659
 
660
+ // ประมวลผลและแสดงผล Top 5 จังหวัด
661
+ const sortedProvinces = Object.entries(provinceCounts)
662
+ .sort((a, b) => b[1] - a[1])
663
+ .slice(0, 5);
664
+
665
+ const topProvsContainer = document.getElementById('topProvincesList');
666
+ if (topProvsContainer) {
667
+ topProvsContainer.innerHTML = '';
668
+ const maxProvVal = sortedProvinces[0] ? sortedProvinces[0][1] : 1;
669
+
670
+ if (sortedProvinces.length === 0) {
671
+ topProvsContainer.innerHTML = '<div class="text-center text-muted py-4 small">ไม่มีข้อมูลจัดอันดับในส่วนนี้</div>';
672
+ } else {
673
+ sortedProvinces.forEach(([province, count], idx) => {
674
+ const percent = (count / maxProvVal) * 100;
675
+ const itemHTML = `
676
+ <div>
677
+ <div class="d-flex justify-content-between align-items-center mb-1">
678
+ <span class="small fw-bold text-slate-700">${idx + 1}. จังหวัด${province}</span>
679
+ <span class="small fw-extrabold text-brand-700">${count.toLocaleString()} คน</span>
680
+ </div>
681
+ <div class="custom-progress">
682
+ <div class="custom-progress-bar" style="width: ${percent}%"></div>
683
+ </div>
684
+ </div>
685
+ `;
686
+ topProvsContainer.insertAdjacentHTML('beforeend', itemHTML);
687
+ });
688
  }
689
  }
690
 
691
+ // ประมวลผลและแสดงผล Top 5 โรงเรียน
692
+ const sortedSchools = Object.entries(schoolCounts)
693
+ .sort((a, b) => b[1] - a[1])
694
+ .slice(0, 5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
695
 
696
+ const topSchoolsContainer = document.getElementById('topSchoolsList');
697
+ if (topSchoolsContainer) {
698
+ topSchoolsContainer.innerHTML = '';
699
+ const maxSchoolVal = sortedSchools[0] ? sortedSchools[0][1] : 1;
700
 
701
+ if (sortedSchools.length === 0) {
702
+ topSchoolsContainer.innerHTML = '<div class="text-center text-muted py-4 small">ไม่มีข้อมูลจัดอันดับในส่วนนี้</div>';
703
+ } else {
704
+ sortedSchools.forEach(([school, count], idx) => {
705
+ const percent = (count / maxSchoolVal) * 100;
706
+ const itemHTML = `
707
+ <div>
708
+ <div class="d-flex justify-content-between align-items-center mb-1">
709
+ <span class="small fw-bold text-slate-700">${idx + 1}. ${school}</span>
710
+ <span class="small fw-extrabold text-amber-700">${count.toLocaleString()} คน</span>
711
+ </div>
712
+ <div class="custom-progress">
713
+ <div class="custom-progress-bar bg-warning" style="width: ${percent}%"></div>
714
+ </div>
715
+ </div>
716
+ `;
717
+ topSchoolsContainer.insertAdjacentHTML('beforeend', itemHTML);
718
+ });
719
+ }
720
  }
721
+ }
722
+
723
+ function updateLeafletMapMarkers(data) {
724
+ if (!markerLayerGroup) return;
725
+ markerLayerGroup.clearLayers();
726
+
727
+ const provinceSummary = {};
728
+ data.forEach(item => {
729
+ if (!provinceSummary[item.province]) {
730
+ provinceSummary[item.province] = { applied: 0, paid: 0, eligible: 0, registered: 0, schools: {} };
 
 
 
 
731
  }
732
+ provinceSummary[item.province].applied += item.applied;
733
+ provinceSummary[item.province].paid += item.paid;
734
+ provinceSummary[item.province].eligible += item.eligible;
735
+ provinceSummary[item.province].registered += item.registered;
736
+ provinceSummary[item.province].schools[item.name] = (provinceSummary[item.province].schools[item.name] || 0) + item.registered;
737
  });
738
 
739
+ for (const [provinceName, stats] of Object.entries(provinceSummary)) {
740
+ const config = provinceConfigs[provinceName];
741
+ if (config && stats.applied > 0) {
742
+ const radius = Math.max(12, Math.min(32, Math.sqrt(stats.registered) * 2.5));
743
+
744
+ const circle = L.circleMarker(config.coords, {
745
+ radius: radius,
746
+ color: config.color,
747
+ fillColor: config.fillColor,
748
+ fillOpacity: 0.75,
749
+ weight: 2
750
+ }).addTo(markerLayerGroup);
751
 
752
+ let schoolListHTML = Object.entries(stats.schools)
753
+ .sort((a,b) => b[1] - a[1])
754
+ .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>`)
755
+ .join('');
756
 
757
+ const popupHTML = `
758
+ <div style="font-family:'Sarabun', sans-serif; min-width: 250px;">
759
+ <div class="d-flex align-items-center mb-2 pb-2 border-bottom">
760
+ <span class="nested-icon-prov me-2" style="background-color: ${config.fillColor}33; color: ${config.color};">
761
+ <i class="fa-solid fa-map-location-dot"></i>
762
+ </span>
763
+ <h6 class="fw-bold text-dark mb-0">จังหวัด${provinceName}</h6>
764
+ </div>
765
+ <div class="row g-2 mb-2 pb-2 border-bottom text-center">
766
+ <div class="col-6">
767
+ <span class="small text-muted d-block" style="font-size: 0.7rem;">ผู้สมัครสะสม</span>
768
+ <span class="fw-bold text-primary">${stats.applied}</span>
769
+ </div>
770
+ <div class="col-6">
771
+ <span class="small text-muted d-block" style="font-size: 0.7rem;">ชำระค่าสมัคร</span>
772
+ <span class="fw-bold text-indigo">${stats.paid}</span>
773
+ </div>
774
+ <div class="col-6 mt-1">
775
+ <span class="small text-muted d-block" style="font-size: 0.7rem;">ผู้มีสิทธิ์เข้าศึกษา</span>
776
+ <span class="fw-bold text-info">${stats.eligible}</span>
777
+ </div>
778
+ <div class="col-6 mt-1">
779
+ <span class="small text-muted d-block" style="font-size: 0.7rem;">ชำระรายงานตัว</span>
780
+ <span class="fw-extrabold text-success">${stats.registered}</span>
781
+ </div>
782
+ </div>
783
+ <div>
784
+ <span class="small text-muted d-block mb-1 font-semibold">รายชื่อสถานศึกษาเดิม (รายงานตัวแล้ว):</span>
785
+ <div style="max-height: 80px; overflow-y: auto;" class="pe-1">
786
+ ${schoolListHTML}
787
+ </div>
788
+ </div>
789
+ </div>
790
+ `;
791
+
792
+ circle.bindPopup(popupHTML);
793
+ }
794
+ }
795
+ }
796
+
797
+ function focusOnProvinceOnMap(provinceName) {
798
+ const config = provinceConfigs[provinceName];
799
+ if (config && map) {
800
+ map.flyTo(config.coords, 10, { duration: 1.2 });
801
  }
802
  }
803
 
804
  function renderTable(data) {
805
  const tbody = document.getElementById('dataTableBody');
806
+ if (!tbody) return;
807
  tbody.innerHTML = '';
808
 
809
  if(data.length === 0) {
810
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger py-5 fw-medium">ไม่พบข้อมูลที่ตรงกับเงื่อนไขตัวกรองในขณะนี้</td></tr>';
811
  return;
812
  }
813
 
814
  const grouped = {};
815
  data.forEach(item => {
816
+ if (!grouped[item.province]) grouped[item.province] = { applied: 0, paid: 0, eligible: 0, registered: 0, schools: {} };
817
+ grouped[item.province].applied += item.applied;
818
+ grouped[item.province].paid += item.paid;
819
+ grouped[item.province].eligible += item.eligible;
820
+ grouped[item.province].registered += item.registered;
821
 
822
  if (!grouped[item.province].schools[item.name]) {
823
+ grouped[item.province].schools[item.name] = { applied: 0, paid: 0, eligible: 0, registered: 0, faculties: {} };
824
  }
825
+ grouped[item.province].schools[item.name].applied += item.applied;
826
+ grouped[item.province].schools[item.name].paid += item.paid;
827
+ grouped[item.province].schools[item.name].eligible += item.eligible;
828
+ grouped[item.province].schools[item.name].registered += item.registered;
829
 
830
  if (!grouped[item.province].schools[item.name].faculties[item.faculty]) {
831
+ grouped[item.province].schools[item.name].faculties[item.faculty] = { applied: 0, paid: 0, eligible: 0, registered: 0, majors: {} };
832
  }
833
+ grouped[item.province].schools[item.name].faculties[item.faculty].applied += item.applied;
834
+ grouped[item.province].schools[item.name].faculties[item.faculty].paid += item.paid;
835
+ grouped[item.province].schools[item.name].faculties[item.faculty].eligible += item.eligible;
836
+ grouped[item.province].schools[item.name].faculties[item.faculty].registered += item.registered;
837
 
 
838
  if (!grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major]) {
839
+ grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major] = { applied: 0, paid: 0, eligible: 0, registered: 0 };
840
  }
841
+ grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].applied += item.applied;
842
+ grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].paid += item.paid;
843
+ grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].eligible += item.eligible;
844
+ grouped[item.province].schools[item.name].faculties[item.faculty].majors[item.major].registered += item.registered;
845
  });
846
 
847
+ const sortedProvinces = Object.keys(grouped).sort((a, b) => grouped[b].registered - grouped[a].registered);
848
 
849
  let provIndex = 1;
850
  sortedProvinces.forEach(prov => {
 
853
 
854
  // --- แถวระดับที่ 1: จังหวัด ---
855
  const trProv = document.createElement('tr');
856
+ trProv.className = 'clickable-row border-bottom border-light';
857
+ trProv.onclick = () => {
858
+ toggleRow(provId);
859
+ focusOnProvinceOnMap(prov);
860
+ };
861
  trProv.innerHTML = `
862
+ <td class="px-4 py-3 text-start fw-bold text-dark">
863
+ <div class="d-flex align-items-center">
864
+ <i class="fa-solid fa-chevron-right text-secondary toggle-icon me-2" id="icon-${provId}"></i>
865
+ <span class="nested-icon-prov me-2.5">
866
+ <i class="fa-solid fa-map-location-dot small"></i>
867
+ </span>
868
+ <span>จังหวัด${prov}</span>
869
+ </div>
870
  </td>
871
+ <td class="px-3 py-3 text-center">
872
+ <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${provData.applied.toLocaleString()}</span>
873
+ </td>
874
+ <td class="px-3 py-3 text-center">
875
+ <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${provData.paid.toLocaleString()}</span>
876
+ </td>
877
+ <td class="px-3 py-3 text-center">
878
+ <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${provData.eligible.toLocaleString()}</span>
879
+ </td>
880
+ <td class="px-3 py-3 text-center">
881
+ <span class="badge rounded-pill badge-registered px-3 py-1.5 fs-7">${provData.registered.toLocaleString()}</span>
882
  </td>
883
  `;
884
  tbody.appendChild(trProv);
885
 
886
+ const sortedSchools = Object.keys(provData.schools).sort((a, b) => provData.schools[b].registered - provData.schools[a].registered);
887
 
888
  let schIndex = 1;
889
  sortedSchools.forEach(sch => {
 
892
 
893
  // --- แถวระดับที่ 2: สถานศึกษา ---
894
  const trSch = document.createElement('tr');
895
+ trSch.className = `child-${provId} clickable-row bg-white border-bottom border-light`;
896
  trSch.style.display = 'none';
897
  trSch.onclick = () => toggleRow(schId);
898
  trSch.innerHTML = `
899
+ <td class="px-4 py-3 text-start fw-semibold text-secondary" style="padding-left: 2.75rem !important;">
900
+ <div class="d-flex align-items-center">
901
+ <i class="fa-solid fa-chevron-right text-secondary toggle-icon me-2" id="icon-${schId}"></i>
902
+ <span class="nested-icon-school me-2">
903
+ <i class="fa-solid fa-school" style="font-size: 0.7rem;"></i>
904
+ </span>
905
+ <span>${sch}</span>
906
+ </div>
907
  </td>
908
+ <td class="px-3 py-3 text-center">
909
+ <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${schData.applied.toLocaleString()}</span>
910
+ </td>
911
+ <td class="px-3 py-3 text-center">
912
+ <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${schData.paid.toLocaleString()}</span>
913
+ </td>
914
+ <td class="px-3 py-3 text-center">
915
+ <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${schData.eligible.toLocaleString()}</span>
916
+ </td>
917
+ <td class="px-3 py-3 text-center">
918
+ <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8">${schData.registered.toLocaleString()}</span>
919
  </td>
920
  `;
921
  tbody.appendChild(trSch);
922
 
923
+ const sortedFaculties = Object.keys(schData.faculties).sort((a, b) => schData.faculties[b].registered - schData.faculties[a].registered);
924
 
925
  let facIndex = 1;
926
  sortedFaculties.forEach(fac => {
 
929
 
930
  // --- แถวระดับที่ 3: คณะ ---
931
  const trFac = document.createElement('tr');
932
+ trFac.className = `child-${schId} parent-${provId} clickable-row bg-white border-bottom border-light`;
933
  trFac.style.display = 'none';
934
  trFac.onclick = () => toggleRow(facId);
935
  trFac.innerHTML = `
936
+ <td class="px-4 py-3 text-start fw-medium text-secondary" style="padding-left: 4.75rem !important;">
937
+ <div class="d-flex align-items-center">
938
+ <i class="fa-solid fa-chevron-right text-secondary toggle-icon me-2" id="icon-${facId}"></i>
939
+ <span class="nested-icon-faculty me-2">
940
+ <i class="fa-solid fa-building-columns" style="font-size: 0.6rem;"></i>
941
+ </span>
942
+ <span>คณะ${fac}</span>
943
+ </div>
944
  </td>
945
+ <td class="px-3 py-3 text-center">
946
+ <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${facData.applied.toLocaleString()}</span>
947
+ </td>
948
+ <td class="px-3 py-3 text-center">
949
+ <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${facData.paid.toLocaleString()}</span>
950
+ </td>
951
+ <td class="px-3 py-3 text-center">
952
+ <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${facData.eligible.toLocaleString()}</span>
953
+ </td>
954
+ <td class="px-3 py-3 text-center">
955
+ <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8">${facData.registered.toLocaleString()}</span>
956
  </td>
957
  `;
958
  tbody.appendChild(trFac);
959
 
960
+ const sortedMajors = Object.keys(facData.majors).sort((a, b) => facData.majors[b].registered - facData.majors[a].registered);
961
 
962
  // --- แถวระดับที่ 4: สาขาวิชา ---
963
  sortedMajors.forEach(majorName => {
964
+ const majorStats = facData.majors[majorName];
965
  const trDet = document.createElement('tr');
966
+ trDet.className = `child-${facId} parent-${schId} parent-${provId} bg-light bg-opacity-25`;
967
  trDet.style.display = 'none';
968
  trDet.innerHTML = `
969
+ <td class="px-4 py-2.5 text-start text-secondary" style="padding-left: 6.75rem !important;">
970
+ <div class="d-flex align-items-center">
971
+ <i class="fa-solid fa-turn-up fa-rotate-90 text-secondary-subtle me-3"></i>
972
+ <span class="me-2"><i class="fa-solid fa-book-open text-secondary-subtle small me-1.5"></i>${majorName}</span>
973
+ </div>
974
+ </td>
975
+ <td class="px-3 py-2.5 text-center">
976
+ <span class="badge rounded-pill badge-applied px-2.5 py-1 fs-8">${majorStats.applied.toLocaleString()}</span>
977
+ </td>
978
+ <td class="px-3 py-2.5 text-center">
979
+ <span class="badge rounded-pill badge-paid px-2.5 py-1 fs-8">${majorStats.paid.toLocaleString()}</span>
980
+ </td>
981
+ <td class="px-3 py-2.5 text-center">
982
+ <span class="badge rounded-pill badge-eligible px-2.5 py-1 fs-8">${majorStats.eligible.toLocaleString()}</span>
983
  </td>
984
+ <td class="px-3 py-2.5 text-center">
985
+ <span class="badge rounded-pill badge-registered px-2.5 py-1 fs-8">
986
+ ${majorStats.registered.toLocaleString()}
987
  </span>
988
  </td>
989
  `;
 
999
  });
1000
  }
1001
 
 
1002
  function toggleRow(targetId) {
1003
  const icon = document.getElementById(`icon-${targetId}`);
1004
+ if (!icon) return;
1005
  const isExpanded = icon.classList.contains('fa-chevron-down');
1006
 
1007
  if (isExpanded) {
 
1013
  const descendants = document.querySelectorAll(`.parent-${targetId}`);
1014
  descendants.forEach(el => el.style.display = 'none');
1015
 
 
1016
  children.forEach(row => {
1017
  const subIcon = row.querySelector('.toggle-icon');
1018
  if (subIcon && subIcon.classList.contains('fa-chevron-down')) {
 
1035
 
1036
  function showInfo() {
1037
  Swal.fire({
1038
+ title: 'ข้อมูลระบบรายงานสถิติใหม่',
1039
  html: `
1040
+ <div class="text-start mt-2" style="font-size: 0.875rem; color: #475569;">
1041
+ <p class="font-bold text-teal-700 mb-2">โครงสร้างรายงานวิเคราะห์ระดับชั้นข้อมูลรับสมัครเรียน</p>
1042
+ <ul class="ps-3 mb-0" style="list-style-type: disc;">
1043
+ <li class="mb-2"><b>จัดเรียงสีกราฟแนวตั้ง (Vertical Alignment):</b> ล็อกคู่สีประจำแต่ละดัชนีชี้วัดในการรับสมัคร ทำให้ตารางมีความคมชัด มีระเบียบ และวิเคราะห์ง่ายในแนวตั้ง</li>
1044
+ <li class="mb-2"><b>แก้ปัญหาเส้นตารางเบี้ยว (Render Layout Fix):</b> ยกเลิกการใช้ Flexbox บนเซลล์แท็ก TD โดยตรง และย้ายเข้าไปทำงานใน DIV ครอบดีไซน์ย่อยแทน เพื่อฟื้นฟูระนาบเส้นขอบตารางให้ตรงกันอย่างเนี๊ยบสวยงาม</li>
1045
+ <li class="mb-2"><b>แผนที่ Leaflet แสนเสรี:</b> ซูมส่องพิกัดพร้อมกล่องข้อมูลสรุปรายจังหวัดได้อย่างราบรื่น</li>
1046
  </ul>
1047
  </div>
1048
  `,
1049
  icon: 'info',
1050
  confirmButtonText: 'ตกลง',
1051
+ confirmButtonColor: '#0d9488',
1052
  });
1053
  }
1054
  </script>