风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员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 { MetricCard } from '@/components/MetricCard'
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
@@ -189,7 +188,7 @@ export function LossDiagnosisTab({ month }: { month: string }) {
const [page, setPage] = useState(1)
const [sort, setSort] = useState('actual_store_contribution')
const [filter, setFilter] = useState('')
const [order, setOrder] = useState('asc')
const [order, setOrder] = useState<'asc' | 'desc'>('asc')
const [selectedStore, setSelectedStore] = useState<StoreDetail | null>(null)
const { data, isLoading } = useQuery({ queryKey: ['se/loss-diagnosis', month, page, sort, filter, order], queryFn: () => api.get(`/store-expense/loss-diagnosis?page=${page}&page_size=${PAGE_SIZE}&sort=${sort}&filter=${filter}&order=${order}&month=${month}`) })
@@ -212,32 +211,36 @@ export function LossDiagnosisTab({ month }: { month: string }) {
</div>
<CollapsibleSection title="亏损门店诊断" subtitle="点击门店名称查看详情">
<div className="flex gap-2 mb-3 flex-wrap">
{[
<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={[
{ key: '', label: '全部' },
{ key: 'P0', label: 'P0 紧急' },
{ key: 'P1', label: 'P1 整改' },
{ key: 'P2', label: 'P2 观察' },
].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>
{[
]}
sortOptions={[
{ key: 'actual_store_contribution', label: '贡献利润' },
{ key: 'actual_store_contribution_rate_pct', label: '贡献率' },
{ key: 'received', label: '实收' },
{ key: 'wage_rate_pct', label: '人工率' },
{ key: 'rent_rate_pct', label: '租金率' },
{ key: 'received_per_sqm', label: '坪效' },
].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
]}
defaultSort="actual_store_contribution"
defaultOrder="asc"
onRowClick={(r) => setSelectedStore(r as StoreDetail)}
columns={[
{ key: 'sales_store_name', label: '门店', render: (r) => <button className="text-blue-600 hover:underline font-medium" onClick={() => setSelectedStore(r as StoreDetail)}>{r.sales_store_name}</button> },
{ key: 'priority', label: '优先级', render: (r) => <span className={`px-2 py-0.5 rounded text-xs font-medium ${PRIORITY_COLORS[r.priority] || ''}`}>{r.priority}</span> },
@@ -251,9 +254,7 @@ export function LossDiagnosisTab({ month }: { month: string }) {
{ key: 'diagnosis_reasons', label: '亏损原因', render: (r) => <span className="text-xs">{r.diagnosis_reasons}</span> },
{ key: 'suggested_actions', label: '建议动作', render: (r) => <span className="text-xs font-medium">{r.suggested_actions}</span> },
]}
data={rows}
/>
<Pagination page={page} pageSize={meta.pageSize} total={meta.total} onPageChange={setPage} />
</CollapsibleSection>
<StoreDetailModal store={selectedStore} onClose={() => setSelectedStore(null)} />