diff --git a/client/src/pages/BossPage.tsx b/client/src/pages/BossPage.tsx index 9c3bbed..dbc435f 100644 --- a/client/src/pages/BossPage.tsx +++ b/client/src/pages/BossPage.tsx @@ -45,6 +45,10 @@ export function BossPage() { 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 @@ -56,6 +60,7 @@ export function BossPage() { 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) => { @@ -351,6 +356,54 @@ export function BossPage() { + {/* ⑤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)}%`}> diff --git a/server/src/routes/data.ts b/server/src/routes/data.ts index 718e6f4..1b3557e 100644 --- a/server/src/routes/data.ts +++ b/server/src/routes/data.ts @@ -539,4 +539,29 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => { } }) +// 门店利润排名 +router.get('/overview/store-profit-ranking', async (req: AuthRequest, res) => { + try { + const result = await query(` + SELECT + r.store_code, + r.store_name, + round(r.received::numeric, 2) AS received, + round(COALESCE(e.actual_food_cost, 0)::numeric, 2) AS food_cost, + round(COALESCE(e.operating_expense, 0)::numeric, 2) AS expense, + round((r.received - COALESCE(e.actual_food_cost, 0) - COALESCE(e.operating_expense, 0))::numeric, 2) AS net_profit, + round((r.received - COALESCE(e.actual_food_cost, 0) - COALESCE(e.operating_expense, 0)) / nullif(r.received, 0) * 100, 2) AS net_margin_pct, + r.risk_level + FROM analytics.mv_store_risk_rating r + LEFT JOIN analytics.mv_store_operating_expense_monthly e + ON r.store_code = e.sales_store_code AND e.report_month = DATE '2026-04-01' + WHERE r.received IS NOT NULL AND r.received > 0 + ORDER BY net_profit DESC + `) + sendSuccess(res, result.rows) + } catch (err: any) { + sendError(res, err.message) + } +}) + export default router