feat: 老板驾驶舱增加门店利润排名 - TOP5赚钱/BOTTOM5亏损,可点击跳转门店详情
This commit is contained in:
@@ -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() {
|
||||
</ResponsiveContainer>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* ⑤b 门店利润排名 */}
|
||||
{profitRanking.length > 0 && (
|
||||
<CollapsibleSection title="门店利润排名" subtitle="TOP5 最赚钱 · BOTTOM5 亏损最多 · 公式:实收 - 食材成本 - 经营费用">
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{/* TOP5 */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-bold text-green-600">利润 TOP5</p>
|
||||
<div className="space-y-2">
|
||||
{profitRanking.slice(0, 5).map((s: any, i: number) => (
|
||||
<div
|
||||
key={s.store_code}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-lg border border-green-200 bg-green-50/30 p-3 hover:bg-green-50/60"
|
||||
onClick={() => navigate(`/stores/${s.store_code}`)}
|
||||
>
|
||||
<span className="text-lg font-bold text-green-600">{i + 1}</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{s.store_name}</p>
|
||||
<p className="text-xs text-muted-foreground">实收 {formatCurrency(s.received)} · 净利率 {formatPercent(s.net_margin_pct)}</p>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-green-700">{formatCurrency(s.net_profit)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* BOTTOM5 */}
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-bold text-red-600">亏损 BOTTOM5</p>
|
||||
<div className="space-y-2">
|
||||
{profitRanking.filter((s: any) => Number(s.net_profit) < 0).slice(-5).reverse().map((s: any, i: number) => (
|
||||
<div
|
||||
key={s.store_code}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-lg border border-red-200 bg-red-50/30 p-3 hover:bg-red-50/60"
|
||||
onClick={() => navigate(`/stores/${s.store_code}`)}
|
||||
>
|
||||
<span className="text-lg font-bold text-red-600">{i + 1}</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{s.store_name}</p>
|
||||
<p className="text-xs text-muted-foreground">实收 {formatCurrency(s.received)} · 净利率 {formatPercent(s.net_margin_pct)}</p>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-red-700">{formatCurrency(s.net_profit)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* ⑥ 日度实收趋势 */}
|
||||
<CollapsibleSection title="日度实收趋势" subtitle={`近30天 · 环比上周 ${trendReceived > 0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`}>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user