From a6661f946ffde9b636a758f1b6bb1510e19e7af3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 8 Aug 2026 23:24:31 -0400 Subject: [PATCH] x --- apps/admin/src/App.tsx | 2 + apps/admin/src/components/Sidebar.tsx | 2 + .../src/pages/TiktokPriceSimulatorPage.tsx | 1387 +++++++++++++++++ 3 files changed, 1391 insertions(+) create mode 100644 apps/admin/src/pages/TiktokPriceSimulatorPage.tsx 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 && ( +
+

{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 */} +
+ + + + + + + + + + + + {products.map(p => { + const margin = p.listPrice > 0 ? ((p.listPrice - p.cost) / p.listPrice) * 100 : 0 + const group = classifyProduct(p.cost, p.listPrice) + const cfg = GROUP_CONFIG[group] + return ( + + + + + + + + + ) + })} + +
ชื่อสินค้าต้นทุน (฿)List Price (฿)Margin %กลุ่ม +
+ updateRow(p.id, 'name', e.target.value)} + /> + + updateRow(p.id, 'cost', parseFloat(e.target.value) || 0)} + /> + + updateRow(p.id, 'listPrice', parseFloat(e.target.value) || 0)} + /> + + = 30 ? 'text-cyan-glow' : margin >= 15 ? 'text-violet-400' : 'text-amber-400'}`}> + {pct(margin)} + + + + {cfg.label} + + + +
+
+ + + + {/* 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 => ( +
+
+ {s.num} +
+
+

{s.title}

+

{s.desc}

+
+
+ ))} +
+ + {/* 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)} +
+
+
+ ) +}