72 lines
4.0 KiB
TypeScript
72 lines
4.0 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import api from '@/lib/api'
|
|
import { CollapsibleSection } from '@/components/CollapsibleSection'
|
|
import { MetricCard } from '@/components/MetricCard'
|
|
import { LoadingSpinner } from '@/components/LoadingSpinner'
|
|
import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils'
|
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'
|
|
|
|
const PIE_COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#f97316', '#eab308', '#8b5cf6', '#06b6d4', '#ec4899', '#84cc16', '#a3a3a3', '#f43f5e', '#6366f1', '#14b8a6', '#facc15', '#7c3aed', '#10b981', '#fb923c']
|
|
|
|
export function ExpenseOverviewTab({ month }: { month: string }) {
|
|
const { data: overview, isLoading: l1 } = useQuery({ queryKey: ['se/overview', month], queryFn: () => api.get('/store-expense/overview', { params: { month } }) })
|
|
const { data: structure, isLoading: l2 } = useQuery({ queryKey: ['se/structure', month], queryFn: () => api.get('/store-expense/expense-structure', { params: { month } }) })
|
|
|
|
if (l1 || l2) return <LoadingSpinner text="加载费用总览..." />
|
|
|
|
const ov = (overview as any)?.data || {}
|
|
const structureData = (structure as any)?.data || []
|
|
|
|
const pieData = structureData.map((r: any) => ({ name: r.account_name, value: parseFloat(r.amount) }))
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
<MetricCard title="经营门店" value={ov.total_stores} unit="家" description={`${ov.profitable_stores}家盈利 / ${ov.loss_stores}家亏损`} />
|
|
<MetricCard title="实收合计" value={ov.total_received} format="currency" description={`费用合计 ${formatCurrency(ov.total_expense)}`} />
|
|
<MetricCard title="门店贡献利润" value={ov.total_contribution} format="currency" description={`贡献率 ${formatPercent(ov.overall_contribution_rate_pct)}`} />
|
|
<MetricCard title="费用率" value={ov.overall_expense_rate_pct} format="percent" description={`人工 ${formatPercent(ov.overall_wage_rate_pct)} · 房租 ${formatPercent(ov.overall_rent_rate_pct)}`} />
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<CollapsibleSection title="费用结构分析" subtitle="2026年4月各费用科目占比">
|
|
<ResponsiveContainer width="100%" height={400}>
|
|
<PieChart>
|
|
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={120} labelLine={false}>
|
|
{pieData.map((_: any, i: number) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
|
|
</Pie>
|
|
<Tooltip formatter={(v: any, name: any) => [formatCurrency(v), name]} />
|
|
<Legend wrapperStyle={{ fontSize: 11 }} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</CollapsibleSection>
|
|
|
|
<CollapsibleSection title="费用科目明细" subtitle="按金额降序">
|
|
<div className="overflow-auto max-h-[400px]">
|
|
<table className="w-full text-xs">
|
|
<thead className="sticky top-0 bg-muted">
|
|
<tr>
|
|
<th className="text-left p-2">费用科目</th>
|
|
<th className="text-right p-2">金额</th>
|
|
<th className="text-right p-2">占比</th>
|
|
<th className="text-right p-2">门店数</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{structureData.map((r: any, i: number) => (
|
|
<tr key={i} className="border-b hover:bg-muted/50">
|
|
<td className="p-2">{r.account_name}</td>
|
|
<td className="text-right p-2">{formatCurrency(r.amount)}</td>
|
|
<td className="text-right p-2">{formatPercent(r.expense_share_pct)}</td>
|
|
<td className="text-right p-2">{r.nonzero_cost_unit_count}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CollapsibleSection>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|