diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx
index b902569..59eb4bc 100644
--- a/apps/admin/src/App.tsx
+++ b/apps/admin/src/App.tsx
@@ -44,6 +44,7 @@ import GlobalAnalyticsPage from './pages/GlobalAnalyticsPage'
import MediaLibraryPage from './pages/MediaLibraryPage'
import ContactMessagesPage from './pages/ContactMessagesPage'
import ProductAttributesPage from './pages/ProductAttributesPage'
+import TiktokPriceSimulatorPage from './pages/TiktokPriceSimulatorPage'
function App() {
return (
@@ -88,6 +89,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/admin/src/components/Sidebar.tsx b/apps/admin/src/components/Sidebar.tsx
index fc999f9..94b090b 100644
--- a/apps/admin/src/components/Sidebar.tsx
+++ b/apps/admin/src/components/Sidebar.tsx
@@ -27,6 +27,7 @@ import {
Wrench,
ChevronDown,
Sparkles,
+ Calculator,
} from 'lucide-react'
const MAIN_MENU = [
@@ -51,6 +52,7 @@ const MAIN_MENU = [
{ icon: ShoppingCart, label: 'TikTok Orders', to: '/tiktok-order' },
{ icon: BarChart3, label: 'TikTok Analytics', to: '/tiktok-analytics' },
{ icon: Layers, label: 'TikTok ทุกร้าน', to: '/tiktok-overview' },
+ { icon: Calculator, label: 'TikTok Price Simulator', to: '/tiktok-price-simulator' },
{ icon: ShoppingCart, label: 'Lazada Orders', to: '/lazada-order' },
{ icon: BarChart3, label: 'Lazada Analytics', to: '/lazada-analytics' },
{ icon: Layers, label: 'Lazada ทุกร้าน', to: '/lazada-overview' },
diff --git a/apps/admin/src/pages/TiktokPriceSimulatorPage.tsx b/apps/admin/src/pages/TiktokPriceSimulatorPage.tsx
new file mode 100644
index 0000000..47e43fc
--- /dev/null
+++ b/apps/admin/src/pages/TiktokPriceSimulatorPage.tsx
@@ -0,0 +1,1387 @@
+import { useState, useMemo, useEffect } from 'react'
+import {
+ Calculator,
+ TrendingUp,
+ BarChart3,
+ AlertTriangle,
+ CheckCircle2,
+ XCircle,
+ Info,
+ Plus,
+ Trash2,
+ ChevronDown,
+ ChevronUp,
+ Zap,
+ Star,
+ Gift,
+ RefreshCw,
+} from 'lucide-react'
+
+// ─── Utility ───────────────────────────────────────────────────────────────────
+
+function fmt(val: number) {
+ return new Intl.NumberFormat('th-TH', {
+ style: 'currency',
+ currency: 'THB',
+ maximumFractionDigits: 2,
+ }).format(val)
+}
+
+function pct(val: number) {
+ return `${val.toFixed(1)}%`
+}
+
+function clamp(v: number, min: number, max: number) {
+ return Math.max(min, Math.min(max, v))
+}
+
+// ─── Types ─────────────────────────────────────────────────────────────────────
+
+interface ProductRow {
+ id: number
+ name: string
+ cost: number
+ listPrice: number
+}
+
+type ProductGroup = 'hero' | 'profit' | 'bundle' | 'unclassified'
+
+// ─── Sub-components ────────────────────────────────────────────────────────────
+
+function SectionCard({
+ icon,
+ title,
+ subtitle,
+ children,
+ accentColor = 'cyan',
+ defaultOpen = true,
+}: {
+ icon: React.ReactNode
+ title: string
+ subtitle?: string
+ children: React.ReactNode
+ accentColor?: 'cyan' | 'violet' | 'amber' | 'rose'
+ defaultOpen?: boolean
+}) {
+ const [open, setOpen] = useState(defaultOpen)
+
+ const accent = {
+ cyan: 'text-cyan-glow border-cyan-glow/20',
+ violet: 'text-violet-400 border-violet-400/20',
+ amber: 'text-amber-400 border-amber-400/20',
+ rose: 'text-rose-400 border-rose-400/20',
+ }[accentColor]
+
+ return (
+
+
+
+ {open &&
{children}
}
+
+ )
+}
+
+function InputRow({
+ label,
+ hint,
+ children,
+}: {
+ label: string
+ hint?: string
+ children: React.ReactNode
+}) {
+ return (
+
+
+
{label}
+ {hint &&
{hint}
}
+
+
{children}
+
+ )
+}
+
+function NumInput({
+ value,
+ onChange,
+ min = 0,
+ max,
+ step = 1,
+ prefix,
+ suffix,
+}: {
+ value: number
+ onChange: (v: number) => void
+ min?: number
+ max?: number
+ step?: number
+ prefix?: string
+ suffix?: string
+}) {
+ return (
+
+ {prefix && {prefix}}
+ onChange(parseFloat(e.target.value) || 0)}
+ />
+ {suffix && {suffix}}
+
+ )
+}
+
+function ResultBadge({
+ label,
+ value,
+ sub,
+ formula,
+ highlight,
+ danger,
+ warn,
+}: {
+ label: string
+ value: string
+ sub?: string
+ formula?: React.ReactNode
+ highlight?: boolean
+ danger?: boolean
+ warn?: boolean
+}) {
+ const bg = danger
+ ? 'bg-rose-500/10 border-rose-500/30'
+ : warn
+ ? 'bg-amber-500/10 border-amber-500/30'
+ : highlight
+ ? 'bg-cyan-glow/10 border-cyan-glow/30'
+ : 'bg-night-800/60 border-white/10'
+
+ const textColor = danger
+ ? 'text-rose-400'
+ : warn
+ ? 'text-amber-400'
+ : highlight
+ ? 'text-cyan-glow'
+ : 'text-white'
+
+ return (
+
+
{label}
+
{value}
+ {sub &&
{sub}
}
+ {formula && (
+
+ )}
+
+ )
+}
+
+// ─── Section 1: Price Buffer Calculator ────────────────────────────────────────
+
+function PriceBufferCalculator() {
+ const [cost, setCost] = useState(100)
+ const [commissionPct, setCommissionPct] = useState(8)
+ const [shipping, setShipping] = useState(30)
+ const [sellerPaysShipping, setSellerPaysShipping] = useState(true)
+ const [targetProfitType, setTargetProfitType] = useState<'pct' | 'amount'>('pct')
+ const [targetProfitPct, setTargetProfitPct] = useState(25)
+ const [targetProfitAmount, setTargetProfitAmount] = useState(50)
+ const [campaignDiscountPct, setCampaignDiscountPct] = useState(20)
+ const [platformVoucherPct, setPlatformVoucherPct] = useState(5)
+
+ const results = useMemo(() => {
+ const shippingCost = sellerPaysShipping ? shipping : 0
+ // Break-even: ราคาที่ต่ำสุดที่ไม่ขาดทุน
+ // breakEven = (cost + shippingCost) / (1 - commissionPct/100)
+ const breakEven = (cost + shippingCost) / (1 - commissionPct / 100)
+
+ // ราคาขายปกติที่ได้กำไรตาม target
+ let sellingPrice = 0
+ if (targetProfitType === 'pct') {
+ sellingPrice = breakEven / (1 - targetProfitPct / 100)
+ } else {
+ sellingPrice = (cost + shippingCost + targetProfitAmount) / (1 - commissionPct / 100)
+ }
+
+ // List Price เผื่อ Campaign discount เพิ่ม
+ // เราต้องการให้เมื่อลด campaignDiscountPct จาก List แล้ว ยังอยู่เหนือ breakEven
+ const listPrice = breakEven / (1 - (campaignDiscountPct + platformVoucherPct) / 100) * 1.05
+
+ // กำไรต่อชิ้นแต่ละระดับ
+ const profitNormal = sellingPrice * (1 - commissionPct / 100) - cost - shippingCost
+ const campaignPrice = listPrice * (1 - campaignDiscountPct / 100)
+ const profitCampaign = campaignPrice * (1 - commissionPct / 100) - cost - shippingCost
+ const profitMin = breakEven * (1 - commissionPct / 100) - cost - shippingCost
+
+ const marginNormal = sellingPrice > 0 ? (profitNormal / sellingPrice) * 100 : 0
+ const marginCampaign = campaignPrice > 0 ? (profitCampaign / campaignPrice) * 100 : 0
+
+ return {
+ breakEven,
+ sellingPrice,
+ listPrice,
+ campaignPrice,
+ profitNormal,
+ profitCampaign,
+ profitMin,
+ marginNormal,
+ marginCampaign,
+ }
+ }, [cost, commissionPct, shipping, sellerPaysShipping, targetProfitType, targetProfitPct, targetProfitAmount, campaignDiscountPct, platformVoucherPct])
+
+ return (
+
+ {/* Inputs */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {targetProfitType === 'pct' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Results */}
+
+
ผลลัพธ์การคำนวณ
+
+
+ [จุดเท่าทุน] ÷ (1 - แคมเปญ% - คูปอง TikTok%) × เผื่อ 5%
+ = {fmt(results.breakEven)} ÷ (1 - {campaignDiscountPct}% - {platformVoucherPct}%) × 1.05
+
+ )}
+ highlight
+ />
+
+ {targetProfitType === 'pct' ? (
+ <>
+ [จุดเท่าทุน] ÷ (1 - กำไรที่ต้องการ%)
+ = {fmt(results.breakEven)} ÷ (1 - {targetProfitPct}%)
+ >
+ ) : (
+ <>
+ (ต้นทุน + ค่าส่ง + กำไรบาท) ÷ (1 - ค่าคอม%)
+ = ({fmt(cost)} + {fmt(sellerPaysShipping ? shipping : 0)} + {fmt(targetProfitAmount)}) ÷ (1 - {commissionPct}%)
+ >
+ )}
+
+ )}
+ />
+
+ [ราคาตั้ง] - ส่วนลดแคมเปญ%
+ = {fmt(results.listPrice)} - {campaignDiscountPct}%
+
+ )}
+ warn={results.marginCampaign < 5}
+ danger={results.profitCampaign < 0}
+ />
+
+ (ต้นทุน + ค่าส่ง) ÷ (1 - ค่าคอม%)
+ = ({fmt(cost)} + {fmt(sellerPaysShipping ? shipping : 0)}) ÷ (1 - {commissionPct}%)
+
+ )}
+ danger
+ />
+
+ ราคาขาย - ค่าคอม - ต้นทุน - ค่าส่ง
+ = {fmt(results.sellingPrice)} - {fmt(results.sellingPrice * commissionPct / 100)} - {fmt(cost)} - {fmt(sellerPaysShipping ? shipping : 0)}
+
+ )}
+ highlight={results.profitNormal > 0}
+ danger={results.profitNormal <= 0}
+ />
+
+ ราคาแคมเปญ - ค่าคอม - ต้นทุน - ค่าส่ง
+ = {fmt(results.campaignPrice)} - {fmt(results.campaignPrice * commissionPct / 100)} - {fmt(cost)} - {fmt(sellerPaysShipping ? shipping : 0)}
+
+ )}
+ highlight={results.profitCampaign > 0 && results.marginCampaign >= 5}
+ warn={results.profitCampaign > 0 && results.marginCampaign < 5}
+ danger={results.profitCampaign <= 0}
+ />
+
+
+
+ {/* Formula hint */}
+
+
+ สูตรที่ใช้คำนวณ
+
+
Break-even = (ต้นทุน + ค่าส่ง) ÷ (1 − ค่าคอม%)
+
ราคาขาย = Break-even ÷ (1 − กำไร%)
+
List Price = Break-even ÷ (1 − Campaign% − Platform%) × 1.05 (safety buffer 5%)
+
+
+ )
+}
+
+// ─── Section 2: Volume vs Margin Simulator ─────────────────────────────────────
+
+function VolumeMarginSimulator() {
+ const [normalQty, setNormalQty] = useState(10)
+ const [normalProfit, setNormalProfit] = useState(50)
+ const [campaignQty, setCampaignQty] = useState(100)
+ const [campaignProfit, setCampaignProfit] = useState(25)
+
+ const normalTotal = normalQty * normalProfit
+ const campaignTotal = campaignQty * campaignProfit
+ const diff = campaignTotal - normalTotal
+ const multiplier = normalTotal > 0 ? campaignTotal / normalTotal : 0
+
+ const shouldJoin = campaignTotal > normalTotal && campaignProfit > 0
+
+ return (
+
+
+ {/* Normal day */}
+
+
+
+ วันขายปกติ
+
+
+
+
+
+
+
+
+
กำไรรวม
+
{fmt(normalTotal)}
+
+
+
+ {/* Campaign day */}
+
+
+
+ วัน Campaign / Mega Sale
+
+
+
+
+
+
+
+
0 ? 'bg-cyan-glow/10 border border-cyan-glow/30' : 'bg-rose-500/10 border border-rose-500/30'}`}>
+
กำไรรวม
+
0 ? 'text-cyan-glow' : 'text-rose-400'}`}>{fmt(campaignTotal)}
+
+
+
+
+ {/* Verdict */}
+
+ {shouldJoin ? (
+
+ ) : (
+
+ )}
+
+
+ {shouldJoin ? '✅ ควรเข้าร่วม Campaign' : '❌ ไม่แนะนำให้เข้า Campaign'}
+
+
+ {shouldJoin
+ ? `กำไรรวมเพิ่มขึ้น ${fmt(diff)} (${multiplier.toFixed(1)}× เทียบวันปกติ) — คุ้มค่าที่จะยอมลดกำไรต่อชิ้น`
+ : campaignProfit <= 0
+ ? 'กำไรต่อชิ้นติดลบ → ขายดีจนเข้าเนื้อ ห้ามเข้าเด็ดขาด!'
+ : `กำไรรวม Campaign (${fmt(campaignTotal)}) ต่ำกว่าวันปกติ (${fmt(normalTotal)}) — ไม่คุ้มค่า`}
+
+
+
+
เพิ่ม/ลด
+
= 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {diff >= 0 ? '+' : ''}{fmt(diff)}
+
+
+
+
+ )
+}
+
+// ─── Section 3: Product Strategy Classifier ────────────────────────────────────
+
+function classifyProduct(cost: number, listPrice: number): ProductGroup {
+ if (listPrice <= 0 || cost <= 0) return 'unclassified'
+ const margin = ((listPrice - cost) / listPrice) * 100
+ if (margin < 15) return 'hero'
+ if (margin >= 30) return 'profit'
+ return 'bundle'
+}
+
+const GROUP_CONFIG: Record<
+ ProductGroup,
+ { label: string; desc: string; icon: React.ReactNode; color: string; bg: string; border: string }
+> = {
+ hero: {
+ label: '🚀 Hero (Traffic Driver)',
+ desc: 'ดึงคนเข้าร้าน — margin บาง แต่ยอดขายมหาศาล',
+ icon: ,
+ color: 'text-amber-400',
+ bg: 'bg-amber-500/10',
+ border: 'border-amber-500/30',
+ },
+ profit: {
+ label: '💎 Profit Maker',
+ desc: 'แหล่งกำไรหลัก — margin หนา ลดน้อย',
+ icon: ,
+ color: 'text-cyan-glow',
+ bg: 'bg-cyan-glow/10',
+ border: 'border-cyan-glow/30',
+ },
+ bundle: {
+ label: '🎁 Bundle / Add-on',
+ desc: 'เพิ่ม AOV — จัดชุดกับสินค้าอื่น',
+ icon: ,
+ color: 'text-violet-400',
+ bg: 'bg-violet-500/10',
+ border: 'border-violet-500/30',
+ },
+ unclassified: {
+ label: '— ยังไม่ระบุ',
+ desc: 'กรอกข้อมูลให้ครบ',
+ icon: ,
+ color: 'text-slate-500',
+ bg: 'bg-night-800/40',
+ border: 'border-white/10',
+ },
+}
+
+let nextId = 1
+
+function ProductStrategyClassifier() {
+ const [products, setProducts] = useState([
+ { id: nextId++, name: 'สินค้า A', cost: 80, listPrice: 129 },
+ { id: nextId++, name: 'สินค้า B', cost: 120, listPrice: 299 },
+ { id: nextId++, name: 'สินค้า C', cost: 50, listPrice: 99 },
+ ])
+
+ const addRow = () => {
+ setProducts(p => [...p, { id: nextId++, name: `สินค้า ${String.fromCharCode(65 + p.length)}`, cost: 0, listPrice: 0 }])
+ }
+
+ const removeRow = (id: number) => setProducts(p => p.filter(r => r.id !== id))
+
+ const updateRow = (id: number, field: keyof ProductRow, value: string | number) => {
+ setProducts(p => p.map(r => r.id === id ? { ...r, [field]: value } : r))
+ }
+
+ const grouped = useMemo(() => {
+ const groups: Record = {
+ hero: [],
+ profit: [],
+ bundle: [],
+ unclassified: [],
+ }
+ products.forEach(p => {
+ const g = classifyProduct(p.cost, p.listPrice)
+ groups[g].push(p)
+ })
+ return groups
+ }, [products])
+
+ return (
+
+ {/* Input table */}
+
+
+
+
+ {/* Grouped summary */}
+
+ {(['hero', 'profit', 'bundle'] as ProductGroup[]).map(g => {
+ const cfg = GROUP_CONFIG[g]
+ const items = grouped[g]
+ return (
+
+
+ {cfg.icon}
+ {cfg.label}
+
+
{cfg.desc}
+ {items.length === 0 ? (
+
ยังไม่มีสินค้า
+ ) : (
+
+ {items.map(p => (
+ -
+ {p.name}
+
+ {pct(p.listPrice > 0 ? ((p.listPrice - p.cost) / p.listPrice) * 100 : 0)}
+
+
+ ))}
+
+ )}
+
+ )
+ })}
+
+
+
+
เกณฑ์การจัดกลุ่ม
+
🚀 Hero: Margin < 15% — ตัดกำไรต่ำมากเพื่อดึง traffic
+
💎 Profit Maker: Margin ≥ 30% — แหล่งรายได้หลัก
+
🎁 Bundle: Margin 15–29% — จัดชุดเพิ่ม AOV
+
+
+ )
+}
+
+// ─── Section 4: Break-even Alert ───────────────────────────────────────────────
+
+function BreakEvenAlert() {
+ const [cost, setCost] = useState(100)
+ const [commissionPct, setCommissionPct] = useState(8)
+ const [shipping, setShipping] = useState(30)
+ const [sellerPaysShipping, setSellerPaysShipping] = useState(true)
+ const [tiktokForcedPrice, setTiktokForcedPrice] = useState(150)
+
+ const shippingCost = sellerPaysShipping ? shipping : 0
+ const breakEven = (cost + shippingCost) / (1 - commissionPct / 100)
+ const margin = tiktokForcedPrice > 0 ? ((tiktokForcedPrice - breakEven) / tiktokForcedPrice) * 100 : -100
+ const profitPerUnit = tiktokForcedPrice * (1 - commissionPct / 100) - cost - shippingCost
+
+ const status: 'safe' | 'warn' | 'danger' =
+ profitPerUnit <= 0 ? 'danger' : margin < 5 ? 'warn' : 'safe'
+
+ const statusConfig = {
+ safe: {
+ icon: ,
+ label: '✅ เข้าร่วม Campaign ได้',
+ desc: `ยังมี margin ${pct(margin)} — กำไรต่อชิ้น ${fmt(profitPerUnit)}`,
+ bg: 'bg-green-500/10 border-green-500/30',
+ text: 'text-green-400',
+ },
+ warn: {
+ icon: ,
+ label: '⚠️ ระวัง — Margin เหลือน้อยมาก',
+ desc: `Margin เหลือเพียง ${pct(margin)} — กำไรต่อชิ้น ${fmt(profitPerUnit)} อาจไม่คุ้มค่าดำเนินการ`,
+ bg: 'bg-amber-500/10 border-amber-500/30',
+ text: 'text-amber-400',
+ },
+ danger: {
+ icon: ,
+ label: '🚫 ห้ามเข้า Campaign นี้เด็ดขาด!',
+ desc: `ราคา ${fmt(tiktokForcedPrice)} ต่ำกว่า Break-even ${fmt(breakEven)} — ขายทุกชิ้น ขาดทุนทุกออเดอร์!`,
+ bg: 'bg-rose-500/10 border-rose-500/30',
+ text: 'text-rose-400',
+ },
+ }[status]
+
+ // Visual gauge
+ const gaugeWidth = clamp(((tiktokForcedPrice / (breakEven * 1.5)) * 100), 0, 100)
+ const breakEvenPos = clamp((1 / 1.5) * 100, 0, 100)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Gauge */}
+
+
ตำแหน่งราคาแคมเปญ vs Break-even
+
+ {/* gradient bar */}
+
+ {/* fill */}
+
+ {/* break-even line */}
+
+ {/* indicator dot */}
+
+
+
+ ฿0
+ Break-even: {fmt(breakEven)}
+ {fmt(breakEven * 1.5)}
+
+
+
+ {/* Verdict */}
+
+ {statusConfig.icon}
+
+
{statusConfig.label}
+
{statusConfig.desc}
+ {status === 'danger' && (
+
+ 💡 ทางเลือก: อย่าเข้า Campaign นี้ หรือส่งเฉพาะ SKU ที่มี Buffer สูงพอ
+
+ )}
+
+
+
+ {/* Key stats */}
+
+
+ 0 ? `Margin ${pct(margin)}` : 'ขาดทุน!'}
+ highlight={status === 'safe'}
+ warn={status === 'warn'}
+ danger={status === 'danger'}
+ />
+
+
+ )
+}
+
+// ─── Main Page ─────────────────────────────────────────────────────────────────
+
+export default function TiktokPriceSimulatorPage() {
+ const [resetKey, setResetKey] = useState(0)
+ const [selectedCase, setSelectedCase] = useState<'case1' | 'case2' | 'case3'>('case1')
+
+ const [simBeforeDiscount, setSimBeforeDiscount] = useState(79.00)
+ const [simDiscountType, setSimDiscountType] = useState<'pct' | 'amount'>('amount')
+ const [simDiscountVal, setSimDiscountVal] = useState(17.90)
+ const [simCost, setSimCost] = useState(30.00)
+
+ const [simTransPct, setSimTransPct] = useState(3.21)
+ const [simCommPct, setSimCommPct] = useState(10.70)
+ const [simGrowthPct, setSimGrowthPct] = useState(7.50)
+ const [simAffiliatePct, setSimAffiliatePct] = useState(0)
+
+ const caseData = {
+ case1: {
+ beforeDiscount: 79.00,
+ shopDiscount: -17.90,
+ baseAmt: 61.10,
+ origTrans: -1.96,
+ origComm: -6.54,
+ origGrowth: -4.58,
+ origAffiliate: 0,
+ origTotalFees: -13.08,
+ originalPayout: 48.02,
+ defaultCost: 30.00,
+ label: 'เคสที่ 1: บิลสินค้า ฿79 (สุทธิ ฿48.02)',
+ },
+ case2: {
+ beforeDiscount: 190.00,
+ shopDiscount: -20.90,
+ baseAmt: 169.10,
+ origTrans: -5.43,
+ origComm: -18.09,
+ origGrowth: -12.67,
+ origAffiliate: 0,
+ origTotalFees: -36.19,
+ originalPayout: 132.91,
+ defaultCost: 80.00,
+ label: 'เคสที่ 2: บิลสินค้า ฿190 (สุทธิ ฿132.91)',
+ },
+ case3: {
+ beforeDiscount: 190.00,
+ shopDiscount: -37.81,
+ baseAmt: 152.19,
+ origTrans: -4.89,
+ origComm: -16.28,
+ origGrowth: -11.40,
+ origAffiliate: -7.61,
+ origTotalFees: -40.18,
+ originalPayout: 112.01,
+ defaultCost: 70.00,
+ label: 'เคสที่ 3: บิลสินค้า ฿190 + Affiliate (สุทธิ ฿112.01)',
+ }
+ }
+
+ const activeCase = caseData[selectedCase]
+
+ useEffect(() => {
+ setSimBeforeDiscount(activeCase.beforeDiscount)
+ setSimDiscountType('amount')
+ setSimDiscountVal(Math.abs(activeCase.shopDiscount))
+ setSimCost(activeCase.defaultCost)
+ setSimTransPct(3.21)
+ setSimCommPct(10.70)
+ setSimGrowthPct(7.50)
+ setSimAffiliatePct(selectedCase === 'case3' ? 5.00 : 0)
+ }, [selectedCase])
+
+ // Simulated calculations
+ const simDiscountAmount = simDiscountType === 'pct' ? simBeforeDiscount * (simDiscountVal / 100) : simDiscountVal
+ const baseAmt = Math.max(0, simBeforeDiscount - simDiscountAmount)
+
+ const simTransAmt = baseAmt * (simTransPct / 100)
+ const simCommAmt = baseAmt * (simCommPct / 100)
+ const simGrowthAmt = baseAmt * (simGrowthPct / 100)
+ const simAffiliateAmt = baseAmt * (simAffiliatePct / 100)
+ const simTotalAmt = simTransAmt + simCommAmt + simGrowthAmt + simAffiliateAmt
+ const simTotalPct = simTransPct + simCommPct + simGrowthPct + simAffiliatePct
+ const simPayout = baseAmt - simTotalAmt
+
+ const originalPayout = activeCase.originalPayout
+ const diffVal = simPayout - originalPayout
+ const diffPct = originalPayout > 0 ? (diffVal / originalPayout) * 100 : 0
+
+ // Profit/Margin/ROI Calculations
+ const origProfit = originalPayout - simCost
+ const origProfitMargin = activeCase.baseAmt > 0 ? (origProfit / activeCase.baseAmt) * 100 : 0
+ const origROI = simCost > 0 ? (origProfit / simCost) * 100 : 0
+
+ const simProfit = simPayout - simCost
+ const simProfitMargin = baseAmt > 0 ? (simProfit / baseAmt) * 100 : 0
+ const simROI = simCost > 0 ? (simProfit / simCost) * 100 : 0
+
+ return (
+
+ {/* Page header */}
+
+
+
+
+
+
+
TikTok Price Simulator
+
+
+ คำนวณราคาตั้ง, จุด Break-even, และกลยุทธ์โปรโมชันก่อนเข้าร่วม Campaign —
+ ป้องกัน "ขายดีแต่เข้าเนื้อ" ทุกออเดอร์
+
+
+
+
+
+ {/* Quick guide */}
+
+ {[
+ { num: '1', title: 'ตั้งราคา', desc: 'คำนวณ List / Selling / Campaign Price พร้อม Buffer' },
+ { num: '2', title: 'Volume vs Margin', desc: 'เปรียบกำไรรวมวันปกติ vs วัน Campaign' },
+ { num: '3', title: 'จัดกลุ่มสินค้า', desc: 'Hero / Profit Maker / Bundle ตาม Margin %' },
+ { num: '4', title: 'เช็ค Break-even', desc: 'ราคาแคมเปญที่ TikTok กำหนด เข้าได้หรือไม่' },
+ ].map(s => (
+
+ ))}
+
+
+ {/* Sections */}
+
}
+ title="ส่วนที่ 1 — Price Buffer Calculator"
+ subtitle="คำนวณราคาตั้งที่เผื่อ Buffer สำหรับ Campaign ไว้แล้วตั้งแต่ต้น"
+ accentColor="cyan"
+ >
+
+
+
+
}
+ title="ส่วนที่ 2 — Volume vs Margin Simulator"
+ subtitle="เปรียบเทียบกำไรรวมวันปกติ vs วัน Campaign เพื่อตัดสินใจ"
+ accentColor="violet"
+ defaultOpen={false}
+ >
+
+
+
+
}
+ title="ส่วนที่ 3 — Product Strategy Classifier"
+ subtitle="จัดกลุ่มสินค้าเป็น 3 กลุ่มตาม Margin % เพื่อวางกลยุทธ์ต่างกัน"
+ accentColor="amber"
+ defaultOpen={false}
+ >
+
+
+
+
}
+ title="ส่วนที่ 4 — Break-even Alert"
+ subtitle="เช็คทันทีว่าราคาที่ TikTok บังคับ ต่ำกว่า Break-even หรือไม่"
+ accentColor="rose"
+ defaultOpen={false}
+ >
+
+
+
+ {/* Footer tip */}
+
+
💡 กฎเหล็ก: อย่าหลงกับตัวเลข GMV
+
+ ยอดขาย (GMV) ตัวเลขสวย ≠ เงินในบัญชี ให้โฟกัสที่{' '}
+ กำไรสุทธิหลังหักทุกอย่างแล้ว (Net Profit) เสมอ
+ ถ้าแคมเปญไหนบังคับให้ขาดทุน → อย่าเข้า หรือส่งเฉพาะ SKU ที่มี Buffer สูงพอเท่านั้น
+
+
+
+ {/* Real Payout Case Study Example */}
+
+
+
+
+
กรณีศึกษา & เครื่องมือจำลองค่าธรรมเนียมจริง (TikTok Shop)
+
+
+
+
+
+
+
+
+
+
+
+
+ รายละเอียดบิลจริงดั้งเดิมของ {selectedCase === 'case1' ? 'ตัวอย่างที่ 1' : selectedCase === 'case2' ? 'ตัวอย่างที่ 2' : 'ตัวอย่างที่ 3'}:
+
+
+
+ ยอดรวมสินค้าก่อนลด
+ {fmt(activeCase.beforeDiscount)}
+
+
+ ส่วนลดจากร้านค้า (ลดไป %)
+
+ {fmt(activeCase.shopDiscount)} (-{((Math.abs(activeCase.shopDiscount) / activeCase.beforeDiscount) * 100).toFixed(1)}%)
+
+
+
+ ยอดรวมหลังหักส่วนลด (ฐานคิดค่าคอม)
+ {fmt(activeCase.baseAmt)}
+
+
+
+ {/* Simulator controls - Price & Discounts */}
+
1. จำลองการตั้งราคา ส่วนลด และต้นทุนสินค้า (ปรับแต่งเองได้):
+
+
+
+
+ setSimBeforeDiscount(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+ ฿
+
+
+
+
+
+
+
+
+
+
setSimDiscountVal(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+
{simDiscountType === 'pct' ? '%' : '฿'}
+
+
+
+
+
+ setSimCost(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+ ฿
+
+
+
+
+ {/* Simulator controls - Fees */}
+
2. จำลองการปรับเปลี่ยนอัตราค่าธรรมเนียม (%):
+
+
+
+
+ setSimTransPct(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+ %
+
+
+
+
+
+ setSimCommPct(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+ %
+
+
+
+
+
+ setSimGrowthPct(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+ %
+
+
+
+
+
+ setSimAffiliatePct(parseFloat(e.target.value) || 0)}
+ className="admin-input w-full text-xs"
+ />
+ %
+
+
+
+
+ {/* Side-by-side comparison tables */}
+
+ {/* ก่อนปรับ (ค่าธรรมเนียมจากบิลตัวอย่างจริง) */}
+
+
+ ก่อนปรับ (บิลดั้งเดิม)
+ รวม {((Math.abs(activeCase.origTotalFees) / activeCase.baseAmt) * 100).toFixed(2)}%
+
+
+
+ ยอดรวมสินค้าก่อนลด
+ {fmt(activeCase.beforeDiscount)}
+
+
+ ส่วนลดจากร้านค้า
+ {fmt(activeCase.shopDiscount)}
+
+
+ ยอดสินค้าสุทธิ (ฐานคิดคอม)
+ {fmt(activeCase.baseAmt)}
+
+
+ - ค่าคำสั่งซื้อ (Transaction 3.21%)
+ {fmt(activeCase.origTrans)}
+
+
+ - ค่าคอม TikTok Shop (10.70%)
+ {fmt(activeCase.origComm)}
+
+
+ - ค่าสนับสนุนการเติบโต (7.50%)
+ {fmt(activeCase.origGrowth)}
+
+ {activeCase.origAffiliate !== 0 && (
+
+ - ค่าคอมแอฟฟิลิเอต (5.00%)
+ {fmt(activeCase.origAffiliate)}
+
+ )}
+
+ รวมค่าธรรมเนียมโดนหัก
+ {fmt(activeCase.origTotalFees)}
+
+
+ ยอดเงินโอนเข้าบัญชีสุทธิ
+ {fmt(activeCase.originalPayout)}
+
+
+ - ต้นทุนสินค้า (Cost)
+ ฿-{simCost.toFixed(2)}
+
+
+ กำไรสุทธิ (Net Profit)
+ = 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {origProfit >= 0 ? '' : '-'}฿{Math.abs(origProfit).toFixed(2)}
+
+
+
+ กำไรต่อยอดขาย (Margin %)
+ = 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {origProfitMargin.toFixed(1)}%
+
+
+
+ กำไรต่อทุน (ROI %)
+ = 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {origROI.toFixed(1)}%
+
+
+
+
+
+ {/* หลังปรับ (จำลองการปรับค่าธรรมเนียม) */}
+
+
+ หลังปรับ (ผลการจำลองใหม่)
+ รวม {simTotalPct.toFixed(2)}%
+
+
+
+ ยอดรวมสินค้าก่อนลด
+ {fmt(simBeforeDiscount)}
+
+
+ ส่วนลดจากร้านค้า
+
+ -{fmt(simDiscountAmount)} {simDiscountType === 'pct' && `(${simDiscountVal}%)`}
+
+
+
+ ยอดสินค้าสุทธิ (ฐานคิดคอม)
+ {fmt(baseAmt)}
+
+
+ - ค่าคำสั่งซื้อ ({simTransPct.toFixed(2)}%)
+ ฿-{simTransAmt.toFixed(2)}
+
+
+ - ค่าคอม TikTok Shop ({simCommPct.toFixed(2)}%)
+ ฿-{simCommAmt.toFixed(2)}
+
+
+ - ค่าสนับสนุนการเติบโต ({simGrowthPct.toFixed(2)}%)
+ ฿-{simGrowthAmt.toFixed(2)}
+
+ {simAffiliatePct > 0 && (
+
+ - ค่าคอมคอมแอฟฟิลิเอต ({simAffiliatePct.toFixed(2)}%)
+ ฿-{simAffiliateAmt.toFixed(2)}
+
+ )}
+
+ รวมค่าธรรมเนียมโดนหัก
+ ฿-{simTotalAmt.toFixed(2)}
+
+
+ ยอดเงินโอนเข้าบัญชีสุทธิ
+ ฿{simPayout.toFixed(2)}
+
+
+ - ต้นทุนสินค้า (Cost)
+ ฿-{simCost.toFixed(2)}
+
+
+ กำไรสุทธิ (Net Profit)
+ = 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {simProfit >= 0 ? '' : '-'}฿{Math.abs(simProfit).toFixed(2)}
+
+
+
+ กำไรต่อยอดขาย (Margin %)
+ = 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {simProfitMargin.toFixed(1)}%
+
+
+
+ กำไรต่อทุน (ROI %)
+ = 0 ? 'text-green-400' : 'text-rose-400'}`}>
+ {simROI.toFixed(1)}%
+
+
+
+
+
+
+ {/* Difference comparison status card */}
+
= 0 ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-rose-500/10 border-rose-500/30 text-rose-400'}`}>
+
+
+
+ {diffVal >= 0
+ ? `ยอดรับเงินเพิ่มขึ้น ฿${diffVal.toFixed(2)} จากโครงสร้างธรรมเนียมใหม่`
+ : `ยอดเงินโอนเข้าบัญชีสุทธิจะต่างไป ฿${diffVal.toFixed(2)} (${diffVal >= 0 ? '+' : ''}${diffPct.toFixed(1)}%) เมื่อเทียบกับบิลจริงดั้งเดิม`}
+
+
+
{diffVal >= 0 ? '+' : ''}฿{diffVal.toFixed(2)}
+
+
+
+ )
+}