import { useQuery } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, LineChart, Line, PieChart, Pie, Legend, ComposedChart } from 'recharts' import api from '@/lib/api' import { MetricCard } from '@/components/MetricCard' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils' import { Crown, TrendingDown, AlertTriangle, TrendingUp, Building2, Receipt } from 'lucide-react' const RISK_COLORS: Record = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' } export function BossPage() { const navigate = useNavigate() const { data: overview, isLoading: odLoading } = useQuery({ queryKey: ['overview'], queryFn: () => api.get('/overview'), }) const { data: daily, isLoading: dailyLoading } = useQuery({ queryKey: ['overview/daily'], queryFn: () => api.get('/overview/daily'), }) const { data: expenseData, isLoading: exLoading } = useQuery({ queryKey: ['store-expense/overview'], queryFn: () => api.get('/store-expense/overview'), }) const { data: waterfallData, isLoading: wfLoading } = useQuery({ queryKey: ['overview/profit-waterfall'], queryFn: () => api.get('/overview/profit-waterfall'), }) const { data: riskData, isLoading: riskLoading } = useQuery({ queryKey: ['stores/risk'], queryFn: () => api.get('/stores/risk'), }) const { data: priorityData, isLoading: priorityLoading } = useQuery({ queryKey: ['stores/priority'], queryFn: () => api.get('/stores/priority'), }) const { data: costOverview, isLoading: costLoading } = useQuery({ queryKey: ['cost-analysis/store-overview'], queryFn: () => api.get('/cost-analysis/store-overview'), }) const { data: expenseStructure, isLoading: structLoading } = useQuery({ queryKey: ['store-expense/expense-structure'], queryFn: () => api.get('/store-expense/expense-structure'), }) const { data: profitRankingData } = useQuery({ queryKey: ['overview/store-profit-ranking'], queryFn: () => api.get('/overview/store-profit-ranking'), }) const pageLoading = odLoading || dailyLoading || exLoading || wfLoading || riskLoading || priorityLoading const od = (overview as any)?.data const ex = (expenseData as any)?.data const wf = (waterfallData as any)?.data const riskRows = (riskData as any)?.data || [] const priorityRows = (priorityData as any)?.data || [] const dailyRows = (daily as any)?.data || [] const costOv = (costOverview as any)?.data || {} const structRows = (expenseStructure as any)?.data || [] const profitRanking = (profitRankingData as any)?.data || [] // 风险分布 const riskSummary = riskRows.reduce((acc: any, r: any) => { acc[r.risk_level] = (acc[r.risk_level] || 0) + 1 return acc }, {}) const riskRevSummary = riskRows.reduce((acc: any, r: any) => { if (!acc[r.risk_level]) acc[r.risk_level] = 0 acc[r.risk_level] += Number(r.received || 0) return acc }, {}) const totalStores = riskRows.length const totalRev = Number(od?.received || 0) // P0/P1门店 const p0Stores = priorityRows.filter((s: any) => s.action_priority?.startsWith('P0')) const p1Stores = priorityRows.filter((s: any) => s.action_priority?.startsWith('P1')) const p0p1Stores = [...p0Stores, ...p1Stores] // 营收TOP5 / BOTTOM5 const sortedByRev = [...riskRows].sort((a: any, b: any) => Number(b.received || 0) - Number(a.received || 0)) const top5 = sortedByRev.slice(0, 5) const bottom5 = sortedByRev.filter((r: any) => Number(r.received) > 0).slice(-5).reverse() const rankingData = [...top5, ...bottom5].map((r: any, i: number) => ({ name: r.store_name?.substring(0, 6) || '', value: Number(r.received || 0), code: r.store_code, isTop: i < 5, })) // 利润瀑布数据 const waterfallSteps = wf ? [ { name: '实收', value: Number(wf.received), type: 'total' }, { name: '食材成本', value: -Number(wf.food_cost), type: 'cost' }, { name: '人工', value: -Number(wf.wage), type: 'cost' }, { name: '房租', value: -Number(wf.rent), type: 'cost' }, { name: '水电', value: -Number(wf.utility), type: 'cost' }, { name: '宿舍', value: -Number(wf.dorm), type: 'cost' }, { name: '外卖佣金', value: -Number(wf.commission), type: 'cost' }, { name: '其他费用', value: -Number(wf.other_expense), type: 'cost' }, { name: '净利润', value: Number(wf.net_profit), type: 'profit' }, ] : [] // 瀑布图累计计算 let cumulative = 0 const waterfallChart = waterfallSteps.map((step) => { if (step.type === 'total') { cumulative = step.value return { name: step.name, base: 0, value: step.value, fill: '#3b82f6' } } else if (step.type === 'profit') { return { name: step.name, base: 0, value: step.value, fill: '#22c55e' } } else { const decrease = Math.abs(step.value) const base = cumulative - decrease const result = { name: step.name, base, value: decrease, fill: '#ef4444' } cumulative = base return result } }) // 费用结构饼图 const pieData = structRows.map((r: any) => ({ name: r.account_name?.length > 6 ? r.account_name.substring(0, 6) + '…' : r.account_name, value: parseFloat(r.amount) })) const PIE_COLORS = ['#3b82f6', '#22c55e', '#f97316', '#eab308', '#a855f7', '#ec4899', '#06b6d4', '#64748b'] // 日度趋势 const last7 = dailyRows.slice(-7) const prev7 = dailyRows.slice(-14, -7) const sumKey = (arr: any[], key: string) => arr.reduce((s, r) => s + Number(r[key] || 0), 0) const trend = (curr: number, prev: number) => prev > 0 ? Math.round((curr - prev) / prev * 1000) / 10 : 0 const trendReceived = trend(sumKey(last7, 'received'), sumKey(prev7, 'received')) const trendBills = trend(sumKey(last7, 'bill_count'), sumKey(prev7, 'bill_count')) // 成本超耗TOP5 const costRed = Number(costOv.red_count || 0) const costOrange = Number(costOv.orange_count || 0) const costGreen = Number(costOv.green_count || 0) if (pageLoading) { return } return (
{/* 标题 */}

老板驾驶舱

经营全景 · 2026年4月

{/* ① 核心经营指标 */}
0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`} /> 0 ? sumKey(last7, 'received') / sumKey(last7, 'bill_count') : 0), (prev7.length > 0 ? sumKey(prev7, 'received') / sumKey(prev7, 'bill_count') : 0))} description={`账单 ${formatNumber(od?.bill_count)} 笔 · 公式:实收 ÷ 账单数`} />
{/* ② 利润瀑布 */} {wf && ( v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> label} formatter={(v: any) => [formatCurrency(v), '金额']} /> { const { x, y, width, height, payload, fill } = props if (!payload.value || payload.value === 0) return const scale = height / payload.value const newY = y - payload.base * scale return }}> {waterfallChart.map((entry, i) => )}
{waterfallSteps.filter(s => s.type === 'cost').map(s => (

{s.name}

{formatCurrency(Math.abs(s.value))}

))}

净利润

{formatCurrency(wf.net_profit)}

{/* 成本费用占比汇总条 */} {wf && (

实收分配(占实收比例)

食材{formatPercent(Number(wf.food_cost) / Number(wf.received) * 100)}
人工{formatPercent(Number(wf.wage) / Number(wf.received) * 100)}
房租{formatPercent(Number(wf.rent) / Number(wf.received) * 100)}
水电{formatPercent(Number(wf.utility) / Number(wf.received) * 100)}
其他{formatPercent((Number(wf.dorm) + Number(wf.commission) + Number(wf.other_expense)) / Number(wf.received) * 100)}
利润{formatPercent(Number(wf.net_profit) / Number(wf.received) * 100)}
食材成本率 {formatPercent(Number(wf.food_cost) / Number(wf.received) * 100)}(建议 ≤ 32%) 费用率 {formatPercent(Number(wf.total_expense) / Number(wf.received) * 100)}(建议 ≤ 40%) 净利率 {formatPercent(Number(wf.net_profit) / Number(wf.received) * 100)}(建议 ≥ 10%)
)}
)} {/* ③ 风险态势 */}
15 ? 'border-red-200 bg-red-50/40' : 'border-yellow-200 bg-yellow-50/40'}`}>
红色门店

