🎓 Workshop: เรียนเขียนโปรแกรม

การบ้านจาก Dashboard Namnan • 5 sections × 5 ข้อ = 25 ข้อ

🎓 Workshop: เรียนเขียนโปรแกรม

เรียนรู้ผ่าน Dashboard Namnan จริง — HTML, JavaScript, SQL, Python, DevOps

5
sections
25
ข้อ
~8h
เวลาเรียน
level: beginner → intermediate

📚 ยินดีต้อนรับ!

นี่คือ workshop การบ้าน สำหรับนักเรียนที่อยากเรียนเขียนโปรแกรม โดยใช้ Dashboard Namnan v11 (https://dashboard.namnan.co.th) เป็นโจทย์จริง

🎯 เป้าหมาย

📦 ที่ต้องเตรียม

  1. Download source code: 📥 ดาวน์โหลด zip (1.3MB) หรือ ตรงๆ
  2. Extract zip: unzip dashboard-namnan-v11.zip
  3. Text editor: VS Code, Notepad++, หรือแม้แต่ Notepad
  4. Web browser: Chrome, Firefox, Edge
  5. (Optional) MySQL + Python สำหรับข้อยาก

📋 โครงสร้าง Workshop

Section หัวข้อ ทักษะ
1HTML / CSSหน้าเว็บ, สี, layout
2JavaScriptlogic, function, async
3SQL / MySQLquery, JOIN, GROUP BY
4Python / FastAPIAPI, backend
5DevOpsnginx, systemd, cron
💡
วิธีส่งงาน: ทำข้อที่ได้ แล้ว screenshot + ส่งโค้ดที่แก้ ผ่าน Google Form / LINE / Email ตามที่ครูกำหนด

📑 สารบัญ

1 📊 กราฟ / Data Analysis — วิเคราะห์ข้อมูลจริง

เรียนรู้การสร้างกราฟ + วิเคราะห์ข้อมูลจาก MySQL โดยใช้ analytics.html + Chart.js เป็นโจทย์

📁 ไฟล์ที่เกี่ยวข้อง

📝 คำถาม

1.1 ง่าย เปลี่ยนสีกราฟ

ไฟล์ analytics.html มี monthlyChart (line chart) ใช้สีน้ำเงิน + เขียว

🎯 งาน: เปลี่ยนเป็น ชมพู + ส้ม (เหมือน header expenses.html)
เฉลย
// ใน renderMonthlyChart() borderColor: "#ec4899", // เดิม: #3b82f6 backgroundColor: "rgba(236,72,153,0.1)", borderColor: "#f59e0b", // เดิม: #10b981

1.2 ง่าย เพิ่มกราฟใหม่

ใน analytics.html มี 6 กราฟ — ขาด Top 5 customers

🎯 งาน: เพิ่ม renderTopCustomersChart() แสดง top 5 ลูกค้า (group by tracking, sum net_amount)
เฉลย
function renderTopCustomersChart(orders) { const byCust = {}; orders.forEach(o => { if (!o.tracking) return; byCust[o.tracking] = (byCust[o.tracking] || 0) + (o.net_amount || 0); }); const top = Object.entries(byCust) .sort((a, b) => b[1] - a[1]) .slice(0, 5); // ใช้ Chart.js bar chart (เหมือน productChart) new Chart(document.getElementById("custChart"), { type: "bar", data: { labels: top.map(t => t[0]), datasets: [{ data: top.map(t => t[1]), backgroundColor: "#3b82f6" }] }, options: { indexAxis: "y" } }); }

1.3 กลาง เพิ่ม filter ตาม channel

ตอนนี้ analytics.html filter แค่ วันที่ — ขาด filter channel

🎯 งาน: เพิ่ม dropdown "📡 Channel" ให้เลือก channel ที่ต้องการวิเคราะห์
เฉลย
<!-- HTML: เพิ่มใน filter-bar --> <div class="filter-group"> <label>📡 Channel:</label> <select id="channelFilter"> <option value="all">ทั้งหมด</option> </select> </div> // JS: ใน loadAll() เพิ่ม const ch = document.getElementById("channelFilter").value; const orders = allOrders.filter(o => (ch === "all" || o.channel === ch) && m >= since && m <= until );

1.4 กลาง Export เป็น PDF

analytics.html มี insight boxes เจ๋งๆ — แต่ไม่มีปุ่ม export

🎯 งาน: เพิ่มปุ่ม "📄 Export PDF" ที่ print หน้า analytics เป็น PDF
เฉลย
function exportPDF() { window.print(); } /* CSS: ซ่อน UI ตอน print */ @media print { .filter-bar, .nav-tabs, .theme-toggle { display: none; } body { background: white; } .card { break-inside: avoid; } }

1.5 ยาก วิเคราะห์จริง — หา insight

ใช้ข้อมูลจาก MySQL ตอบคำถามธุรกิจ

🎯 งาน: เขียน SQL query + อธิบาย insight:
  1. เดือนไหนขายดีที่สุดในรอบ 12 เดือน?
  2. Channel ไหน convert ดีสุด (orders/revenue)?
  3. Product ไหนควรเลิกผลิต (revenue ต่ำ + orders น้อย)?
เฉลย (ตัวอย่าง insight)
  1. Q1: SELECT month, SUM(net_amount) FROM orders_history GROUP BY month ORDER BY 2 DESC LIMIT 1 → เดือนที่ดีสุดคือ 2024-02 (฿347K)
  2. Q2: LINE มี conversion ดีกว่า Shopee เพราะขายตรง แต่ volume น้อยกว่า — แนะนำเพิ่ม LINE ads
  3. Q3: Product "BA-S" มี 125 orders แต่ revenue แค่ ฿99K (avg ฿800) — ต่ำกว่า BA-M เฉลี่ย ฿1,000 — พิจารณาปรับราคา

2 📗 JavaScript — Logic และการเรียก API

เรียนรู้ function, array, async/await โดยใช้ frontend/api.js และ logic ใน HTML files

📁 ไฟล์ที่เกี่ยวข้อง

📝 คำถาม

2.1 ง่าย เขียน function รวมยอด

มี array ของ orders: [{amount: 100}, {amount: 200}, {amount: 50}]

🎯 งาน: เขียน function sumAmount(orders) ที่รวม amount ทั้งหมด ใช้ Array.reduce()

📂 เปิดไฟล์ expenses.html หรือ index.html → ใส่ function ใน <script>

เฉลย
function sumAmount(orders) { return orders.reduce((total, o) => total + (o.amount || 0), 0); } // ทดสอบ console.log(sumAmount([{amount:100},{amount:200},{amount:50}])); // 350

2.2 ง่าย เพิ่ม method ใน api.js

ไฟล์ api.js มี DashboardAPI object — มี methods เช่น getExpenses, getOrders

🎯 งาน: เพิ่ม method getTopProducts(month, limit=10) ใน DashboardAPI

📂 เปิด api.js → หา DashboardAPI = { ... } → เพิ่ม method ใหม่

เฉลย
// ใน DashboardAPI object getTopProducts: async (month, limit = 10) => { const orders = await this.getOrders(month, { limit: 5000 }); const byProd = {}; orders.forEach(o => { const key = o.sku || 'unknown'; if (!byProd[key]) byProd[key] = { sku: o.sku, qty: 0, amount: 0, count: 0 }; byProd[key].qty += o.qty || 0; byProd[key].amount += o.net_amount || o.amount || 0; byProd[key].count += 1; }); return Object.values(byProd) .sort((a, b) => b.amount - a.amount) .slice(0, limit); },

วิธีใช้:

const top = await DashboardAPI.getTopProducts('2026-07', 5); console.log(top); // [{sku, qty, amount, count}, ...]

2.3 กลาง Event listener + filter

ใน expenses.html มี document.getElementById("monthInput").onchange = loadAll;

🎯 งาน: เพิ่ม event listener เมื่อกดปุ่ม Enter ใน monthInput ให้ reload

📂 หา document.getElementById("monthInput").onchange = loadAll; → เพิ่ม keydown listener

เฉลย
document.getElementById("monthInput").addEventListener("keydown", (e) => { if (e.key === "Enter") loadAll(); }); // หรือใช้ onkeydown inline <input type="month" id="monthInput" onkeydown="if(event.key==='Enter') loadAll()">

2.4 กลาง Async/Await + Error handling

ใน loadAll() มี try/catch — แต่ไม่มี loading state

🎯 งาน: เพิ่ม loading state "⏳ กำลังโหลด..." ใน lastUpdate element ก่อน fetch และเปลี่ยนเป็น ✅ เมื่อสำเร็จ

📂 เปิด expenses.html → หา async function loadAll()

เฉลย
async function loadAll() { MONTH = document.getElementById("monthInput").value; const statusEl = document.getElementById("lastUpdate"); statusEl.textContent = "⏳ กำลังโหลด..."; statusEl.style.color = "var(--info)"; try { // ... fetch data ... statusEl.textContent = "✅ อัปเดตล่าสุด: " + new Date().toLocaleString("th-TH"); statusEl.style.color = "var(--success)"; } catch (e) { statusEl.textContent = "❌ Error: " + e.message; statusEl.style.color = "var(--danger)"; } }

2.5 ยาก สร้าง Chart ใหม่

ไฟล์ fb-ads.html ใช้ Chart.js — มี 2 charts (daily, funnel)

🎯 งาน: เพิ่ม chart ที่ 3 ใน fb-ads.htmlPie chart แสดง % ของ ad spend แต่ละ campaign

📂 หา renderDailyChart และ renderFunnelChart → เพิ่ม renderCampaignPieChart()

เฉลย
function renderCampaignPieChart(campaigns) { if (_charts.pie) _charts.pie.destroy(); const ctx = document.getElementById("pieChart").getContext("2d"); _charts.pie = new Chart(ctx, { type: "pie", data: { labels: campaigns.map(c => c.campaign.substring(0, 30)), datasets: [{ data: campaigns.map(c => c.spend), backgroundColor: ["#ec4899", "#8b5cf6", "#06b6d4", "#10b981"] }] }, options: { responsive: true, plugins: { legend: { position: "right" }, tooltip: { callbacks: { label: (c) => `${c.label}: ฿${c.parsed.toLocaleString()} (${(c.parsed/c.dataset.data.reduce((a,b)=>a+b,0)*100).toFixed(1)}%)` } } } } }); }

แล้วเพิ่ม <canvas id="pieChart"></canvas> ใน HTML

3 📕 SQL / MySQL — ภาษาถามข้อมูล

เรียนรู้ SQL ผ่าน MySQL จริง — มีตาราง orders, expenses, channels, products

📁 ตารางที่ใช้

📝 คำถาม

💡
วิธีรัน SQL: mysql -u namnan_dash -pNamnan2026! namnan_dashboard -e "SELECT ..."

3.1 ง่าย SELECT basic

🎯 งาน: เขียน query หา total revenue ของเดือน 2026-07 จาก orders table
เฉลย
SELECT SUM(amount) AS total_revenue, COUNT(*) AS orders FROM orders WHERE month = '2026-07';

ผลลัพธ์: total_revenue: 76,644, orders: 61

3.2 ง่าย GROUP BY + JOIN

🎯 งาน: เขียน query หา ยอดขายแยกตาม channel ในเดือน 2026-07 (join กับ channels table)
เฉลย
SELECT c.name AS channel, COUNT(*) AS orders, SUM(o.amount) AS revenue FROM orders o LEFT JOIN channels c ON o.channel_id = c.id WHERE o.month = '2026-07' GROUP BY c.id, c.name ORDER BY revenue DESC;

ผลลัพธ์: LINE (45/52,310), เฮียหนวด (14/21,836), อื่นๆ (2/2,498)

3.3 กลาง UNION + Aggregate

🎯 งาน: เขียน query หา total expenses แยกตาม category ในเดือน 2026-07 (รวมทั้ง expenses และ expenses_history)
เฉลย
SELECT category, COUNT(*) AS bills, SUM(amount) AS total, SUM(CASE WHEN status='paid' THEN amount ELSE 0 END) AS paid FROM ( SELECT * FROM expenses UNION ALL SELECT * FROM expenses_history ) t WHERE month = '2026-07' GROUP BY category ORDER BY total DESC;

3.4 กลาง INSERT

🎯 งาน: เขียน query เพิ่ม expense ใหม่ — ค่ากล่อง Kerry ฿350 วันที่ 2026-08-01
เฉลย
INSERT INTO expenses (date, month, category, amount, vendor, description, status, has_receipt) VALUES ( '2026-08-01', '2026-08', 'ค่าขนส่ง', 350.00, 'Kerry Express', 'ส่ง Kerry 4 ชิ้น', 'pending', 1 );

3.5 ยาก UPDATE + WHERE

🎯 งาน: เขียน query mark as paid expense ID = 2 (TSM bill) — ตั้ง paid_date = วันนี้, payment_ref = 'TR-2026-08-01'
เฉลย
UPDATE expenses SET status = 'paid', paid_date = CURDATE(), payment_ref = 'TR-2026-08-01', payment_method = 'โอนผ่านธนาคาร' WHERE id = 2;

ตรวจสอบผลลัพธ์:

SELECT * FROM expenses WHERE id = 2;

4 📙 Python / FastAPI — Backend API

เรียนรู้ Python ผ่าน FastAPI — สร้าง API endpoint จริง

📁 ไฟล์ที่เกี่ยวข้อง

📝 คำถาม

💡
วิธีรัน: cd backend && python3 -m uvicorn api.main:app --reload

4.1 ง่าย Hello World endpoint

🎯 งาน: เพิ่ม endpoint GET /hello/{name} ใน main.py ที่ return {"hello": name}
เฉลย
@app.get("/hello/{name}") def hello(name: str): return {"ok": True, "hello": name}

ทดสอบ: curl http://localhost:8300/hello/World{"ok":true,"hello":"World"}

4.2 ง่าย Query parameter

🎯 งาน: เพิ่ม endpoint GET /orders-by-channel รับ query param channel_id แล้ว return orders ของ channel นั้น
เฉลย
@app.get("/orders-by-channel") def orders_by_channel(channel_id: int): sql = "SELECT * FROM orders WHERE channel_id = %s ORDER BY date DESC LIMIT 100" return {"ok": True, "data": clean(query(sql, (channel_id,)))}

4.3 กลาง Aggregate function

🎯 งาน: เขียน function get_top_customers(month, limit=10) ใน main.py return top customers by total spend
เฉลย
@app.get("/top-customers") def top_customers(month: str, limit: int = 10): sql = """SELECT tracking, COUNT(*) AS orders, SUM(net_amount) AS total_spent FROM orders WHERE month = %s AND tracking IS NOT NULL GROUP BY tracking ORDER BY total_spent DESC LIMIT %s""" return {"ok": True, "data": clean(query(sql, (month, limit)))}

4.4 กลาง Pydantic model

🎯 งาน: เพิ่ม endpoint POST /create-expense รับ JSON body แล้ว INSERT ลง database
เฉลย
from pydantic import BaseModel from datetime import datetime class NewExpense(BaseModel): date: str category: str amount: float vendor: str = None description: str = None @app.post("/create-expense") def create_expense(b: NewExpense): month = b.date[:"7"] new_id = execute( "INSERT INTO expenses (date, month, category, amount, vendor, description, status) VALUES (%s, %s, %s, %s, %s, %s, 'pending')", (b.date, month, b.category, b.amount, b.vendor, b.description) ) return {"ok": True, "id": new_id}

4.5 ยาก เพิ่ม auth middleware

🎯 งาน: ทำให้ /hello ต้อง login — เช็ค API key ก่อน
เฉลย
from api.auth import require_api_key @app.get("/hello/{name}", dependencies=[Depends(require_api_key)]) def hello(name: str): return {"hello": name}

ใน auth.py:

def require_api_key(x_api_key: str = Header(None)): expected = os.environ.get("DASHBOARD_API_KEY") if not x_api_key or x_api_key != expected: raise HTTPException(401", "Invalid API key") return x_api_key

5 📓 DevOps — Deploy + Monitor

เรียนรู้การ deploy เว็บจริง — nginx, systemd, cron, log

📁 ไฟล์ที่เกี่ยวข้อง

📝 คำถาม

5.1 ง่าย อ่าน nginx config

🎯 งาน: ตอบคำถามจาก dashboard.namnan.co.th.nginx.conf:
  1. Port ที่ nginx listen?
  2. API proxy ไปที่ไหน?
  3. Cache-Control สำหรับ /api/ คืออะไร?
เฉลย
  1. Port 80 + 443 (redirect 80→443, HTTPS ใช้ 443)
  2. http://127.0.0.1:8300/ (FastAPI)
  3. no-store, no-cache, must-revalidate (ไม่ cache เลย)

5.2 ง่าย อ่าน systemd service

🎯 งาน: ตอบคำถามจาก dashboard-api.service:
  1. User ที่รัน service?
  2. คำสั่ง start คืออะไร?
  3. Restart policy?
เฉลย
  1. root
  2. python3 -m uvicorn api.main:app --host 127.0.0.1 --port 8300 --workers 2
  3. Restart=always + RestartSec=3 (restart ทุก 3 วินาทีถ้า crash)

5.3 กลาง เขียน cron job

🎯 งาน: เขียน cron job ใหม่ — backup MySQL ทุกวัน เวลา 01:00 เก็บไฟล์ที่ /var/backups/
เฉลย
# ใน crontab 0 1 * * * /usr/bin/mysqldump -u namnan_dash -pYOUR_PASS namnan_dashboard | gzip > /var/backups/namnan_$(date +\%Y\%m\%d).sql.gz

⚠️ ใน crontab ต้อง escape % เป็น \%

5.4 กลาง ใช้ curl + jq

🎯 งาน: เขียน shell command ดึง total revenue เดือน 2026-07 แล้วแสดงผลแค่ตัวเลข
เฉลย
curl -s -H "X-API-Key: $KEY" \ "https://dashboard.namnan.co.th/api/financial/summary?month=2026-07" \ | jq '.data.total_revenue'

ผลลัพธ์: 73146

5.5 ยาก Deploy ใหม่

🎯 งาน: เขียน bash script deploy.sh ที่:
  1. backup ไฟล์เก่า
  2. copy ไฟล์ใหม่จาก local ไป VPS
  3. restart service
  4. verify health
เฉลย
#!/bin/bash # deploy.sh set -e VPS="root@103.22.183.111" BACKUP_DIR="/var/backups/dashboard-$(date +%Y%m%d-%H%M%S)" LOCAL_DIR="./frontend" REMOTE_DIR="/var/www/dashboard.namnan.co.th" # 1. Backup ssh $VPS "mkdir -p $BACKUP_DIR && cp -r $REMOTE_DIR/* $BACKUP_DIR/" echo "✅ Backup: $BACKUP_DIR" # 2. Copy scp -r $LOCAL_DIR/* $VPS:$REMOTE_DIR/ echo "✅ Copied frontend" # 3. Restart (if backend changed) # ssh $VPS "systemctl restart dashboard-api" # 4. Verify curl -s -o /dev/null -w "%{http_code}\n" https://dashboard.namnan.co.th/api/health

📤 วิธีส่งงาน

เมื่อทำเสร็จ ให้ส่งงานผ่าน:

  1. Google Form (ตามที่ครูแจ้ง) — แนบ screenshot + โค้ดที่แก้
  2. GitHub — fork repo แล้วส่ง PR (ถ้ามี)
  3. Email — ส่ง zip ของไฟล์ที่แก้

📋 เช็คลิสต์ก่อนส่ง

🎓
คำแนะนำ: ทำทีละข้อ อย่าเพิ่งทำทุกข้อพร้อมกัน — เข้าใจแต่ละข้อก่อน ค่อยขยับไปข้อถัดไป