diff --git a/client/src/App.tsx b/client/src/App.tsx index 9f72deb..b8f7184 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,6 +13,7 @@ import { StorePage } from '@/pages/StorePage' import { SKUPage } from '@/pages/SKUPage' import { CostPage } from '@/pages/CostPage' import { CostAnalysisPage } from '@/pages/CostAnalysisPage' +import { StoreExpensePage } from '@/pages/StoreExpensePage' import { PlatformPage } from '@/pages/PlatformPage' import { MemberPage } from '@/pages/MemberPage' import { RiskPage } from '@/pages/RiskPage' @@ -73,6 +74,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index 26a08a7..ea98b56 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,6 +1,6 @@ import { ReactNode, useState } from 'react' import { Link, useLocation } from 'react-router-dom' -import { LayoutDashboard, Store, ClipboardList, TrendingUp, Settings, LogOut, Menu, X, Package, DollarSign, ShoppingBag, Users, AlertTriangle, Clock, Database, MapPin, PieChart } from 'lucide-react' +import { LayoutDashboard, Store, ClipboardList, TrendingUp, Settings, LogOut, Menu, X, Package, DollarSign, ShoppingBag, Users, AlertTriangle, Clock, Database, MapPin, PieChart, Wallet } from 'lucide-react' import { cn } from '@/lib/utils' interface LayoutProps { @@ -38,6 +38,7 @@ const menuGroups: MenuGroup[] = [ { path: '/sku', label: '商品SKU', icon: Package, roles: ['hq', 'dept'] }, { path: '/cost', label: '成本库存', icon: DollarSign, roles: ['hq', 'dept'] }, { path: '/cost-analysis', label: '菜品成本分析', icon: PieChart, roles: ['hq', 'dept'] }, + { path: '/store-expense', label: '门店费用分析', icon: Wallet, roles: ['hq', 'dept'] }, { path: '/platform', label: '平台优惠', icon: ShoppingBag, roles: ['hq', 'dept'] }, { path: '/member', label: '会员复购', icon: Users, roles: ['hq', 'dept'] }, { path: '/risk', label: '风险内控', icon: AlertTriangle, roles: ['hq', 'dept'] }, diff --git a/client/src/components/cost-analysis/AdjustmentTab.tsx b/client/src/components/cost-analysis/AdjustmentTab.tsx index 16e0ab1..30a82ba 100644 --- a/client/src/components/cost-analysis/AdjustmentTab.tsx +++ b/client/src/components/cost-analysis/AdjustmentTab.tsx @@ -37,12 +37,16 @@ export function AdjustmentTab() { const [adjPage, setAdjPage] = useState(1) const [diagPriority, setDiagPriority] = useState('') const [adjStatus, setAdjStatus] = useState('') + const [diagSort, setDiagSort] = useState('priority') + const [diagOrder, setDiagOrder] = useState('asc') + const [adjSort, setAdjSort] = useState('effective_date') + const [adjOrder, setAdjOrder] = useState('desc') const [showForm, setShowForm] = useState(false) const [verifyId, setVerifyId] = useState(null) const [formData, setFormData] = useState({}) - const { data: diag, isLoading: ld } = useQuery({ queryKey: ['ca/diagnosis', diagPage, diagPriority], queryFn: () => api.get(`/cost-analysis/diagnosis?page=${diagPage}&page_size=${PAGE_SIZE}${diagPriority ? `&priority=${diagPriority}` : ''}`) }) - const { data: adj, isLoading: la } = useQuery({ queryKey: ['ca/adjustment', adjPage, adjStatus], queryFn: () => api.get(`/cost-analysis/adjustment?page=${adjPage}&page_size=${PAGE_SIZE}${adjStatus ? `&status=${adjStatus}` : ''}`) }) + const { data: diag, isLoading: ld } = useQuery({ queryKey: ['ca/diagnosis', diagPage, diagPriority, diagSort, diagOrder], queryFn: () => api.get(`/cost-analysis/diagnosis?page=${diagPage}&page_size=${PAGE_SIZE}&sort=${diagSort}&order=${diagOrder}${diagPriority ? `&priority=${diagPriority}` : ''}`) }) + const { data: adj, isLoading: la } = useQuery({ queryKey: ['ca/adjustment', adjPage, adjStatus, adjSort, adjOrder], queryFn: () => api.get(`/cost-analysis/adjustment?page=${adjPage}&page_size=${PAGE_SIZE}&sort=${adjSort}&order=${adjOrder}${adjStatus ? `&status=${adjStatus}` : ''}`) }) const { data: verify, isLoading: lv } = useQuery({ queryKey: ['ca/adjustment/verify', verifyId], queryFn: () => api.get(`/cost-analysis/adjustment/${verifyId}/verify`), enabled: verifyId !== null }) const generateDiagnosis = useMutation({ @@ -120,6 +124,19 @@ export function AdjustmentTab() { + | + {[ + { key: 'priority', label: '优先级' }, + { key: 'cost_variance_amount', label: '成本差异' }, + { key: 'sales_amount', label: '销售额' }, + { key: 'actual_margin_pct', label: '实际毛利率' }, + ].map(s => ( + + ))} + | +
@@ -189,7 +206,7 @@ export function AdjustmentTab() { {subTab === 'adjustment' && ( <> -
+
+ | + {[ + { key: 'effective_date', label: '执行日期' }, + { key: 'dish_name', label: '菜品名' }, + { key: 'adjustment_type', label: '调整类型' }, + ].map(s => ( + + ))} + | +
{la ? : ( <> diff --git a/client/src/components/cost-analysis/BomTab.tsx b/client/src/components/cost-analysis/BomTab.tsx index 15689c9..67dc5a6 100644 --- a/client/src/components/cost-analysis/BomTab.tsx +++ b/client/src/components/cost-analysis/BomTab.tsx @@ -15,11 +15,16 @@ export function BomTab() { const [complexPage, setComplexPage] = useState(1) const [missingPage, setMissingPage] = useState(1) const [selectedSku, setSelectedSku] = useState('') + const [complexSort, setComplexSort] = useState('material_count') + const [complexOrder, setComplexOrder] = useState('desc') + const [complexFilter, setComplexFilter] = useState('') + const [missSort, setMissSort] = useState('sales_amount') + const [missOrder, setMissOrder] = useState('desc') const { data: bomOv, isLoading: l1 } = useQuery({ queryKey: ['ca/bom-overview'], queryFn: () => api.get('/cost-analysis/bom-overview') }) - const { data: complexity, isLoading: l2 } = useQuery({ queryKey: ['ca/bom-complexity', complexPage], queryFn: () => api.get(`/cost-analysis/bom-complexity?page=${complexPage}&page_size=${PAGE_SIZE}`) }) + const { data: complexity, isLoading: l2 } = useQuery({ queryKey: ['ca/bom-complexity', complexPage, complexSort, complexOrder, complexFilter], queryFn: () => api.get(`/cost-analysis/bom-complexity?page=${complexPage}&page_size=${PAGE_SIZE}&sort=${complexSort}&order=${complexOrder}&filter=${complexFilter}`) }) const { data: highLoss, isLoading: l3 } = useQuery({ queryKey: ['ca/bom-high-loss'], queryFn: () => api.get('/cost-analysis/bom-high-loss?threshold=20') }) - const { data: missing, isLoading: l4 } = useQuery({ queryKey: ['ca/bom-missing', missingPage], queryFn: () => api.get(`/cost-analysis/bom-missing?page=${missingPage}&page_size=${PAGE_SIZE}`) }) + const { data: missing, isLoading: l4 } = useQuery({ queryKey: ['ca/bom-missing', missingPage, missSort, missOrder], queryFn: () => api.get(`/cost-analysis/bom-missing?page=${missingPage}&page_size=${PAGE_SIZE}&sort=${missSort}&order=${missOrder}`) }) const { data: composition, isLoading: l5 } = useQuery({ queryKey: ['ca/bom-composition', selectedSku], queryFn: () => api.get(`/cost-analysis/bom-composition?sku_code=${selectedSku}`), @@ -79,7 +84,28 @@ export function BomTab() { - + +
+ {[ + { key: '', label: '全部' }, + { key: 'high_loss', label: '含高损耗' }, + { key: 'unique_heavy', label: '独有原料多' }, + ].map(f => ( + + ))} + | + {[ + { key: 'material_count', label: '原料数' }, + { key: 'unique_material_count', label: '独有原料' }, + { key: 'high_loss_count', label: '高损耗' }, + ].map(s => ( + + ))} + | + +
+
+ {[ + { key: 'sales_amount', label: '销售额' }, + { key: 'sales_quantity', label: '销量' }, + { key: 'standard_name', label: '菜品名' }, + ].map(s => ( + + ))} + | + +
{ + const av = parseFloat(a[topSort]) || 0 + const bv = parseFloat(b[topSort]) || 0 + return topOrder === 'asc' ? av - bv : bv - av + }) const typeData = (typeLoss as any)?.data || [] const reasonColor = (reason: string) => { @@ -129,8 +136,22 @@ export function MaterialTab() {
- - + +
+ {[ + { key: 'total_loss_qty', label: '总损耗量' }, + { key: 'avg_loss_rate', label: '平均损耗率' }, + { key: 'total_loss_amount', label: '损耗金额' }, + { key: 'dish_count', label: '涉及菜品数' }, + ].map(s => ( + + ))} + | + +
+
formatPercent(r.max_loss_rate) }, { key: 'total_loss_amount', label: '损耗金额', align: 'right', render: (r) => formatCurrency(r.total_loss_amount) }, ]} - data={topData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)} + data={sortedTopData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)} />
diff --git a/client/src/components/cost-analysis/PackagingTab.tsx b/client/src/components/cost-analysis/PackagingTab.tsx index 6a6058b..a7cebb3 100644 --- a/client/src/components/cost-analysis/PackagingTab.tsx +++ b/client/src/components/cost-analysis/PackagingTab.tsx @@ -12,9 +12,12 @@ const PAGE_SIZE = 15 export function PackagingTab() { const [page, setPage] = useState(1) + const [sort, setSort] = useState('total_cost') + const [order, setOrder] = useState('desc') + const [filter, setFilter] = useState('') const { data: ov, isLoading: l1 } = useQuery({ queryKey: ['ca/packaging-overview'], queryFn: () => api.get('/cost-analysis/packaging-overview') }) - const { data: detail, isLoading: l2 } = useQuery({ queryKey: ['ca/packaging-detail', page], queryFn: () => api.get(`/cost-analysis/packaging-detail?page=${page}&page_size=${PAGE_SIZE}`) }) + const { data: detail, isLoading: l2 } = useQuery({ queryKey: ['ca/packaging-detail', page, sort, order, filter], queryFn: () => api.get(`/cost-analysis/packaging-detail?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&order=${order}&filter=${filter}`) }) if (l1 || l2) return @@ -38,6 +41,28 @@ export function PackagingTab() {
+
+ {[ + { key: '', label: '全部' }, + { key: 'high_loss', label: '高损耗' }, + { key: 'normal', label: '正常' }, + ].map(f => ( + + ))} + | + {[ + { key: 'total_cost', label: '总成本' }, + { key: 'loss_qty', label: '损耗量' }, + { key: 'avg_loss_rate', label: '损耗率' }, + { key: 'dish_count', label: '涉及菜品数' }, + ].map(s => ( + + ))} + | + +
= { export function ProfitabilityTab() { const [page, setPage] = useState(1) const [category, setCategory] = useState('') + const [sort, setSort] = useState('sales_amount') + const [order, setOrder] = useState('desc') + const [filter, setFilter] = useState('') const { data: matrix, isLoading: l1 } = useQuery({ queryKey: ['ca/menu-engineering'], queryFn: () => api.get('/cost-analysis/menu-engineering') }) - const { data: profit, isLoading: l2 } = useQuery({ queryKey: ['ca/profitability', page, category], queryFn: () => api.get(`/cost-analysis/profitability?page=${page}&page_size=${PAGE_SIZE}${category ? `&category=${category}` : ''}`) }) + const { data: profit, isLoading: l2 } = useQuery({ queryKey: ['ca/profitability', page, category, sort, order, filter], queryFn: () => api.get(`/cost-analysis/profitability?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&order=${order}&filter=${filter}${category ? `&category=${category}` : ''}`) }) const { data: pricing, isLoading: l3 } = useQuery({ queryKey: ['ca/pricing'], queryFn: () => api.get('/cost-analysis/pricing?threshold=50') }) if (l1 || l2 || l3) return @@ -81,9 +84,32 @@ export function ProfitabilityTab() {
- -
- + +
+ {[ + { key: '', label: '全部' }, + { key: 'profitable', label: '盈利菜品' }, + { key: 'loss', label: '亏损菜品' }, + { key: 'high_variance', label: '成本超耗' }, + ].map(f => ( + + ))} + | + {[ + { key: 'sales_amount', label: '销售额' }, + { key: 'sales_quantity', label: '销量' }, + { key: 'actual_margin_rate_pct', label: '实际毛利率' }, + { key: 'theoretical_margin_rate_pct', label: '理论毛利率' }, + { key: 'cost_variance_amount', label: '成本差异' }, + ].map(s => ( + + ))} + | + + | +