From 2df4f0799b1e78abb90b771df332c2c322fd94fc Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Thu, 30 Jul 2026 14:37:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=80=81=E6=9D=BF=E9=A9=BE=E9=A9=B6?= =?UTF-8?q?=E8=88=B1=E5=A2=9E=E5=8A=A0=E9=97=A8=E5=BA=97=E5=88=A9=E6=B6=A6?= =?UTF-8?q?=E6=8E=92=E5=90=8D=20-=20TOP5=E8=B5=9A=E9=92=B1/BOTTOM5?= =?UTF-8?q?=E4=BA=8F=E6=8D=9F=EF=BC=8C=E5=8F=AF=E7=82=B9=E5=87=BB=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E9=97=A8=E5=BA=97=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/BossPage.tsx | 53 +++++++++++++++++++++++++++++++++++ server/src/routes/data.ts | 25 +++++++++++++++++ 2 files changed, 78 insertions(+) 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