77 lines
4.0 KiB
TypeScript
77 lines
4.0 KiB
TypeScript
import { useState } from 'react'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import api from '@/lib/api'
|
|
import { CollapsibleSection } from '@/components/CollapsibleSection'
|
|
import { FilterableTable } from '@/components/FilterableTable'
|
|
import { LoadingSpinner } from '@/components/LoadingSpinner'
|
|
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
|
|
|
|
const PAGE_SIZE = 20
|
|
|
|
export function StoreRankingTab({ month }: { month: string }) {
|
|
const [page, setPage] = useState(1)
|
|
const [sort, setSort] = useState('operating_expense_rate_pct')
|
|
const [filter, setFilter] = useState('')
|
|
const [order, setOrder] = useState<'asc' | 'desc'>('desc')
|
|
|
|
const { data, isLoading } = useQuery({ queryKey: ['se/ranking', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/store-ranking?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
|
|
|
|
if (isLoading) return <LoadingSpinner text="加载门店费用率排名..." />
|
|
|
|
const rows = (data as any)?.data || []
|
|
const meta = (data as any)?.meta || { page, pageSize: PAGE_SIZE, total: 0 }
|
|
|
|
const filters = [
|
|
{ key: '', label: '全部' },
|
|
{ key: 'profitable', label: '盈利门店' },
|
|
{ key: 'loss', label: '亏损门店' },
|
|
{ key: 'zero_sales', label: '零销售门店' },
|
|
]
|
|
|
|
const sorts = [
|
|
{ key: 'operating_expense_rate_pct', label: '费用率' },
|
|
{ key: 'wage_rate_pct', label: '人工率' },
|
|
{ key: 'rent_rate_pct', label: '租金率' },
|
|
{ key: 'received', label: '实收' },
|
|
{ key: 'actual_store_contribution', label: '贡献利润' },
|
|
{ key: 'received_per_sqm', label: '坪效' },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<CollapsibleSection title="门店费用率排名" subtitle="按费用率降序,支持筛选和排序">
|
|
<FilterableTable
|
|
data={rows}
|
|
serverSide
|
|
page={page}
|
|
onPageChange={setPage}
|
|
sort={sort}
|
|
onSortChange={setSort}
|
|
order={order}
|
|
onOrderChange={setOrder}
|
|
pageSize={PAGE_SIZE}
|
|
total={meta.total}
|
|
filter={filter}
|
|
onFilterChange={setFilter}
|
|
filterButtons={filters}
|
|
sortOptions={sorts}
|
|
defaultSort="operating_expense_rate_pct"
|
|
defaultOrder="desc"
|
|
columns={[
|
|
{ key: 'sales_store_name', label: '门店' },
|
|
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
|
|
{ key: 'operating_expense', label: '费用合计', align: 'right', render: (r) => formatCurrency(r.operating_expense) },
|
|
{ key: 'operating_expense_rate_pct', label: '费用率', align: 'right', render: (r) => <span className={parseFloat(r.operating_expense_rate_pct) > 50 ? 'text-red-600 font-medium' : ''}>{formatPercent(r.operating_expense_rate_pct)}</span> },
|
|
{ key: 'wage_rate_pct', label: '人工率', align: 'right', render: (r) => <span className={parseFloat(r.wage_rate_pct) > 30 ? 'text-red-600' : ''}>{formatPercent(r.wage_rate_pct)}</span> },
|
|
{ key: 'rent_rate_pct', label: '租金率', align: 'right', render: (r) => <span className={parseFloat(r.rent_rate_pct) > 20 ? 'text-red-600' : ''}>{formatPercent(r.rent_rate_pct)}</span> },
|
|
{ key: 'utility_rate_pct', label: '水电率', align: 'right', render: (r) => formatPercent(r.utility_rate_pct) },
|
|
{ key: 'actual_store_contribution', label: '贡献利润', align: 'right', render: (r) => <span className={parseFloat(r.actual_store_contribution) < 0 ? 'text-red-600 font-medium' : 'text-green-600'}>{formatCurrency(r.actual_store_contribution)}</span> },
|
|
{ key: 'actual_store_contribution_rate_pct', label: '贡献率', align: 'right', render: (r) => formatPercent(r.actual_store_contribution_rate_pct) },
|
|
{ key: 'received_per_sqm', label: '坪效', align: 'right', render: (r) => formatNumber(r.received_per_sqm) },
|
|
]}
|
|
/>
|
|
</CollapsibleSection>
|
|
</div>
|
|
)
|
|
}
|