4b3a48976d
新增 MonthPicker 的页面(6个): - CostPage: 成本与库存管理 - SKUPage: 商品SKU管理 - PlatformPage: 平台与优惠管理 - MemberPage: 会员与复购管理 - SiteSelectionPage: 门店选址分析 - DataQualityPage: 数据质量看板 容器+子组件添加 MonthPicker(2个页面, 11个子组件): - StoreExpensePage: 门店营业费用分析(10个子组件全部传month) - CostAnalysisPage/StoreTab: 门店成本tab 跳过(后端不支持month参数): - RiskPage: 风险管理(全量视图, 不按月过滤) - TimePage: 时间段分析(全量视图, 不按月过滤)
72 lines
4.0 KiB
TypeScript
72 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 { DataTable } from '@/components/DataTable'
|
|
import { Pagination } from '@/components/Pagination'
|
|
import { LoadingSpinner } from '@/components/LoadingSpinner'
|
|
import { formatCurrency, formatPercent } from '@/lib/utils'
|
|
|
|
const PAGE_SIZE = 20
|
|
|
|
export function FixedVariableTab({ month }: { month: string }) {
|
|
const [page, setPage] = useState(1)
|
|
const [sort, setSort] = useState('fixed_rate_pct')
|
|
const [filter, setFilter] = useState('')
|
|
const [order, setOrder] = useState('desc')
|
|
const { data, isLoading } = useQuery({ queryKey: ['se/fixed-variable', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/fixed-variable?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: '亏损门店' },
|
|
]
|
|
|
|
const sorts = [
|
|
{ key: 'fixed_rate_pct', label: '固定费用率' },
|
|
{ key: 'variable_rate_pct', label: '变动费用率' },
|
|
{ key: 'received', label: '实收' },
|
|
{ key: 'contribution_after_variable', label: '边际贡献' },
|
|
{ key: 'break_even_sales', label: '盈亏平衡额' },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<CollapsibleSection title="固定/变动费用拆分" subtitle="固定费用(租金+宿舍+维修50%) vs 变动费用(人工+水电+佣金+刷卡+维修50%)">
|
|
<div className="flex gap-2 mb-3 flex-wrap">
|
|
{filters.map(f => (
|
|
<button key={f.key} onClick={() => { setFilter(f.key); setPage(1) }} className={`px-3 py-1 rounded text-xs ${filter === f.key ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>{f.label}</button>
|
|
))}
|
|
<span className="mx-2 text-muted-foreground">|</span>
|
|
{sorts.map(s => (
|
|
<button key={s.key} onClick={() => { setSort(s.key); setPage(1) }} className={`px-3 py-1 rounded text-xs ${sort === s.key ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>按{s.label}</button>
|
|
))}
|
|
<span className="mx-2 text-muted-foreground">|</span>
|
|
<button onClick={() => { setOrder(order === 'asc' ? 'desc' : 'asc'); setPage(1) }} className="px-3 py-1 rounded text-xs bg-muted">
|
|
{order === 'asc' ? '↑ 升序' : '↓ 降序'}
|
|
</button>
|
|
</div>
|
|
<DataTable
|
|
columns={[
|
|
{ key: 'sales_store_name', label: '门店' },
|
|
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
|
|
{ key: 'fixed_expense', label: '固定费用', align: 'right', render: (r) => formatCurrency(r.fixed_expense) },
|
|
{ key: 'fixed_rate_pct', label: '固定费用率', align: 'right', render: (r) => formatPercent(r.fixed_rate_pct) },
|
|
{ key: 'variable_expense', label: '变动费用', align: 'right', render: (r) => formatCurrency(r.variable_expense) },
|
|
{ key: 'variable_rate_pct', label: '变动费用率', align: 'right', render: (r) => formatPercent(r.variable_rate_pct) },
|
|
{ key: 'contribution_after_variable', label: '边际贡献(扣变动)', align: 'right', render: (r) => <span className={parseFloat(r.contribution_after_variable) < 0 ? 'text-red-600 font-medium' : 'text-green-600'}>{formatCurrency(r.contribution_after_variable)}</span> },
|
|
{ key: 'break_even_sales', label: '盈亏平衡销售额', align: 'right', render: (r) => formatCurrency(r.break_even_sales) },
|
|
]}
|
|
data={rows}
|
|
/>
|
|
<Pagination page={page} pageSize={meta.pageSize} total={meta.total} onPageChange={setPage} />
|
|
</CollapsibleSection>
|
|
</div>
|
|
)
|
|
}
|