import { useState } from 'react' 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, Target, ChevronDown, Volume2 } from 'lucide-react' import { MonthPicker } from '@/components/MonthPicker' import { KPISection } from '@/components/KPISection' import { BusinessReportDialog } from '@/components/BusinessReportDialog' const RISK_COLORS: Record = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' } function ProfitOppItem({ index, o, opp, pct, confColor }: { index: number; o: any; opp: number; pct: number; confColor: string }) { const [expanded, setExpanded] = useState(false) return (
setExpanded(!expanded)} >
{index + 1}
{o.category} {o.confidence}

基线: {typeof o.baseline === 'number' ? (o.baseline < 100 ? formatPercent(o.baseline) : formatNumber(o.baseline)) : o.baseline} {typeof o.baseline === 'number' && o.baseline < 100 ? '%' : ''} · 责任: {o.owner} · 验收: {o.evidence}

{formatCurrency(opp)}

占比 {formatPercent(pct)}

{expanded && o.detail && (
{o.detail.split('\n').map((line: string, idx: number) => { const isHeader = line.endsWith(':') || line.endsWith(':') const isAction = line.startsWith('行动指向') || /^\d)/.test(line) const isStoreLine = line.includes('费率') || line.includes('差异') || line.includes('优惠率') || line.includes('佣金') || line.includes('收入') return (

{line || '\u00A0'}

) })}
)}
) } export function BossPage() { const navigate = useNavigate() const [month, setMonth] = useState('2026-04') const [showReport, setShowReport] = useState(false) const { data: overview, isLoading: odLoading } = useQuery({ queryKey: ['overview', month], queryFn: () => api.get('/overview', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: daily, isLoading: dailyLoading } = useQuery({ queryKey: ['overview/daily', month], queryFn: () => api.get('/overview/daily', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: expenseData, isLoading: exLoading } = useQuery({ queryKey: ['store-expense/overview', month], queryFn: () => api.get('/store-expense/overview', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: waterfallData, isLoading: wfLoading } = useQuery({ queryKey: ['overview/profit-waterfall', month], queryFn: () => api.get('/overview/profit-waterfall', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: riskData, isLoading: riskLoading } = useQuery({ queryKey: ['stores/risk', month], queryFn: () => api.get('/stores/risk', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: priorityData, isLoading: priorityLoading } = useQuery({ queryKey: ['stores/priority', month], queryFn: () => api.get('/stores/priority', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: costOverview, isLoading: costLoading } = useQuery({ queryKey: ['cost-analysis/store-overview', month], queryFn: () => api.get('/cost-analysis/store-overview', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: expenseStructure, isLoading: structLoading } = useQuery({ queryKey: ['store-expense/expense-structure', month], queryFn: () => api.get('/store-expense/expense-structure', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: profitRankingData } = useQuery({ queryKey: ['overview/store-profit-ranking', month], queryFn: () => api.get('/overview/store-profit-ranking', { params: { month } }), staleTime: 5 * 60 * 1000, }) const { data: profitOppData } = useQuery({ queryKey: ['overview/profit-opportunity', month], queryFn: () => api.get('/overview/profit-opportunity', { params: { month } }), staleTime: 5 * 60 * 1000, }) 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 profitOpp = (profitOppData as any)?.data?.items || [] const totalOpportunity = profitOpp.reduce((s: number, o: any) => s + Math.max(Number(o.opportunity || 0), 0), 0) // 风险分布 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.consumption), type: 'total' }, { name: '优惠', value: -Number(wf.discount), type: 'cost' }, { name: '实收', value: Number(wf.received), type: 'subtotal' }, { 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.store_contribution), 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 === 'subtotal') { cumulative = step.value return { name: step.name, base: 0, value: step.value, fill: '#6366f1' } } 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 (
{/* 标题 */}

老板驾驶舱

经营全景 · {month}

{/* ① 核心经营指标 */}
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(ex?.total_bills)} 笔`} />
{/* ①b KPI达成率 */} {/* ② 利润瀑布 */} {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.store_contribution)}

{/* 成本费用占比汇总条 */} {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.store_contribution) / 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.store_contribution) / Number(wf.received) * 100)}(建议 ≥ 10%)
)}
)} {/* ②b 利润机会池 */} {profitOpp.length > 0 && (
{[...profitOpp].sort((a: any, b: any) => Number(b.opportunity || 0) - Number(a.opportunity || 0)).map((o: any, i: number) => { const opp = Number(o.opportunity || 0) const pct = totalOpportunity > 0 ? (opp / totalOpportunity * 100) : 0 const confColor = o.confidence === '中高' ? 'text-green-600 bg-green-50' : o.confidence === '中' ? 'text-blue-600 bg-blue-50' : 'text-yellow-600 bg-yellow-50' return ( ) })}
30天整改目标

理论月度机会合计 {formatCurrency(totalOpportunity)},考虑项目重叠和促销弹性,经营承诺值取 ≥ 150万元。 标准门店贡献率目标从 7.29% 提升至约 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.contribution_margin_pct)}

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

亏损 BOTTOM5

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

{s.store_name}

实收 {formatCurrency(s.received)} · 贡献率 {formatPercent(s.contribution_margin_pct)}

{formatCurrency(s.store_contribution)}
))}
)} {/* ⑥ 日度实收趋势 */} 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)} /> {/* 本月经营态势弹窗 */} setShowReport(false)} data={{ month, overview: od, expense: ex, waterfall: wf, riskRows, priorityRows, costOverview: costOv, profitOpp, totalOpportunity: totalOpportunity, trendReceived, }} />
) }