x
This commit is contained in:
1 parent
85a0f984eb
commit
e25d998893
5 files changed
+938
-44
No files matched your search
@@ -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 />} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user