风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员TOP15按异常账单数排序, 双击跳转明细, MonthPicker靠右, 侧边栏底部间距

This commit is contained in:
freedakgmail
2026-08-01 21:28:15 +08:00
parent ac8e39e2f9
commit d724a565f5
60 changed files with 3154 additions and 2561 deletions
@@ -2,8 +2,7 @@ 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 { FilterableTable } from '@/components/FilterableTable'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
@@ -22,7 +21,7 @@ export function RentRiskTab({ month }: { month: string }) {
const [sort, setSort] = useState('rent_rate_pct')
const [filter, setFilter] = useState('')
const [riskOnly, setRiskOnly] = useState(false)
const [order, setOrder] = useState('desc')
const [order, setOrder] = useState<'asc' | 'desc'>('desc')
const { data, isLoading } = useQuery({ queryKey: ['se/rent', month, page, sort, filter, riskOnly, order], queryFn: () => api.get(`/store-expense/rent-risk?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&risk_only=${riskOnly}&order=${order}&month=${month}`) })
if (isLoading) return <LoadingSpinner text="加载房租及租约风险..." />
@@ -49,23 +48,27 @@ export function RentRiskTab({ month }: { month: string }) {
<div className="space-y-4">
<CollapsibleSection title="房租及租约风险" subtitle="租金占比 + 租约到期日,识别续租和关停风险门店">
<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>
<span className="mx-2 text-muted-foreground">|</span>
<button onClick={() => { setRiskOnly(!riskOnly); setPage(1) }} className={`px-3 py-1 rounded text-xs ${riskOnly ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
{riskOnly ? '显示全部' : '仅显示风险门店'}
</button>
</div>
<DataTable
<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="rent_rate_pct"
defaultOrder="desc"
columns={[
{ key: 'sales_store_name', label: '门店' },
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
@@ -77,9 +80,7 @@ export function RentRiskTab({ month }: { month: string }) {
{ key: 'risk_level', label: '风险等级', render: (r) => <span className={RISK_COLORS[r.risk_level] || ''}>{r.risk_level}</span> },
{ key: 'suggestion', label: '建议' },
]}
data={rows}
/>
<Pagination page={page} pageSize={meta.pageSize} total={meta.total} onPageChange={setPage} />
</CollapsibleSection>
</div>
)