{riskSummary['红色'] || 0}

实收 {formatCurrency(riskRevSummary['红色'])} · 占比 {totalRev > 0 ? formatPercent(riskRevSummary['红色'] / totalRev * 100) : '-'}

黄色门店

{riskSummary['黄色'] || 0}

实收 {formatCurrency(riskRevSummary['黄色'])} · 占比 {totalRev > 0 ? formatPercent(riskRevSummary['黄色'] / totalRev * 100) : '-'}

绿色门店

{riskSummary['绿色'] || 0}

实收 {formatCurrency(riskRevSummary['绿色'])} · 占比 {totalRev > 0 ? formatPercent(riskRevSummary['绿色'] / totalRev * 100) : '-'}

{/* P0/P1 门店清单 */} {p0p1Stores.length > 0 && (
{p0p1Stores.map((s: any) => ( navigate(`/stores/${s.store_code}`)} > ))}
门店 实收 优先级 风险 问题
{s.store_name} {formatCurrency(s.received)} {s.action_priority} {s.risk_level} {s.problem_combination || '-'}
)} {/* ④ 成本异常 + 费用结构 */}

严重超耗

{costRed}

明显超耗

{costOrange}

基本正常

{costGreen}

总差异金额

{formatCurrency(costOv.total_variance)}

公式:实际食材成本 - 理论食材成本。正值表示超耗

{!structLoading && pieData.length > 0 && ( `${e.name}`}> {pieData.map((_: any, i: number) => )} formatCurrency(v)} /> )}
{/* ⑤ 门店营收排名 */} v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> formatCurrency(v)} /> d.code && navigate(`/stores/${d.code}`)}> {rankingData.map((entry, i) => ( ))} {/* ⑤b 门店利润排名 */} {profitRanking.length > 0 && (
{/* TOP5 */}

利润 TOP5

{profitRanking.slice(0, 5).map((s: any, i: number) => (
navigate(`/stores/${s.store_code}`)} > {i + 1}

{s.store_name}

实收 {formatCurrency(s.received)} · 净利率 {formatPercent(s.net_margin_pct)}

{formatCurrency(s.net_profit)}
))}
{/* BOTTOM5 */}

亏损 BOTTOM5

{profitRanking.filter((s: any) => Number(s.net_profit) < 0).slice(-5).reverse().map((s: any, i: number) => (
navigate(`/stores/${s.store_code}`)} > {i + 1}

{s.store_name}

实收 {formatCurrency(s.received)} · 净利率 {formatPercent(s.net_margin_pct)}

{formatCurrency(s.net_profit)}
))}
)} {/* ⑥ 日度实收趋势 */} 0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`}> v?.substring(5, 10)} tick={{ fontSize: 10 }} /> v >= 10000 ? `${(v / 10000).toFixed(0)}万` : v} tick={{ fontSize: 10 }} /> v?.substring(0, 10)} formatter={(v: any) => formatCurrency(v)} />
) }