การงง การเงิน
BI & ANALYTICS
กำลังโหลดข้อมูล…
waranyu.trkm@gmail.com
📈 แนวโน้มการเติบโตและการเข้าชม (Growth & Performance Trends)
เปรียบเทียบผู้เข้าชมเว็บไซต์ (GA4) กับยอดคลิกค้นหาออร์แกนิกจาก Google Search Console
ผู้เข้าชม (Users)
คลิกค้นหา (GSC Clicks)
🔍 เจาะลึกคำค้นหา Google Search Console (Search Queries Analysis)
คีย์เวิร์ดที่ดึงดูดผู้ใช้งานเข้ามาวางแผนการเงิน พร้อมระบุคีย์เวิร์ดที่มีศักยภาพดันอันดับ (Opportunity)
| คำค้นหา (Query) | จำนวนคลิก (Clicks) | การแสดงผล (Impressions) | CTR (%) | อันดับเฉลี่ย | โอกาสเติบโต |
|---|
🏆 ประสิทธิภาพเครื่องมือและหน้าเว็บยอดนิยม (Top Content & Calculators)
หน้าเครื่องมือวางแผนการเงินที่ได้รับความสนใจและการมีส่วนร่วมสูงสุด
| เครื่องมือ / หน้าเว็บ | ยอดเข้าชม (Pageviews) | ผู้ใช้งาน (Unique Users) | คลิกจาก Google (GSC) |
|---|
⚡ สถิติการมีส่วนร่วมและ Conversion (User Engagement Matrix)
พฤติกรรมการคำนวณ, การคัดลอกสรุปผลลัพธ์ (Copy Summary), และการคลิกติดต่อ
(function() { const INITIAL_BI_SNAPSHOT = { "generatedAt": "2026-09-19T17:50:52.553Z", "siteUrl": "https://kanngongkanngoen.com", "domain": "kanngongkanngoen.com", "days": 30, "summary": { "totalVisitors": 12477, "totalSessions": 16440, "totalPageviews": 43605, "totalCalculations": 17888, "totalCopyClicks": 4341, "totalClicks": 10702, "totalImpressions": 87057, "avgCtr": 12.29, "avgPosition": 4.2 }, "dailyMetrics": [ { "date": "2026-08-21", "visitors": 342, "sessions": 427, "pageviews": 1242, "calculations": 460, "copy_summary_clicks": 112, "gsc_clicks": 294, "gsc_impressions": 2611, "gsc_ctr": 0.1126, "gsc_avg_position": 4.72 }, { "date": "2026-08-22", "visitors": 385, "sessions": 491, "pageviews": 1297, "calculations": 510, "copy_summary_clicks": 125, "gsc_clicks": 256, "gsc_impressions": 2385, "gsc_ctr": 0.1073, "gsc_avg_position": 4.57 }, { "date": "2026-08-23", "visitors": 381, "sessions": 458, "pageviews": 1118, "calculations": 510, "copy_summary_clicks": 126, "gsc_clicks": 280, "gsc_impressions": 2393, "gsc_ctr": 0.117, "gsc_avg_position": 4.56 }, { "date": "2026-08-24", "visitors": 373, "sessions": 495, "pageviews": 1230, "calculations": 515, "copy_summary_clicks": 142, "gsc_clicks": 308, "gsc_impressions": 2323, "gsc_ctr": 0.1326, "gsc_avg_position": 4.54 }, { "date": "2026-08-25", "visitors": 346, "sessions": 439, "pageviews": 1196, "calculations": 523, "copy_summary_clicks": 117, "gsc_clicks": 312, "gsc_impressions": 2562, "gsc_ctr": 0.1218, "gsc_avg_position": 4.42 }, { "date": "2026-08-26", "visitors": 337, "sessions": 435, "pageviews": 1243, "calculations": 496, "copy_summary_clicks": 112, "gsc_clicks": 323, "gsc_impressions": 2637, "gsc_ctr": 0.1225, "gsc_avg_position": 4.44 }, { "date": "2026-08-27", "visitors": 335, "sessions": 419, "pageviews": 1026, "calculations": 464, "copy_summary_clicks": 118, "gsc_clicks": 327, "gsc_impressions": 2842, "gsc_ctr": 0.1151, "gsc_avg_position": 4.52 }, { "date": "2026-08-28", "visitors": 391, "sessions": 479, "pageviews": 1174, "calculations": 610, "copy_summary_clicks": 143, "gsc_clicks": 351, "gsc_impressions": 2554, "gsc_ctr": 0.1374, "gsc_avg_position": 4.42 }, { "date": "2026-08-29", "visitors": 472, "sessions": 624, "pageviews": 1718, "calculations": 621, "copy_summary_clicks": 148, "gsc_clicks": 356, "gsc_impressions": 2581, "gsc_ctr": 0.1379, "gsc_avg_position": 4.34 }, { "date": "2026-08-30", "visitors": 418, "sessions": 562, "pageviews": 1334, "calculations": 583, "copy_summary_clicks": 135, "gsc_clicks": 337, "gsc_impressions": 2403, "gsc_ctr": 0.1402, "gsc_avg_position": 4.34 }, { "date": "2026-08-31", "visitors": 325, "sessions": 449, "pageviews": 1186, "calculations": 522, "copy_summary_clicks": 115, "gsc_clicks": 347, "gsc_impressions": 2438, "gsc_ctr": 0.1423, "gsc_avg_position": 4.35 }, { "date": "2026-09-01", "visitors": 352, "sessions": 444, "pageviews": 1048, "calculations": 449, "copy_summary_clicks": 128, "gsc_clicks": 316, "gsc_impressions": 2643, "gsc_ctr": 0.1196, "gsc_avg_position": 4.28 }, { "date": "2026-09-02", "visitors": 361, "sessions": 498, "pageviews": 1372, "calculations": 495, "copy_summary_clicks": 122, "gsc_clicks": 353, "gsc_impressions": 2583, "gsc_ctr": 0.1367, "gsc_avg_position": 4.28 }, { "date": "2026-09-03", "visitors": 427, "sessions": 608, "pageviews": 1494, "calculations": 591, "copy_summary_clicks": 157, "gsc_clicks": 354, "gsc_impressions": 2755, "gsc_ctr": 0.1285, "gsc_avg_position": 4.25 }, { "date": "2026-09-04", "visitors": 408, "sessions": 556, "pageviews": 1437, "calculations": 536, "copy_summary_clicks": 127, "gsc_clicks": 382, "gsc_impressions": 2837, "gsc_ctr": 0.1346, "gsc_avg_position": 4.13 }, { "date": "2026-09-05", "visitors": 485, "sessions": 598, "pageviews": 1703, "calculations": 667, "copy_summary_clicks": 163, "gsc_clicks": 343, "gsc_impressions": 3283, "gsc_ctr": 0.1045, "gsc_avg_position": 4.09 }, { "date": "2026-09-06", "visitors": 502, "sessions": 641, "pageviews": 1897, "calculations": 676, "copy_summary_clicks": 194, "gsc_clicks": 367, "gsc_impressions": 3241, "gsc_ctr": 0.1132, "gsc_avg_position": 4.25 }, { "date": "2026-09-07", "visitors": 426, "sessions": 606, "pageviews": 1596, "calculations": 674, "copy_summary_clicks": 158, "gsc_clicks": 377, "gsc_impressions": 2726, "gsc_ctr": 0.1383, "gsc_avg_position": 4.02 }, { "date": "2026-09-08", "visitors": 420, "sessions": 555, "pageviews": 1534, "calculations": 656, "copy_summary_clicks": 133, "gsc_clicks": 344, "gsc_impressions": 3387, "gsc_ctr": 0.1016, "gsc_avg_position": 4.06 }, { "date": "2026-09-09", "visitors": 369, "sessions": 524, "pageviews": 1473, "calculations": 551, "copy_summary_clicks": 123, "gsc_clicks": 384, "gsc_impressions": 3393, "gsc_ctr": 0.1132, "gsc_avg_position": 4.1 }, { "date": "2026-09-10", "visitors": 401, "sessions": 574, "pageviews": 1412, "calculations": 611, "copy_summary_clicks": 139, "gsc_clicks": 377, "gsc_impressions": 3039, "gsc_ctr": 0.1241, "gsc_avg_position": 3.97 }, { "date": "2026-09-11", "visitors": 396, "sessions": 570, "pageviews": 1501, "calculations": 612, "copy_summary_clicks": 139, "gsc_clicks": 340, "gsc_impressions": 3132, "gsc_ctr": 0.1086, "gsc_avg_position": 3.93 }, { "date": "2026-09-12", "visitors": 556, "sessions": 664, "pageviews": 1601, "calculations": 882, "copy_summary_clicks": 191, "gsc_clicks": 402, "gsc_impressions": 2884, "gsc_ctr": 0.1394, "gsc_avg_position": 4.05 }, { "date": "2026-09-13", "visitors": 546, "sessions": 704, "pageviews": 1808, "calculations": 821, "copy_summary_clicks": 209, "gsc_clicks": 400, "gsc_impressions": 3257, "gsc_ctr": 0.1228, "gsc_avg_position": 4 }, { "date": "2026-09-14", "visitors": 419, "sessions": 614, "pageviews": 1598, "calculations": 670, "copy_summary_clicks": 159, "gsc_clicks": 399, "gsc_impressions": 3483, "gsc_ctr": 0.1146, "gsc_avg_position": 3.95 }, { "date": "2026-09-15", "visitors": 446, "sessions": 607, "pageviews": 1742, "calculations": 662, "copy_summary_clicks": 163, "gsc_clicks": 415, "gsc_impressions": 3306, "gsc_ctr": 0.1255, "gsc_avg_position": 3.88 }, { "date": "2026-09-16", "visitors": 458, "sessions": 533, "pageviews": 1530, "calculations": 656, "copy_summary_clicks": 167, "gsc_clicks": 413, "gsc_impressions": 3187, "gsc_ctr": 0.1296, "gsc_avg_position": 3.68 }, { "date": "2026-09-17", "visitors": 432, "sessions": 564, "pageviews": 1425, "calculations": 603, "copy_summary_clicks": 154, "gsc_clicks": 368, "gsc_impressions": 3057, "gsc_ctr": 0.1204, "gsc_avg_position": 3.7 }, { "date": "2026-09-18", "visitors": 462, "sessions": 592, "pageviews": 1654, "calculations": 598, "copy_summary_clicks": 144, "gsc_clicks": 421, "gsc_impressions": 3545, "gsc_ctr": 0.1188, "gsc_avg_position": 3.73 }, { "date": "2026-09-19", "visitors": 506, "sessions": 710, "pageviews": 2016, "calculations": 664, "copy_summary_clicks": 178, "gsc_clicks": 456, "gsc_impressions": 3590, "gsc_ctr": 0.127, "gsc_avg_position": 3.84 } ], "aggregatedQueries": [ { "query": "วางแผนเกษียณ", "clicks": 408, "impressions": 2850, "ctr": 14.32, "avgPosition": 2.8, "isOpportunity": false }, { "query": "สูตรดอกเบี้ยทบต้น", "clicks": 337, "impressions": 2639, "ctr": 12.77, "avgPosition": 3.2, "isOpportunity": false }, { "query": "เงินสำรองฉุกเฉิน ควรมีกี่บาท", "clicks": 283, "impressions": 2280, "ctr": 12.41, "avgPosition": 2.4, "isOpportunity": false }, { "query": "วิธีปลดหนี้ บัตรเครดิต", "clicks": 256, "impressions": 2027, "ctr": 12.63, "avgPosition": 3.5, "isOpportunity": false }, { "query": "คำนวณเงินเกษียณ 60", "clicks": 233, "impressions": 1803, "ctr": 12.92, "avgPosition": 2.9, "isOpportunity": false }, { "query": "จัดงบประมาณ 50 30 20", "clicks": 204, "impressions": 1652, "ctr": 12.35, "avgPosition": 3.1, "isOpportunity": false }, { "query": "การงง การเงิน เครื่องมือ", "clicks": 168, "impressions": 858, "ctr": 19.58, "avgPosition": 1.2, "isOpportunity": false }, { "query": "ตารางออมเงิน 1 ปี", "clicks": 164, "impressions": 1740, "ctr": 9.43, "avgPosition": 4.8, "isOpportunity": true }, { "query": "early retirement คำนวณ fire", "clicks": 138, "impressions": 1464, "ctr": 9.43, "avgPosition": 5.2, "isOpportunity": true }, { "query": "เงินดาวน์บ้าน ต้องมีเท่าไหร่", "clicks": 121, "impressions": 1356, "ctr": 8.92, "avgPosition": 4.6, "isOpportunity": true }, { "query": "เช็คสุขภาพทางการเงิน ฟรี", "clicks": 114, "impressions": 1252, "ctr": 9.11, "avgPosition": 3.8, "isOpportunity": false }, { "query": "ออมเงินเกษียณเดือนละเท่าไหร่", "clicks": 101, "impressions": 1177, "ctr": 8.58, "avgPosition": 4.3, "isOpportunity": true }, { "query": "คํานวณความมั่งคั่งสุทธิ", "clicks": 95, "impressions": 1011, "ctr": 9.4, "avgPosition": 4.1, "isOpportunity": true }, { "query": "วางแผนการเงินส่วนบุคคล เริ่มต้น", "clicks": 90, "impressions": 1093, "ctr": 8.23, "avgPosition": 4.9, "isOpportunity": true }, { "query": "snowball vs avalanche ปลดหนี้", "clicks": 74, "impressions": 959, "ctr": 7.72, "avgPosition": 5.8, "isOpportunity": true } ], "aggregatedPages": [ { "path": "/retirement-planner/", "title": "เครื่องมือวางแผนเกษียณ", "pageviews": 9759, "users": 2721, "gsc_clicks": 2307, "gsc_impressions": 19062 }, { "path": "/compound-interest-calculator/", "title": "เครื่องคำนวณดอกเบี้ยทบต้น", "pageviews": 7753, "users": 2263, "gsc_clicks": 1902, "gsc_impressions": 15488 }, { "path": "/emergency-fund-planner/", "title": "คำนวณเงินสำรองฉุกเฉิน", "pageviews": 6165, "users": 1740, "gsc_clicks": 1504, "gsc_impressions": 12315 }, { "path": "/debt-payoff-planner/", "title": "วางแผนปลดหนี้ (Snowball vs Avalanche)", "pageviews": 5340, "users": 1470, "gsc_clicks": 1265, "gsc_impressions": 10344 }, { "path": "/early-retirement-calculator/", "title": "เครื่องคำนวณเกษียณก่อนกำหนด (FIRE)", "pageviews": 3451, "users": 994, "gsc_clicks": 860, "gsc_impressions": 6991 }, { "path": "/monthly-budget-planner/", "title": "จัดงบประมาณ 50/30/20", "pageviews": 3039, "users": 868, "gsc_clicks": 746, "gsc_impressions": 5960 }, { "path": "/home-down-payment-planner/", "title": "วางแผนดาวน์บ้าน", "pageviews": 2139, "users": 626, "gsc_clicks": 536, "gsc_impressions": 4351 }, { "path": "/financial-health-check/", "title": "เช็กสุขภาพการเงิน", "pageviews": 1770, "users": 510, "gsc_clicks": 419, "gsc_impressions": 3537 }, { "path": "/savings-goal-calculator/", "title": "คำนวณเป้าหมายเงินออม", "pageviews": 1747, "users": 497, "gsc_clicks": 435, "gsc_impressions": 3445 }, { "path": "/net-worth-calculator/", "title": "คำนวณความมั่งคั่งสุทธิ", "pageviews": 1291, "users": 376, "gsc_clicks": 323, "gsc_impressions": 2611 }, { "path": "/personal-financial-plan/", "title": "แผนการเงินส่วนบุคคล", "pageviews": 881, "users": 245, "gsc_clicks": 217, "gsc_impressions": 1732 }, { "path": "/blog/", "title": "บทความการเงิน", "pageviews": 438, "users": 124, "gsc_clicks": 107, "gsc_impressions": 870 } ], "aggregatedEvents": [ { "event_name": "calculate_click", "event_label": "retirement", "count": 5009 }, { "event_name": "copy_summary_click", "event_label": "financial_plan", "count": 4341 }, { "event_name": "calculate_click", "event_label": "compound_interest", "count": 3935 }, { "event_name": "calculate_click", "event_label": "emergency_fund", "count": 3222 }, { "event_name": "calculate_click", "event_label": "debt_payoff", "count": 2682 }, { "event_name": "calculate_click", "event_label": "early_retirement", "count": 1789 }, { "event_name": "social_outbound_click", "event_label": "line_oa", "count": 1000 }, { "event_name": "social_outbound_click", "event_label": "youtube", "count": 623 } ] };
/* ==================================================================== Kanngongkanngoen Admin BI Dashboard Client Engine Interactive Pure-SVG Visualizations, Table Filters, CSV Export & Auth Gate ==================================================================== */
const ADMIN_EMAIL_WHITELIST = ["waranyu.trkm@gmail.com"];
const state = { data: null, activeDays: 30, searchQuery: "", chipFilter: "all", // 'all' | 'opportunity' | 'top10' isAuthenticated: false, currentUser: null };
/* ---------------------------------------------------------------- Data Fetching */
async function loadBiData() { try { const res = await Promise.resolve({ ok: true, json: () => Promise.resolve(INITIAL_BI_SNAPSHOT) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); state.data = await res.json(); console.log("[BI Dashboard] Snapshot loaded:", state.data.summary); } catch (err) { console.warn("[BI Dashboard] Could not load local snapshot, falling back to mock data:", err); state.data = generateFallbackData(); } updateDashboard(); }
function generateFallbackData() { return { generatedAt: new Date().toISOString(), days: 30, summary: { totalVisitors: 12477, totalSessions: 16440, totalPageviews: 43605, totalCalculations: 17888, totalCopyClicks: 4341, totalClicks: 10702, totalImpressions: 87057, avgCtr: 12.29, avgPosition: 4.2 }, dailyMetrics: [], aggregatedQueries: [ { query: "วางแผนเกษียณ", clicks: 3360, impressions: 24600, ctr: 13.66, avgPosition: 2.8, isOpportunity: false }, { query: "สูตรดอกเบี้ยทบต้น", clicks: 2820, impressions: 22500, ctr: 12.53, avgPosition: 3.2, isOpportunity: false }, { query: "เงินสำรองฉุกเฉิน ควรมีกี่บาท", clicks: 2460, impressions: 19200, ctr: 12.81, avgPosition: 2.4, isOpportunity: false }, { query: "วิธีปลดหนี้ บัตรเครดิต", clicks: 2130, impressions: 17400, ctr: 12.24, avgPosition: 3.5, isOpportunity: false }, { query: "คำนวณเงินเกษียณ 60", clicks: 1950, impressions: 15600, ctr: 12.50, avgPosition: 2.9, isOpportunity: false }, { query: "จัดงบประมาณ 50 30 20", clicks: 1740, impressions: 13800, ctr: 12.61, avgPosition: 3.1, isOpportunity: false }, { query: "ตารางออมเงิน 1 ปี", clicks: 1350, impressions: 14700, ctr: 9.18, avgPosition: 4.8, isOpportunity: true }, { query: "early retirement คำนวณ fire", clicks: 1170, impressions: 12300, ctr: 9.51, avgPosition: 5.2, isOpportunity: true }, { query: "เงินดาวน์บ้าน ต้องมีเท่าไหร่", clicks: 1020, impressions: 11400, ctr: 8.95, avgPosition: 4.6, isOpportunity: true }, { query: "ออมเงินเกษียณเดือนละเท่าไหร่", clicks: 840, impressions: 9900, ctr: 8.48, avgPosition: 4.3, isOpportunity: true } ], aggregatedPages: [ { path: "/retirement-planner/", title: "เครื่องมือวางแผนเกษียณ", pageviews: 9593, users: 2745, gsc_clicks: 2354, gsc_impressions: 19152 }, { path: "/compound-interest-calculator/", title: "เครื่องคำนวณดอกเบี้ยทบต้น", pageviews: 7848, users: 2245, gsc_clicks: 1926, gsc_impressions: 15670 }, { path: "/emergency-fund-planner/", title: "คำนวณเงินสำรองฉุกเฉิน", pageviews: 6104, users: 1746, gsc_clicks: 1498, gsc_impressions: 12187 }, { path: "/debt-payoff-planner/", title: "วางแผนปลดหนี้ (Snowball vs Avalanche)", pageviews: 5232, users: 1497, gsc_clicks: 1284, gsc_impressions: 10446 }, { path: "/early-retirement-calculator/", title: "เครื่องคำนวณเกษียณก่อนกำหนด (FIRE)", pageviews: 3488, users: 998, gsc_clicks: 856, gsc_impressions: 6964 } ], aggregatedEvents: [ { event_name: "calculate_click", event_label: "retirement", count: 5008 }, { event_name: "calculate_click", event_label: "compound_interest", count: 3935 }, { event_name: "calculate_click", event_label: "emergency_fund", count: 3220 }, { event_name: "copy_summary_click", event_label: "financial_plan", count: 4341 }, { event_name: "social_outbound_click", event_label: "line_oa", count: 998 } ] }; }
/* ---------------------------------------------------------------- Render Engine */
function updateDashboard() { if (!state.data) return;
const rawDaily = state.data.dailyMetrics || []; const sliceDays = Math.min(state.activeDays, rawDaily.length || state.activeDays); const daily = rawDaily.slice(-sliceDays);
renderHeaderInfo(); renderKPIs(daily); renderTrendChart(daily); renderQueriesTable(); renderTopPagesTable(); renderEngagementMatrix(); }
function renderHeaderInfo() { const syncDateEl = document.getElementById("bi-sync-timestamp"); if (syncDateEl && state.data.generatedAt) { const d = new Date(state.data.generatedAt); syncDateEl.textContent = `ข้อมูลล่าสุด: ${d.toLocaleDateString("th-TH")} ${d.toLocaleTimeString("th-TH", { hour: "2-digit", minute: "2-digit" })}`; } }
function renderKPIs(daily) { let visitors, sessions, pageviews, calculations, clicks, impressions, ctr, avgPos;
if (daily && daily.length > 0) { visitors = daily.reduce((acc, r) => acc + (r.visitors || 0), 0); sessions = daily.reduce((acc, r) => acc + (r.sessions || 0), 0); pageviews = daily.reduce((acc, r) => acc + (r.pageviews || 0), 0); calculations = daily.reduce((acc, r) => acc + (r.calculations || 0), 0); clicks = daily.reduce((acc, r) => acc + (r.gsc_clicks || 0), 0); impressions = daily.reduce((acc, r) => acc + (r.gsc_impressions || 0), 0); ctr = Number(((clicks / (impressions || 1)) * 100).toFixed(2)); avgPos = Number((daily.reduce((acc, r) => acc + (r.gsc_avg_position || 0), 0) / daily.length).toFixed(1)); } else { const s = state.data.summary; visitors = s.totalVisitors; sessions = s.totalSessions; pageviews = s.totalPageviews; calculations = s.totalCalculations; clicks = s.totalClicks; impressions = s.totalImpressions; ctr = s.avgCtr; avgPos = s.avgPosition; }
setKpiValue("kpi-val-users", visitors.toLocaleString()); setKpiSub("kpi-sub-users", `จาก ${sessions.toLocaleString()} sessions`);
setKpiValue("kpi-val-views", pageviews.toLocaleString()); setKpiSub("kpi-sub-views", `เฉลี่ย ${(pageviews / (visitors || 1)).toFixed(1)} หน้า/คน`);
setKpiValue("kpi-val-calc", calculations.toLocaleString()); setKpiSub("kpi-sub-calc", `กดคัดลอกสรุป ${(state.data.summary.totalCopyClicks || 0).toLocaleString()} ครั้ง`);
setKpiValue("kpi-val-clicks", clicks.toLocaleString()); setKpiSub("kpi-sub-clicks", `สัดส่วนคลิก Google ${(clicks / (sessions || 1) * 100).toFixed(0)}%`);
setKpiValue("kpi-val-impressions", impressions.toLocaleString()); setKpiSub("kpi-sub-impressions", `CTR เฉลี่ย ${ctr}%`);
setKpiValue("kpi-val-position", avgPos.toFixed(1)); setKpiSub("kpi-sub-position", "อันดับเฉลี่ยบน Google Search"); }
function setKpiValue(id, val) { const el = document.getElementById(id); if (el) el.textContent = val; }
function setKpiSub(id, val) { const el = document.getElementById(id); if (el) el.textContent = val; }
/* ---------------------------------------------------------------- SVG Trend Chart */
function renderTrendChart(daily) { const container = document.getElementById("bi-chart-container"); if (!container) return;
if (!daily || daily.length < 2) { container.innerHTML = `
`; return; }
const w = container.clientWidth || 900; const h = 240; const padLeft = 45; const padRight = 20; const padTop = 20; const padBottom = 30;
const chartW = w - padLeft - padRight; const chartH = h - padTop - padBottom;
const maxVisitors = Math.max(...daily.map((d) => d.visitors || 0), 10); const maxClicks = Math.max(...daily.map((d) => d.gsc_clicks || 0), 10); const maxVal = Math.max(maxVisitors, maxClicks * 1.5) * 1.1;
const getX = (index) => padLeft + (index / (daily.length - 1)) * chartW; const getY = (val) => padTop + chartH - (val / maxVal) * chartH;
// Build SVG Paths let visitorsPath = `M ${getX(0)} ${getY(daily[0].visitors)}`; let visitorsArea = `M ${getX(0)} ${getY(daily[0].visitors)}`; let clicksPath = `M ${getX(0)} ${getY(daily[0].gsc_clicks)}`;
for (let i = 1; i < daily.length; i++) {
const x = getX(i);
const yV = getY(daily[i].visitors);
const yC = getY(daily[i].gsc_clicks);
visitorsPath += ` L ${x} ${yV}`;
visitorsArea += ` L ${x} ${yV}`;
clicksPath += ` L ${x} ${yC}`;
}
visitorsArea += ` L ${getX(daily.length - 1)} ${padTop + chartH} L ${getX(0)} ${padTop + chartH} Z`;
// Horizontal Grid Lines
let gridLines = "";
const gridSteps = 4;
for (let s = 0; s <= gridSteps; s++) {
const val = Math.round((maxVal / gridSteps) * s);
const y = getY(val);
gridLines += `
// X Axis Date Labels (show ~5 points)
let dateLabels = "";
const labelInterval = Math.max(1, Math.floor(daily.length / 5));
for (let i = 0; i < daily.length; i += labelInterval) {
const d = daily[i].date ? daily[i].date.slice(5) : `D${i + 1}`;
dateLabels += `
// Interactive Hover Points
let hoverCircles = "";
for (let i = 0; i < daily.length; i++) {
const d = daily[i];
const x = getX(i);
const yV = getY(d.visitors);
const yC = getY(d.gsc_clicks);
hoverCircles += `
container.innerHTML = `
`;
setupChartTooltips(container); }
function setupChartTooltips(container) { const tooltip = container.querySelector("#bi-chart-tooltip"); if (!tooltip) return;
const dots = container.querySelectorAll(".bi-hover-dot"); dots.forEach((dot) => { dot.addEventListener("mouseenter", (e) => { const date = dot.getAttribute("data-date"); const visitors = dot.getAttribute("data-visitors"); const clicks = dot.getAttribute("data-clicks"); const views = dot.getAttribute("data-views");
tooltip.innerHTML = `
`; tooltip.style.display = "block"; const rect = dot.getBoundingClientRect(); const parentRect = container.getBoundingClientRect(); tooltip.style.left = `${rect.left - parentRect.left + rect.width / 2}px`; tooltip.style.top = `${rect.top - parentRect.top}px`; });
dot.addEventListener("mouseleave", () => { tooltip.style.display = "none"; }); }); }
/* ---------------------------------------------------------------- Queries Table */
function renderQueriesTable() { const tbody = document.getElementById("bi-queries-tbody"); if (!tbody || !state.data) return;
let queries = state.data.aggregatedQueries || [];
// Filter by search text if (state.searchQuery.trim()) { const q = state.searchQuery.toLowerCase().trim(); queries = queries.filter((item) => item.query.toLowerCase().includes(q)); }
// Filter by chip if (state.chipFilter === "opportunity") { queries = queries.filter((item) => item.isOpportunity); } else if (state.chipFilter === "top10") { queries = queries.filter((item) => item.avgPosition <= 10.0); } if (queries.length === 0) { tbody.innerHTML = `
`; return; }
tbody.innerHTML = queries.slice(0, 50).map((q, idx) => { let statusBadge = ""; if (q.isOpportunity) { statusBadge = `🚀 โอกาสดันอันดับ`; } else if (q.avgPosition <= 3.0) { statusBadge = `🏆 อันดับต้น`; }
return `
`; }).join(""); }
/* ---------------------------------------------------------------- Top Pages Table */
function renderTopPagesTable() { const tbody = document.getElementById("bi-pages-tbody"); if (!tbody || !state.data) return;
const pages = state.data.aggregatedPages || []; const maxViews = Math.max(...pages.map((p) => p.pageviews), 1);
tbody.innerHTML = pages.slice(0, 15).map((p) => { const sharePercent = Math.round((p.pageviews / maxViews) * 100); return `
${p.pageviews.toLocaleString()}
`; }).join(""); }
/* ---------------------------------------------------------------- Engagement Matrix */
function renderEngagementMatrix() { const container = document.getElementById("bi-matrix-container"); if (!container || !state.data) return;
const events = state.data.aggregatedEvents || []; const calcEvents = events.filter((e) => e.event_name === "calculate_click"); const otherEvents = events.filter((e) => e.event_name !== "calculate_click");
const renderList = (items) => { if (items.length === 0) return `
`; return `
-
${items.map((it) => `
- ${it.event_label || it.event_name} ${it.count.toLocaleString()} ครั้ง
`).join("")}
`; };
container.innerHTML = `
${renderList(calcEvents)}
${renderList(otherEvents)}
`; }
/* ---------------------------------------------------------------- CSV Export */
function exportQueriesToCsv() { if (!state.data || !state.data.aggregatedQueries) { alert("ไม่มีข้อมูลสำหรับส่งออก"); return; }
const queries = state.data.aggregatedQueries; const header = ["Search Query", "Clicks", "Impressions", "CTR (%)", "Avg Position", "Status"]; const rows = queries.map((q) => [ `"${q.query.replace(/"/g, '""')}"`, q.clicks, q.impressions, q.ctr, q.avgPosition, q.isOpportunity ? "Opportunity" : "Standard" ]);
const csvContent = "\uFEFF" + [header.join(","), ...rows.map((r) => r.join(","))].join("\r\n"); const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `kanngong-gsc-queries-${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }
/* ---------------------------------------------------------------- Auth & Initialization */
function initAuth() { const authOverlay = document.getElementById("bi-auth-overlay"); const adminEmailDisplay = document.getElementById("bi-admin-email-display"); const storedAuth = localStorage.getItem("kgk_admin_auth");
if (storedAuth) { try { const user = JSON.parse(storedAuth); if (ADMIN_EMAIL_WHITELIST.includes(user.email)) { state.isAuthenticated = true; state.currentUser = user; } } catch { localStorage.removeItem("kgk_admin_auth"); } }
if (state.isAuthenticated) { if (authOverlay) authOverlay.classList.remove("active"); if (adminEmailDisplay) adminEmailDisplay.textContent = state.currentUser.email; } else { if (authOverlay) authOverlay.classList.add("active"); }
// Setup Preview / Login Buttons const btnGoogle = document.getElementById("bi-btn-google-login"); if (btnGoogle) { btnGoogle.addEventListener("click", () => { // Authenticate as whitelist admin const adminUser = { email: "waranyu.trkm@gmail.com", name: "Waranyu T.", role: "admin" }; localStorage.setItem("kgk_admin_auth", JSON.stringify(adminUser)); state.isAuthenticated = true; state.currentUser = adminUser; if (authOverlay) authOverlay.classList.remove("active"); if (adminEmailDisplay) adminEmailDisplay.textContent = adminUser.email; }); }
const btnPreview = document.getElementById("bi-btn-local-preview"); if (btnPreview) { btnPreview.addEventListener("click", () => { const previewUser = { email: "preview@kanngongkanngoen.com", name: "Dev Preview", role: "admin" }; state.isAuthenticated = true; state.currentUser = previewUser; if (authOverlay) authOverlay.classList.remove("active"); if (adminEmailDisplay) adminEmailDisplay.textContent = "🔒 Local Preview Mode"; }); }
const btnSignOut = document.getElementById("bi-btn-signout"); if (btnSignOut) { btnSignOut.addEventListener("click", () => { localStorage.removeItem("kgk_admin_auth"); state.isAuthenticated = false; state.currentUser = null; if (authOverlay) authOverlay.classList.add("active"); }); } }
function initEventHandlers() { // Date Range Buttons const rangeBtns = document.querySelectorAll(".bi-range-btn"); rangeBtns.forEach((btn) => { btn.addEventListener("click", () => { rangeBtns.forEach((b) => b.classList.remove("active")); btn.classList.add("active"); state.activeDays = parseInt(btn.getAttribute("data-days"), 10); updateDashboard(); }); });
// Search Input Filter const searchInput = document.getElementById("bi-search-queries"); if (searchInput) { let debounceTimer; searchInput.addEventListener("input", (e) => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { state.searchQuery = e.target.value; renderQueriesTable(); }, 150); }); }
// Filter Chips const chips = document.querySelectorAll(".bi-chip"); chips.forEach((chip) => { chip.addEventListener("click", () => { chips.forEach((c) => c.classList.remove("active")); chip.classList.add("active"); state.chipFilter = chip.getAttribute("data-filter"); renderQueriesTable(); }); });
// CSV Export Button const btnExport = document.getElementById("bi-btn-export-csv"); if (btnExport) { btnExport.addEventListener("click", exportQueriesToCsv); }
// Refresh Sync Button const btnRefresh = document.getElementById("bi-btn-refresh-sync"); if (btnRefresh) { btnRefresh.addEventListener("click", async () => { btnRefresh.textContent = "⏳ กำลังโหลด..."; await loadBiData(); btnRefresh.textContent = "🔄 อัปเดตข้อมูล"; }); }
// Window Resize (Redraw SVG Chart responsively) window.addEventListener("resize", () => { if (state.data) { const rawDaily = state.data.dailyMetrics || []; const sliceDays = Math.min(state.activeDays, rawDaily.length || state.activeDays); renderTrendChart(rawDaily.slice(-sliceDays)); } }); }
/* ---------------------------------------------------------------- Bootstrap */
document.addEventListener("DOMContentLoaded", () => { initAuth(); initEventHandlers(); loadBiData(); });
})();