This commit is contained in:
Your Name committed 2026-08-12 01:25:33 -04:00
1 parent 85a0f984eb
commit e25d998893
5 files changed
+938 -44

No files matched your search

+2
View File
@@ -46,6 +46,7 @@ import ContactMessagesPage from './pages/ContactMessagesPage'
import ProductAttributesPage from './pages/ProductAttributesPage'
import TiktokPriceSimulatorPage from './pages/TiktokPriceSimulatorPage'
import TiktokShopCasePage from './pages/TiktokShopCasePage'
import ShopeeShopCasePage from './pages/ShopeeShopCasePage'
function App() {
return (
@@ -92,6 +93,7 @@ function App() {
<Route path="/tiktok-analysis" element={<TiktokAnalysisPage />} />
<Route path="/tiktok-price-simulator" element={<TiktokPriceSimulatorPage />} />
<Route path="/tiktok-shop-case" element={<TiktokShopCasePage />} />
<Route path="/shopee-shop-case" element={<ShopeeShopCasePage />} />
<Route path="/shopee" element={<ShopeePage />} />
<Route path="/shopee/:orderId" element={<ShopeeOrderDetailPage />} />
<Route path="/shopee-analytics" element={<ShopeeAnalyticsPage />} />
+5 -2
View File
@@ -1,11 +1,14 @@
import { useState } from 'react'
import { Outlet } from 'react-router-dom'
import Sidebar from './Sidebar'
export default function AdminLayout() {
const [isCollapsed, setIsCollapsed] = useState(false)
return (
<div className="flex min-h-screen bg-night-950">
<Sidebar />
<main className="flex-1 ml-72 p-10">
<Sidebar isCollapsed={isCollapsed} onToggle={() => setIsCollapsed(!isCollapsed)} />
<main className={`flex-1 p-10 transition-all duration-300 ${isCollapsed ? 'ml-20' : 'ml-72'}`}>
<div className="w-full max-w-[1600px] mx-auto space-y-10">
<Outlet />
</div>
+353 -41
View File
@@ -29,36 +29,62 @@ import {
Sparkles,
Calculator,
Info,
ChevronLeft,
} from 'lucide-react'
const MAIN_MENU = [
{ icon: LayoutDashboard, label: 'Dashboard', to: '/' },
]
const PRODUCT_MENU = [
{ icon: Tags, label: 'Categories', to: '/categories' },
{ icon: Package, label: 'Products', to: '/products' },
{ icon: Ruler, label: 'Product Attributes', to: '/product-attributes' },
{ icon: PackageOpen, label: 'Raw Materials', to: '/raw-materials' },
{ icon: Users, label: 'LINE Customers', to: '/customers' },
]
const SALES_MENU = [
{ icon: ShoppingCart, label: 'Orders', to: '/orders' },
{ icon: Ticket, label: 'Promotions', to: '/promotions' },
{ icon: Ticket, label: 'Coupons', to: '/coupons' },
{ icon: Layers, label: 'Page Builder (CMS)', to: '/page-builder' },
{ icon: Sparkles, label: 'CMS Block Showcase', to: '/cms-mockup' },
{ icon: ImageIcon, label: 'Media Library', to: '/media' },
{ icon: BookOpen, label: 'Scent Guide', to: '/articles' },
{ icon: TrendingUp, label: 'TikTok Data', to: '/tiktok' },
{ icon: TrendingUp, label: 'TikTok Analysis', to: '/tiktok-analysis' },
{ icon: ShoppingBag, label: 'Shopee (Hom Aroma)', to: '/shopee' },
{ icon: BarChart3, label: 'Shopee Analytics', to: '/shopee-analytics' },
{ icon: Layers, label: 'Shopee ทุกร้าน', to: '/shopee-overview' },
{ icon: Globe2, label: 'All Analytics', to: '/global-analytics' },
]
const TIKTOK_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: Info, label: 'TikTok Shop Case', to: '/tiktok-shop-case' },
]
const TIKTOK_LEGACY_MENU = [
{ icon: TrendingUp, label: 'TikTok Report Import', to: '/tiktok' },
{ icon: TrendingUp, label: 'Product SKU Analysis', to: '/tiktok-analysis' },
]
const SHOPEE_MENU = [
{ icon: ShoppingBag, label: 'Shopee (Hom Aroma)', to: '/shopee' },
{ icon: BarChart3, label: 'Shopee Analytics', to: '/shopee-analytics' },
{ icon: Layers, label: 'Shopee ทุกร้าน', to: '/shopee-overview' },
{ icon: Info, label: 'Shopee Shop Case', to: '/shopee-shop-case' },
]
const LAZADA_MENU = [
{ icon: ShoppingCart, label: 'Lazada Orders', to: '/lazada-order' },
{ icon: BarChart3, label: 'Lazada Analytics', to: '/lazada-analytics' },
{ icon: Layers, label: 'Lazada ทุกร้าน', to: '/lazada-overview' },
{ icon: Globe2, label: 'All Analytics', to: '/global-analytics' },
]
const CMS_MENU = [
{ icon: Layers, label: 'Page Builder (CMS)', to: '/page-builder' },
{ icon: Sparkles, label: 'CMS Block Showcase', to: '/cms-mockup' },
{ icon: ImageIcon, label: 'Media Library', to: '/media' },
{ icon: BookOpen, label: 'Scent Guide', to: '/articles' },
]
const CUSTOMER_MENU = [
{ icon: Users, label: 'LINE Customers', to: '/customers' },
{ icon: Users, label: 'Subscribers', to: '/subscribers' },
{ icon: MessageSquare, label: 'Contact Messages', to: '/contacts' },
]
@@ -78,11 +104,33 @@ const SETTINGS_MENU = [
{ icon: Wrench, label: 'System', to: '/settings/system' },
]
export default function Sidebar() {
interface SidebarProps {
isCollapsed: boolean
onToggle: () => void
}
export default function Sidebar({ isCollapsed, onToggle }: SidebarProps) {
const navigate = useNavigate()
const location = useLocation()
const isInProduct = ['/categories', '/products', '/product-attributes', '/raw-materials'].some(p => location.pathname.startsWith(p))
const isInSales = ['/orders', '/promotions', '/coupons', '/global-analytics'].some(p => location.pathname.startsWith(p))
const isInTiktokLegacy = location.pathname === '/tiktok' || location.pathname.startsWith('/tiktok-analysis')
const isInTiktok = location.pathname.startsWith('/tiktok') && !isInTiktokLegacy
const isInShopee = location.pathname.startsWith('/shopee')
const isInLazada = location.pathname.startsWith('/lazada')
const isInCms = ['/page-builder', '/cms-mockup', '/media', '/articles'].some(p => location.pathname.startsWith(p))
const isInCustomer = ['/customers', '/subscribers', '/contacts'].some(p => location.pathname.startsWith(p))
const isInMenuManager = location.pathname.startsWith('/menu-manager')
const isInSettings = location.pathname.startsWith('/settings')
const [productOpen, setProductOpen] = useState(isInProduct)
const [salesOpen, setSalesOpen] = useState(isInSales)
const [tiktokOpen, setTiktokOpen] = useState(isInTiktok)
const [tiktokLegacyOpen, setTiktokLegacyOpen] = useState(isInTiktokLegacy)
const [shopeeOpen, setShopeeOpen] = useState(isInShopee)
const [lazadaOpen, setLazadaOpen] = useState(isInLazada)
const [cmsOpen, setCmsOpen] = useState(isInCms)
const [customerOpen, setCustomerOpen] = useState(isInCustomer)
const [menuManagerOpen, setMenuManagerOpen] = useState(isInMenuManager)
const [settingsOpen, setSettingsOpen] = useState(isInSettings)
@@ -92,13 +140,31 @@ export default function Sidebar() {
navigate('/login')
}
const handleGroupClick = (setOpen: React.Dispatch<React.SetStateAction<boolean>>) => {
if (isCollapsed) {
onToggle()
setOpen(true)
} else {
setOpen(v => !v)
}
}
return (
<aside className="w-72 h-screen fixed left-0 top-0 bg-night-900 border-r border-white/5 flex flex-col z-50">
{/* Brand */}
<div className="p-8">
<div className="text-xl font-bold tracking-widest text-white uppercase italic">
HomAroma <span className="text-cyan-glow text-xs not-italic font-light opacity-60">Admin</span>
</div>
<aside className={`h-screen fixed left-0 top-0 bg-night-900 border-r border-white/5 flex flex-col z-50 transition-all duration-300 ${isCollapsed ? 'w-20' : 'w-72'}`}>
{/* Brand & Toggle Header */}
<div className={`p-6 flex items-center ${isCollapsed ? 'justify-center' : 'justify-between'} border-b border-white/5`}>
{!isCollapsed && (
<div className="text-xl font-bold tracking-widest text-white uppercase italic truncate">
HomAroma <span className="text-cyan-glow text-xs not-italic font-light opacity-60">Admin</span>
</div>
)}
<button
onClick={onToggle}
title={isCollapsed ? 'ขยายเมนู' : 'ย่อเมนู'}
className="p-1.5 rounded-lg bg-white/5 border border-white/10 text-slate-400 hover:text-white transition-colors cursor-pointer"
>
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</button>
</div>
{/* Navigation — scrollable */}
@@ -108,28 +174,270 @@ export default function Sidebar() {
key={item.to}
to={item.to}
end={item.to === '/'}
title={isCollapsed ? item.label : undefined}
className={({ isActive }) =>
`sidebar-link ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
`sidebar-link ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'} ${isCollapsed ? 'justify-center px-0' : ''}`
}
>
<item.icon className="w-5 h-5 shrink-0" />
<span className="flex-1">{item.label}</span>
<ChevronRight className="w-4 h-4 opacity-0 group-hover:opacity-40 transition-opacity" />
{!isCollapsed && <span className="flex-1">{item.label}</span>}
{!isCollapsed && <ChevronRight className="w-4 h-4 opacity-0 group-hover:opacity-40 transition-opacity" />}
</NavLink>
))}
{/* Catalog Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setProductOpen)}
title={isCollapsed ? 'Catalog' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInProduct ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<Package className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">Catalog</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${productOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && productOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{PRODUCT_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* Sales & Marketing Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setSalesOpen)}
title={isCollapsed ? 'Sales & Marketing' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInSales ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<ShoppingCart className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">Sales & Marketing</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${salesOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && salesOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{SALES_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* TikTok Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setTiktokOpen)}
title={isCollapsed ? 'TikTok' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInTiktok ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<TrendingUp className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">TikTok</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${tiktokOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && tiktokOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{TIKTOK_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* TikTok Legacy Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setTiktokLegacyOpen)}
title={isCollapsed ? 'TikTok Legacy' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInTiktokLegacy ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<TrendingUp className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">TikTok Legacy</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${tiktokLegacyOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && tiktokLegacyOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{TIKTOK_LEGACY_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* Shopee Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setShopeeOpen)}
title={isCollapsed ? 'Shopee' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInShopee ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<ShoppingBag className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">Shopee</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${shopeeOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && shopeeOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{SHOPEE_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* Lazada Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setLazadaOpen)}
title={isCollapsed ? 'Lazada' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInLazada ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<ShoppingCart className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">Lazada</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${lazadaOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && lazadaOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{LAZADA_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* Content (CMS) Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setCmsOpen)}
title={isCollapsed ? 'Content (CMS)' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInCms ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<Layers className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">Content (CMS)</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${cmsOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && cmsOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{CMS_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* Customers & Messages Group */}
<div className="pt-1">
<button
onClick={() => handleGroupClick(setCustomerOpen)}
title={isCollapsed ? 'Customers & Messages' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInCustomer ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<Users className="w-5 h-5 shrink-0" />
{!isCollapsed && <span className="flex-1 text-left">Customers & Messages</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${customerOpen ? 'rotate-180' : ''}`} />}
</button>
{!isCollapsed && customerOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{CUSTOMER_MENU.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
`sidebar-link text-sm ${isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'}`
}
>
<item.icon className="w-4 h-4 shrink-0" />
<span className="flex-1">{item.label}</span>
</NavLink>
))}
</div>
)}
</div>
{/* Menu Manager Group */}
<div className="pt-1">
<button
onClick={() => setMenuManagerOpen(v => !v)}
className={`sidebar-link sidebar-link-inactive w-full ${isInMenuManager ? 'text-white' : ''}`}
onClick={() => handleGroupClick(setMenuManagerOpen)}
title={isCollapsed ? 'Menu Manager' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInMenuManager ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<Layers className="w-5 h-5 shrink-0" />
<span className="flex-1 text-left">Menu Manager</span>
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${menuManagerOpen ? 'rotate-180' : ''}`} />
{!isCollapsed && <span className="flex-1 text-left">Menu Manager</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${menuManagerOpen ? 'rotate-180' : ''}`} />}
</button>
{menuManagerOpen && (
{!isCollapsed && menuManagerOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{MENU_MANAGER_MENU.map((item) => (
<NavLink
@@ -150,15 +458,16 @@ export default function Sidebar() {
{/* Settings Group */}
<div className="pt-1">
<button
onClick={() => setSettingsOpen(v => !v)}
className={`sidebar-link sidebar-link-inactive w-full ${isInSettings ? 'text-white' : ''}`}
onClick={() => handleGroupClick(setSettingsOpen)}
title={isCollapsed ? 'Settings' : undefined}
className={`sidebar-link sidebar-link-inactive w-full ${isInSettings ? 'text-white' : ''} ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<Settings className="w-5 h-5 shrink-0" />
<span className="flex-1 text-left">Settings</span>
<ChevronDown className={`w-4 h-4 transition-transform duration-200 ${settingsOpen ? 'rotate-180' : ''}`} />
{!isCollapsed && <span className="flex-1 text-left">Settings</span>}
{!isCollapsed && <ChevronDown className={`w-4 h-4 transition-transform duration-200 ${settingsOpen ? 'rotate-180' : ''}`} />}
</button>
{settingsOpen && (
{!isCollapsed && settingsOpen && (
<div className="mt-1 ml-4 pl-4 border-l border-white/10 space-y-1">
{SETTINGS_MENU.map((item) => (
<NavLink
@@ -179,23 +488,26 @@ export default function Sidebar() {
</nav>
{/* User / Bottom */}
<div className="p-4 border-t border-white/5 space-y-2">
<div className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-all group">
<div className="w-9 h-9 rounded-full bg-cyan-glow/20 flex items-center justify-center text-cyan-glow font-bold text-xs border border-cyan-glow/30">
<div className={`p-4 border-t border-white/5 space-y-2 ${isCollapsed ? 'items-center justify-center' : ''}`}>
<div className={`flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-all group ${isCollapsed ? 'justify-center px-0' : ''}`} title={isCollapsed ? 'Admin Account' : undefined}>
<div className="w-9 h-9 rounded-full bg-cyan-glow/20 flex items-center justify-center text-cyan-glow font-bold text-xs border border-cyan-glow/30 shrink-0">
A
</div>
<div className="flex-1 overflow-hidden">
<p className="text-sm font-bold text-white truncate">Admin Account</p>
<p className="text-[10px] uppercase tracking-wider text-slate-500">System Manager</p>
</div>
{!isCollapsed && (
<div className="flex-1 overflow-hidden">
<p className="text-sm font-bold text-white truncate">Admin Account</p>
<p className="text-[10px] uppercase tracking-wider text-slate-500">System Manager</p>
</div>
)}
</div>
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-rose-500 hover:bg-rose-500/10 transition-all cursor-pointer"
title={isCollapsed ? 'Logout' : undefined}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-rose-500 hover:bg-rose-500/10 transition-all cursor-pointer ${isCollapsed ? 'justify-center px-0' : ''}`}
>
<LogOut className="w-5 h-5" />
Logout
<LogOut className="w-5 h-5 shrink-0" />
{!isCollapsed && <span>Logout</span>}
</button>
</div>
</aside>
+577
View File
@@ -0,0 +1,577 @@
import { useState, useEffect } from 'react'
import {
Calculator,
Info,
RefreshCw
} from 'lucide-react'
function fmt(val: number) {
return '฿' + val.toLocaleString('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
export default function ShopeeShopCasePage() {
const [resetKey, setResetKey] = useState(0)
const [selectedCase, setSelectedCase] = useState<'case1' | 'case2' | 'case3'>('case1')
const [simBeforeDiscount, setSimBeforeDiscount] = useState(79.00)
const [discounts, setDiscounts] = useState<Array<{ type: 'pct' | 'amount'; value: number }>>([
{ type: 'amount', value: 0 },
{ type: 'amount', value: 0 },
{ type: 'amount', value: 0 },
])
const [simCost, setSimCost] = useState(30.00)
const [simCommPct, setSimCommPct] = useState(16.46)
const [simServicePct, setSimServicePct] = useState(8.86)
const [simTransPct, setSimTransPct] = useState(3.80)
const [simAmsPct, setSimAmsPct] = useState(0)
const caseData = {
case1: {
beforeDiscount: 79.00,
shopDiscount: 0,
baseAmt: 79.00,
origComm: -13.00,
origService: -7.00,
origTrans: -3.00,
origAms: 0,
origTotalFees: -23.00,
originalPayout: 56.00,
defaultCost: 30.00,
label: 'เคสที่ 1: บิลสินค้า ฿79 (สุทธิ ฿56.00)',
},
case2: {
beforeDiscount: 79.00,
shopDiscount: 0,
baseAmt: 79.00,
origComm: -13.00,
origService: -7.00,
origTrans: -3.00,
origAms: -1.00,
origTotalFees: -24.00,
originalPayout: 55.00,
defaultCost: 30.00,
label: 'เคสที่ 2: บิลสินค้า ฿79 + AMS (สุทธิ ฿55.00)',
},
case3: {
beforeDiscount: 190.00,
shopDiscount: 0,
baseAmt: 190.00,
origComm: -24.00,
origService: -16.00,
origTrans: -7.00,
origAms: -13.00,
origTotalFees: -60.00,
originalPayout: 130.00,
defaultCost: 80.00,
label: 'เคสที่ 3: บิลสินค้า ฿190 + AMS (สุทธิ ฿130.00)',
}
}
const activeCase = caseData[selectedCase]
useEffect(() => {
setSimBeforeDiscount(activeCase.beforeDiscount)
setDiscounts([
{ type: 'amount', value: Math.abs(activeCase.shopDiscount) },
{ type: 'amount', value: 0 },
{ type: 'amount', value: 0 },
])
setSimCost(activeCase.defaultCost)
if (selectedCase === 'case1') {
setSimCommPct(16.46)
setSimServicePct(8.86)
setSimTransPct(3.80)
setSimAmsPct(0)
} else if (selectedCase === 'case2') {
setSimCommPct(16.46)
setSimServicePct(8.86)
setSimTransPct(3.80)
setSimAmsPct(1.27)
} else {
setSimCommPct(12.63)
setSimServicePct(8.42)
setSimTransPct(3.68)
setSimAmsPct(6.84)
}
}, [selectedCase])
// Simulated calculations
const simDiscountAmount = discounts.reduce((sum, d) => {
const amt = d.type === 'pct' ? simBeforeDiscount * (d.value / 100) : d.value
return sum + amt
}, 0)
const baseAmt = Math.max(0, simBeforeDiscount - simDiscountAmount)
const simCommAmt = baseAmt * (simCommPct / 100)
const simServiceAmt = baseAmt * (simServicePct / 100)
const simTransAmt = baseAmt * (simTransPct / 100)
const simAmsAmt = baseAmt * (simAmsPct / 100)
const simTotalAmt = simCommAmt + simServiceAmt + simTransAmt + simAmsAmt
const simTotalPct = simCommPct + simServicePct + simTransPct + simAmsPct
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 (
<div className="min-h-screen p-6 md:p-8 max-w-5xl mx-auto animate-fade-in" key={resetKey}>
{/* Page header */}
<div className="flex items-start justify-between mb-8 gap-4">
<div>
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center">
<Calculator className="w-5 h-5 text-orange-500" />
</div>
<h1 className="text-2xl font-bold text-white tracking-tight">Shopee Shop Case</h1>
</div>
<p className="text-sm text-slate-500 max-w-2xl">
ศึกษาโครงสร้างบิลรับเงินจริงจากระบบ Shopee พร้อมทั้งจำลองคำนวณราคาขาย, คูปองส่วนลดร้านค้า, ต้นทุน และประเมินสถิติกำไร & ROI ก่อนขายจริง
</p>
</div>
<button
onClick={() => {
setResetKey(k => k + 1)
if (selectedCase === 'case1') {
setSimCommPct(16.46)
setSimServicePct(8.86)
setSimTransPct(3.80)
setSimAmsPct(0)
} else if (selectedCase === 'case2') {
setSimCommPct(16.46)
setSimServicePct(8.86)
setSimTransPct(3.80)
setSimAmsPct(1.27)
} else {
setSimCommPct(12.63)
setSimServicePct(8.42)
setSimTransPct(3.68)
setSimAmsPct(6.84)
}
}}
className="btn-ghost flex items-center gap-2 text-xs shrink-0"
title="รีเซ็ตทุกค่า"
>
<RefreshCw className="w-4 h-4" />
Reset
</button>
</div>
{/* Real Payout Case Study Example */}
<div className="admin-card border-white/10 animate-fade-in">
<div className="flex items-center justify-between mb-4 flex-wrap gap-4 border-b border-white/5 pb-4">
<div className="flex items-center gap-3">
<Info className="w-5 h-5 text-orange-500" />
<h2 className="text-lg font-bold text-white">กรณีศึกษา & เครื่องมือจำลองค่าธรรมเนียมจริง (Shopee)</h2>
</div>
<div className="flex items-center gap-2">
<div className="flex p-1 bg-night-900 border border-white/10 rounded-lg">
<button
type="button"
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-all ${selectedCase === 'case1' ? 'bg-orange-500/20 text-orange-500' : 'text-slate-400 hover:text-white'}`}
onClick={() => setSelectedCase('case1')}
>
ตัวอย่างที่ 1 (ยอด ฿79)
</button>
<button
type="button"
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-all ${selectedCase === 'case2' ? 'bg-orange-500/20 text-orange-500' : 'text-slate-400 hover:text-white'}`}
onClick={() => setSelectedCase('case2')}
>
ตัวอย่างที่ 2 (ยอด ฿79 + AMS)
</button>
<button
type="button"
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-all ${selectedCase === 'case3' ? 'bg-orange-500/20 text-orange-500' : 'text-slate-400 hover:text-white'}`}
onClick={() => setSelectedCase('case3')}
>
ตัวอย่างที่ 3 (ยอด ฿190 + AMS)
</button>
</div>
<button
onClick={() => {
if (selectedCase === 'case1') {
setSimCommPct(16.46)
setSimServicePct(8.86)
setSimTransPct(3.80)
setSimAmsPct(0)
} else if (selectedCase === 'case2') {
setSimCommPct(16.46)
setSimServicePct(8.86)
setSimTransPct(3.80)
setSimAmsPct(1.27)
} else {
setSimCommPct(12.63)
setSimServicePct(8.42)
setSimTransPct(3.68)
setSimAmsPct(6.84)
}
}}
className="btn-ghost text-xs py-1.5 px-3 rounded border border-white/10"
>
ใช้ค่าตั้งต้นตัวอย่าง
</button>
</div>
</div>
<p className="text-xs text-slate-400 mb-2 font-medium">
รายละเอียดบิลจริงดั้งเดิมของ {selectedCase === 'case1' ? 'ตัวอย่างที่ 1' : 'ตัวอย่างที่ 2'}:
</p>
<div className="grid grid-cols-3 gap-4 p-3 rounded-lg bg-night-800/40 border border-white/5 text-xs mb-6 opacity-80">
<div>
<span className="text-slate-500 block">ยอดรวมสินค้าก่อนลด</span>
<span className="font-bold text-white font-mono">{fmt(activeCase.beforeDiscount)}</span>
</div>
<div>
<span className="text-slate-500 block">ส่วนลดจากร้านค้า</span>
<span className="font-bold text-rose-400 font-mono">
{fmt(activeCase.shopDiscount)} (0.0%)
</span>
</div>
<div>
<span className="text-slate-500 block">ยอดรวมหลังหักส่วนลด (ฐานคิดค่าคอม)</span>
<span className="font-bold text-orange-500 font-mono">{fmt(activeCase.baseAmt)}</span>
</div>
</div>
{/* Simulator controls - Price & Discounts */}
<p className="text-xs text-slate-400 mb-2 font-medium">1. จำลองการตั้งราคา ส่วนลด และต้นทุนสินค้า (ปรับแต่งเองได้):</p>
<div className="mb-4 grid grid-cols-1 sm:grid-cols-3 gap-4 p-4 rounded-xl bg-night-800/40 border border-white/5">
<div className="space-y-4">
<div>
<label className="block text-xs text-slate-400 font-medium mb-1.5">ราคาขายก่อนลด (List Price)</label>
<div className="flex items-center gap-2">
<input
type="number"
step="1"
min="0"
value={simBeforeDiscount}
onChange={e => setSimBeforeDiscount(parseFloat(e.target.value) || 0)}
className="admin-input w-full text-xs"
/>
<span className="text-xs text-slate-500">฿</span>
</div>
</div>
<div>
<label className="block text-xs text-slate-400 font-medium mb-1.5">ต้นทุนสินค้า (Cost)</label>
<div className="flex items-center gap-2 mb-2">
<input
type="number"
step="1"
min="0"
value={simCost}
onChange={e => setSimCost(parseFloat(e.target.value) || 0)}
className="admin-input w-full text-xs"
/>
<span className="text-xs text-slate-500">฿</span>
</div>
<div className="flex gap-2">
{[20, 45, 50].map(val => (
<button
key={val}
type="button"
onClick={() => setSimCost(val)}
className={`px-2.5 py-1 text-[10px] font-medium rounded border transition-all ${simCost === val ? 'bg-orange-500/20 text-orange-500 border-orange-500/30' : 'bg-night-900 text-slate-400 border-white/10 hover:text-white hover:border-white/20'}`}
>
฿{val}
</button>
))}
</div>
</div>
</div>
<div className="space-y-3 sm:col-span-2">
<label className="block text-xs text-slate-400 font-medium">ส่วนลดจากร้านค้า (คูปองร้านค้า 1-3)</label>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{discounts.map((discount, index) => (
<div key={index} className="flex flex-col gap-2 p-2.5 rounded-lg bg-night-900/60 border border-white/5">
<span className="text-[10px] text-slate-500 font-bold">คูปองที่ #{index + 1}</span>
<div className="flex items-center gap-2">
<div className="flex p-0.5 bg-night-950 border border-white/10 rounded-md shrink-0">
<button
type="button"
className={`px-1.5 py-0.5 text-[9px] font-medium rounded transition-all ${discount.type === 'pct' ? 'bg-orange-500/20 text-orange-500' : 'text-slate-400 hover:text-white'}`}
onClick={() => {
const updated = [...discounts]
updated[index] = { ...updated[index], type: 'pct' }
setDiscounts(updated)
}}
>
%
</button>
<button
type="button"
className={`px-1.5 py-0.5 text-[9px] font-medium rounded transition-all ${discount.type === 'amount' ? 'bg-orange-500/20 text-orange-500' : 'text-slate-400 hover:text-white'}`}
onClick={() => {
const updated = [...discounts]
updated[index] = { ...updated[index], type: 'amount' }
setDiscounts(updated)
}}
>
฿
</button>
</div>
<input
type="number"
step="0.1"
min="0"
value={discount.value || ''}
onChange={e => {
const updated = [...discounts]
updated[index] = { ...updated[index], value: parseFloat(e.target.value) || 0 }
setDiscounts(updated)
}}
placeholder="0"
className="admin-input w-full text-xs p-1"
/>
</div>
</div>
))}
</div>
</div>
</div>
{/* Simulator controls - Fees */}
<p className="text-xs text-slate-400 mb-2 font-medium">2. จำลองการปรับเปลี่ยนอัตราค่าธรรมเนียม Shopee (%):</p>
<div className="mb-6 grid grid-cols-2 sm:grid-cols-4 gap-4 p-4 rounded-xl bg-night-800/40 border border-white/5">
<div>
<label className="block text-xs text-slate-400 font-medium mb-1.5">ค่าคอมมิชชั่น (%)</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.01"
min="0"
max="100"
value={simCommPct}
onChange={e => setSimCommPct(parseFloat(e.target.value) || 0)}
className="admin-input w-full text-xs"
/>
<span className="text-xs text-slate-500">%</span>
</div>
</div>
<div>
<label className="block text-xs text-slate-400 font-medium mb-1.5">ค่าบริการ (%)</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.01"
min="0"
max="100"
value={simServicePct}
onChange={e => setSimServicePct(parseFloat(e.target.value) || 0)}
className="admin-input w-full text-xs"
/>
<span className="text-xs text-slate-500">%</span>
</div>
</div>
<div>
<label className="block text-xs text-slate-400 font-medium mb-1.5">ค่าธุรกรรม (%)</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.01"
min="0"
max="100"
value={simTransPct}
onChange={e => setSimTransPct(parseFloat(e.target.value) || 0)}
className="admin-input w-full text-xs"
/>
<span className="text-xs text-slate-500">%</span>
</div>
</div>
<div>
<label className="block text-xs text-slate-400 font-medium mb-1.5">ค่าคอม AMS (%)</label>
<div className="flex items-center gap-2">
<input
type="number"
step="0.01"
min="0"
max="100"
value={simAmsPct}
onChange={e => setSimAmsPct(parseFloat(e.target.value) || 0)}
className="admin-input w-full text-xs"
/>
<span className="text-xs text-slate-500">%</span>
</div>
</div>
</div>
{/* Side-by-side comparison tables */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* ก่อนปรับ (ค่าธรรมเนียมจากบิลตัวอย่างจริง) */}
<div className="rounded-xl bg-night-800/30 border border-white/5 p-4 space-y-4">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider border-b border-white/5 pb-2 flex justify-between">
<span>ก่อนปรับ (บิลดั้งเดิม)</span>
<span className="text-slate-500">รวม {((Math.abs(activeCase.origTotalFees) / activeCase.baseAmt) * 100).toFixed(2)}%</span>
</h3>
<div className="space-y-2 text-xs">
<div className="flex justify-between text-slate-400">
<span>ยอดรวมสินค้าก่อนลด</span>
<span className="font-mono">{fmt(activeCase.beforeDiscount)}</span>
</div>
<div className="flex justify-between text-slate-400">
<span>ส่วนลดจากร้านค้า</span>
<span className="font-mono text-rose-400">{fmt(activeCase.shopDiscount)}</span>
</div>
<div className="flex justify-between border-t border-white/5 pt-1.5 font-medium text-slate-300">
<span>ยอดสินค้าสุทธิ (ฐานคิดคอม)</span>
<span>{fmt(activeCase.baseAmt)}</span>
</div>
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าคอมมิชชั่น (Commission Fee)</span>
<span className="font-mono text-rose-400">{fmt(activeCase.origComm)}</span>
</div>
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าบริการ (Service Fee)</span>
<span className="font-mono text-rose-400">{fmt(activeCase.origService)}</span>
</div>
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าธุรกรรมการชำระเงิน</span>
<span className="font-mono text-rose-400">{fmt(activeCase.origTrans)}</span>
</div>
{activeCase.origAms !== 0 && (
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าคอมมิชชั่น AMS</span>
<span className="font-mono text-rose-400">{fmt(activeCase.origAms)}</span>
</div>
)}
<div className="flex justify-between border-t border-white/5 pt-1.5 font-medium text-slate-300">
<span>รวมค่าธรรมเนียมโดนหัก</span>
<span className="font-mono text-rose-400">{fmt(activeCase.origTotalFees)}</span>
</div>
<div className="flex justify-between border-t border-white/5 pt-2 font-bold text-white text-sm">
<span>ยอดเงินโอนเข้าบัญชีสุทธิ</span>
<span className="font-mono text-orange-500">{fmt(activeCase.originalPayout)}</span>
</div>
<div className="flex justify-between border-t border-white/5 pt-1.5 text-slate-400">
<span>- ต้นทุนสินค้า (Cost)</span>
<span className="font-mono text-rose-400">฿-{simCost.toFixed(2)}</span>
</div>
<div className="flex justify-between border-t border-white/5 pt-2 font-bold text-white text-sm">
<span>กำไรสุทธิ (Net Profit)</span>
<span className={`font-mono ${origProfit >= 0 ? 'text-green-400' : 'text-rose-400'}`}>
{origProfit >= 0 ? '' : '-'}฿{Math.abs(origProfit).toFixed(2)}
</span>
</div>
<div className="flex justify-between text-slate-400 pl-3">
<span>กำไรต่อยอดขาย (Margin %)</span>
<span className={`font-mono ${origProfit >= 0 ? 'text-green-400' : 'text-rose-400'}`}>
{origProfitMargin.toFixed(1)}%
</span>
</div>
<div className="flex justify-between text-slate-400 pl-3">
<span>กำไรต่อทุน (ROI %)</span>
<span className={`font-mono ${origProfit >= 0 ? 'text-green-400' : 'text-rose-400'}`}>
{origROI.toFixed(1)}%
</span>
</div>
</div>
</div>
{/* หลังปรับ (จำลองการปรับค่าธรรมเนียม) */}
<div className="rounded-xl bg-orange-500/5 border border-orange-500/20 p-4 space-y-4">
<h3 className="text-xs font-bold text-orange-500 uppercase tracking-wider border-b border-orange-500/10 pb-2 flex justify-between">
<span>หลังปรับ (ผลการจำลองใหม่)</span>
<span>รวม {simTotalPct.toFixed(2)}%</span>
</h3>
<div className="space-y-2 text-xs">
<div className="flex justify-between text-slate-400">
<span>ยอดรวมสินค้าก่อนลด</span>
<span className="font-mono">{fmt(simBeforeDiscount)}</span>
</div>
<div className="flex justify-between text-slate-400">
<span>ส่วนลดจากร้านค้า (รวม)</span>
<span className="font-mono text-rose-400">
-{fmt(simDiscountAmount)}
</span>
</div>
{discounts.map((d, i) => d.value > 0 ? (
<div key={i} className="flex justify-between text-[10px] text-slate-500 pl-3">
<span>- คูปอง #{i + 1} ({d.type === 'pct' ? `${d.value}%` : `฿${d.value}`})</span>
<span className="font-mono text-rose-400">
-{fmt(d.type === 'pct' ? simBeforeDiscount * (d.value / 100) : d.value)}
</span>
</div>
) : null)}
<div className="flex justify-between border-t border-orange-500/10 pt-1.5 font-medium text-slate-300">
<span>ยอดสินค้าสุทธิ (ฐานคิดคอม)</span>
<span className="font-mono text-orange-500">{fmt(baseAmt)}</span>
</div>
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าคอมมิชชั่น ({simCommPct.toFixed(2)}%)</span>
<span className="font-mono text-rose-400">฿-{simCommAmt.toFixed(2)}</span>
</div>
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าบริการ ({simServicePct.toFixed(2)}%)</span>
<span className="font-mono text-rose-400">฿-{simServiceAmt.toFixed(2)}</span>
</div>
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าธุรกรรมการชำระเงิน ({simTransPct.toFixed(2)}%)</span>
<span className="font-mono text-rose-400">฿-{simTransAmt.toFixed(2)}</span>
</div>
{simAmsPct > 0 && (
<div className="flex justify-between pl-3 text-slate-500">
<span>- ค่าคอมมิชชั่น AMS ({simAmsPct.toFixed(2)}%)</span>
<span className="font-mono text-rose-400">฿-{simAmsAmt.toFixed(2)}</span>
</div>
)}
<div className="flex justify-between border-t border-orange-500/10 pt-1.5 font-medium text-slate-300">
<span>รวมค่าธรรมเนียมโดนหัก</span>
<span className="font-mono text-rose-400">฿-{simTotalAmt.toFixed(2)}</span>
</div>
<div className="flex justify-between border-t border-orange-500/10 pt-2 font-bold text-white text-sm">
<span>ยอดเงินโอนเข้าบัญชีสุทธิ</span>
<span className="font-mono text-orange-500">฿{simPayout.toFixed(2)}</span>
</div>
<div className="flex justify-between border-t border-orange-500/10 pt-1.5 text-slate-400">
<span>- ต้นทุนสินค้า (Cost)</span>
<span className="font-mono text-rose-400">฿-{simCost.toFixed(2)}</span>
</div>
<div className="flex justify-between border-t border-orange-500/10 pt-2 font-bold text-white text-sm">
<span>กำไรสุทธิ (Net Profit)</span>
<span className={`font-mono ${simProfit >= 0 ? 'text-green-400' : 'text-rose-400'}`}>
{simProfit >= 0 ? '' : '-'}฿{Math.abs(simProfit).toFixed(2)}
</span>
</div>
<div className="flex justify-between text-slate-400 pl-3">
<span>กำไรต่อยอดขาย (Margin %)</span>
<span className={`font-mono ${simProfit >= 0 ? 'text-green-400' : 'text-rose-400'}`}>
{simProfitMargin.toFixed(1)}%
</span>
</div>
<div className="flex justify-between text-slate-400 pl-3">
<span>กำไรต่อทุน (ROI %)</span>
<span className={`font-mono ${simProfit >= 0 ? 'text-green-400' : 'text-rose-400'}`}>
{simROI.toFixed(1)}%
</span>
</div>
</div>
</div>
</div>
{/* Difference comparison status card */}
<div className={`mt-6 rounded-xl p-4 flex items-center justify-between text-xs border ${diffVal >= 0 ? 'bg-green-500/10 border-green-500/30 text-green-400' : 'bg-rose-500/10 border-rose-500/30 text-rose-400'}`}>
<div className="flex items-center gap-2">
<Info className="w-4 h-4" />
<span className="font-medium">
{diffVal >= 0
? `ยอดรับเงินเพิ่มขึ้น ฿${diffVal.toFixed(2)} จากโครงสร้างธรรมเนียมใหม่`
: `ยอดเงินโอนเข้าบัญชีสุทธิจะต่างไป ฿${diffVal.toFixed(2)} (${diffVal >= 0 ? '+' : ''}${diffPct.toFixed(1)}%) เมื่อเทียบกับบิลจริงดั้งเดิม`}
</span>
</div>
<span className="font-bold font-mono text-sm">{diffVal >= 0 ? '+' : ''}฿{diffVal.toFixed(2)}</span>
</div>
</div>
</div>
)
}
@@ -1,4 +1,4 @@
import { useState, useMemo, useEffect } from 'react'
import { useState, useMemo } from 'react'
import {
Calculator,
TrendingUp,