风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员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,9 +2,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '@/lib/api'
import { CollapsibleSection } from '@/components/CollapsibleSection'
import { MetricCard } from '@/components/MetricCard'
import { DataTable } from '@/components/DataTable'
import { FilterableTable } from '@/components/FilterableTable'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { Pagination } from '@/components/Pagination'
import { formatCurrency, formatPercent, formatNumber, priorityColor } from '@/lib/utils'
import { useState } from 'react'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, Legend } from 'recharts'
@@ -30,7 +29,7 @@ const STATUS_LABELS: Record<string, string> = {
'cancelled': '已取消',
}
export function AdjustmentTab() {
export function AdjustmentTab({ month }: { month: string }) {
const queryClient = useQueryClient()
const [subTab, setSubTab] = useState<'diagnosis' | 'adjustment' | 'verify'>('diagnosis')
const [diagPage, setDiagPage] = useState(1)
@@ -38,15 +37,15 @@ export function AdjustmentTab() {
const [diagPriority, setDiagPriority] = useState('')
const [adjStatus, setAdjStatus] = useState('')
const [diagSort, setDiagSort] = useState('priority')
const [diagOrder, setDiagOrder] = useState('asc')
const [diagOrder, setDiagOrder] = useState<'asc' | 'desc'>('asc')
const [adjSort, setAdjSort] = useState('effective_date')
const [adjOrder, setAdjOrder] = useState('desc')
const [adjOrder, setAdjOrder] = useState<'asc' | 'desc'>('desc')
const [showForm, setShowForm] = useState(false)
const [verifyId, setVerifyId] = useState<number | null>(null)
const [formData, setFormData] = useState<any>({})
const { data: diag, isLoading: ld } = useQuery({ queryKey: ['ca/diagnosis', diagPage, diagPriority, diagSort, diagOrder], queryFn: () => api.get(`/cost-analysis/diagnosis?page=${diagPage}&page_size=${PAGE_SIZE}&sort=${diagSort}&order=${diagOrder}${diagPriority ? `&priority=${diagPriority}` : ''}`) })
const { data: adj, isLoading: la } = useQuery({ queryKey: ['ca/adjustment', adjPage, adjStatus, adjSort, adjOrder], queryFn: () => api.get(`/cost-analysis/adjustment?page=${adjPage}&page_size=${PAGE_SIZE}&sort=${adjSort}&order=${adjOrder}${adjStatus ? `&status=${adjStatus}` : ''}`) })
const { data: diag, isLoading: ld } = useQuery({ queryKey: ['ca/diagnosis', month, diagPage, diagPriority, diagSort, diagOrder], queryFn: () => api.get(`/cost-analysis/diagnosis`, { params: { month, page: diagPage, page_size: PAGE_SIZE, sort: diagSort, order: diagOrder, ...(diagPriority ? { priority: diagPriority } : {}) } }) })
const { data: adj, isLoading: la } = useQuery({ queryKey: ['ca/adjustment', month, adjPage, adjStatus, adjSort, adjOrder], queryFn: () => api.get(`/cost-analysis/adjustment`, { params: { month, page: adjPage, page_size: PAGE_SIZE, sort: adjSort, order: adjOrder, ...(adjStatus ? { status: adjStatus } : {}) } }) })
const { data: verify, isLoading: lv } = useQuery({ queryKey: ['ca/adjustment/verify', verifyId], queryFn: () => api.get(`/cost-analysis/adjustment/${verifyId}/verify`), enabled: verifyId !== null })
const generateDiagnosis = useMutation({
@@ -124,19 +123,6 @@ export function AdjustmentTab() {
<option value="P2">P2</option>
<option value="P3">P3</option>
</select>
<span className="mx-2 text-muted-foreground">|</span>
{[
{ key: 'priority', label: '优先级' },
{ key: 'cost_variance_amount', label: '成本差异' },
{ key: 'sales_amount', label: '销售额' },
{ key: 'actual_margin_pct', label: '实际毛利率' },
].map(s => (
<button key={s.key} onClick={() => { setDiagSort(s.key); setDiagPage(1) }} className={`px-3 py-1 rounded text-xs ${diagSort === s.key ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>{s.label}</button>
))}
<span className="mx-2 text-muted-foreground">|</span>
<button onClick={() => { setDiagOrder(diagOrder === 'asc' ? 'desc' : 'asc'); setDiagPage(1) }} className="px-3 py-1 rounded text-xs bg-muted">
{diagOrder === 'asc' ? '↑ 升序' : '↓ 降序'}
</button>
</div>
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
@@ -146,30 +132,42 @@ export function AdjustmentTab() {
</div>
{ld ? <LoadingSpinner text="加载诊断..." /> : (
<>
<Pagination page={diagPage} pageSize={PAGE_SIZE} total={diagMeta.total} onPageChange={setDiagPage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'dish_name', label: '菜品' },
{ key: 'category_l1', label: '品类' },
{ key: 'diagnosis_type', label: '诊断类型' },
{ key: 'diagnosis_detail', label: '诊断详情' },
{ key: 'theoretical_margin_pct', label: '理论毛利率', align: 'right', render: (r) => formatPercent(r.theoretical_margin_pct) },
{ key: 'actual_margin_pct', label: '实际毛利率', align: 'right', render: (r) => formatPercent(r.actual_margin_pct) },
{ key: 'cost_variance_amount', label: '成本差异', align: 'right', render: (r) => formatCurrency(r.cost_variance_amount) },
{ key: 'suggested_action', label: '建议动作', render: (r) => ACTION_LABELS[r.suggested_action] || r.suggested_action },
{ key: 'priority', label: '优先级', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium border ${priorityColor(r.priority)}`}>{r.priority}</span>
)},
{ key: 'action', label: '操作', render: (r) => (
<button onClick={() => handleCreateAdjustment(r)} className="text-xs text-blue-600 hover:underline"></button>
)},
]}
data={diagData}
/>
</div>
</>
<FilterableTable
data={diagData}
serverSide
page={diagPage}
onPageChange={setDiagPage}
sort={diagSort}
onSortChange={setDiagSort}
order={diagOrder}
onOrderChange={setDiagOrder}
pageSize={PAGE_SIZE}
total={diagMeta.total}
sortOptions={[
{ key: 'priority', label: '优先级' },
{ key: 'cost_variance_amount', label: '成本差异' },
{ key: 'sales_amount', label: '销售额' },
{ key: 'actual_margin_pct', label: '实际毛利率' },
]}
defaultSort="priority"
defaultOrder="asc"
columns={[
{ key: 'dish_name', label: '菜品' },
{ key: 'category_l1', label: '品类' },
{ key: 'diagnosis_type', label: '诊断类型' },
{ key: 'diagnosis_detail', label: '诊断详情' },
{ key: 'theoretical_margin_pct', label: '理论毛利率', align: 'right', render: (r) => formatPercent(r.theoretical_margin_pct) },
{ key: 'actual_margin_pct', label: '实际毛利率', align: 'right', render: (r) => formatPercent(r.actual_margin_pct) },
{ key: 'cost_variance_amount', label: '成本差异', align: 'right', render: (r) => formatCurrency(r.cost_variance_amount) },
{ key: 'suggested_action', label: '建议动作', render: (r) => ACTION_LABELS[r.suggested_action] || r.suggested_action },
{ key: 'priority', label: '优先级', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium border ${priorityColor(r.priority)}`}>{r.priority}</span>
)},
{ key: 'action', label: '操作', render: (r) => (
<button onClick={() => handleCreateAdjustment(r)} className="text-xs text-blue-600 hover:underline"></button>
)},
]}
/>
)}
{showForm && (
@@ -214,46 +212,44 @@ export function AdjustmentTab() {
<option value="completed"></option>
<option value="cancelled"></option>
</select>
<span className="mx-2 text-muted-foreground">|</span>
{[
{ key: 'effective_date', label: '执行日期' },
{ key: 'dish_name', label: '菜品名' },
{ key: 'adjustment_type', label: '调整类型' },
].map(s => (
<button key={s.key} onClick={() => { setAdjSort(s.key); setAdjPage(1) }} className={`px-3 py-1 rounded text-xs ${adjSort === s.key ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>{s.label}</button>
))}
<span className="mx-2 text-muted-foreground">|</span>
<button onClick={() => { setAdjOrder(adjOrder === 'asc' ? 'desc' : 'asc'); setAdjPage(1) }} className="px-3 py-1 rounded text-xs bg-muted">
{adjOrder === 'asc' ? '↑ 升序' : '↓ 降序'}
</button>
</div>
{la ? <LoadingSpinner text="加载调整记录..." /> : (
<>
<Pagination page={adjPage} pageSize={PAGE_SIZE} total={adjMeta.total} onPageChange={setAdjPage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'dish_name', label: '菜品' },
{ key: 'adjustment_type', label: '调整类型', render: (r) => ACTION_LABELS[r.adjustment_type] || r.adjustment_type },
{ key: 'effective_date', label: '执行日期' },
{ key: 'before_theoretical_margin', label: '调整前毛利率', align: 'right', render: (r) => formatPercent(r.before_theoretical_margin) },
{ key: 'after_target_margin', label: '目标毛利率', align: 'right', render: (r) => formatPercent(r.after_target_margin) },
{ key: 'decided_by', label: '决策人' },
{ key: 'status', label: '状态', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${statusColor(r.status)}`}>{STATUS_LABELS[r.status] || r.status}</span>
)},
{ key: 'actions', label: '操作', render: (r) => (
<div className="flex gap-2">
{r.status === 'planned' && <button onClick={() => updateStatus.mutate({ id: r.id, status: 'executing' })} className="text-xs text-blue-600 hover:underline"></button>}
{r.status === 'executing' && <button onClick={() => updateStatus.mutate({ id: r.id, status: 'completed' })} className="text-xs text-green-600 hover:underline"></button>}
<button onClick={() => { setVerifyId(r.id); setSubTab('verify') }} className="text-xs text-blue-600 hover:underline"></button>
</div>
)},
]}
data={adjData}
/>
</div>
</>
<FilterableTable
data={adjData}
serverSide
page={adjPage}
onPageChange={setAdjPage}
sort={adjSort}
onSortChange={setAdjSort}
order={adjOrder}
onOrderChange={setAdjOrder}
pageSize={PAGE_SIZE}
total={adjMeta.total}
sortOptions={[
{ key: 'effective_date', label: '执行日期' },
{ key: 'dish_name', label: '菜品名' },
{ key: 'adjustment_type', label: '调整类型' },
]}
defaultSort="effective_date"
columns={[
{ key: 'dish_name', label: '菜品' },
{ key: 'adjustment_type', label: '调整类型', render: (r) => ACTION_LABELS[r.adjustment_type] || r.adjustment_type },
{ key: 'effective_date', label: '执行日期' },
{ key: 'before_theoretical_margin', label: '调整前毛利率', align: 'right', render: (r) => formatPercent(r.before_theoretical_margin) },
{ key: 'after_target_margin', label: '目标毛利率', align: 'right', render: (r) => formatPercent(r.after_target_margin) },
{ key: 'decided_by', label: '决策人' },
{ key: 'status', label: '状态', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${statusColor(r.status)}`}>{STATUS_LABELS[r.status] || r.status}</span>
)},
{ key: 'actions', label: '操作', render: (r) => (
<div className="flex gap-2">
{r.status === 'planned' && <button onClick={() => updateStatus.mutate({ id: r.id, status: 'executing' })} className="text-xs text-blue-600 hover:underline"></button>}
{r.status === 'executing' && <button onClick={() => updateStatus.mutate({ id: r.id, status: 'completed' })} className="text-xs text-green-600 hover:underline"></button>}
<button onClick={() => { setVerifyId(r.id); setSubTab('verify') }} className="text-xs text-blue-600 hover:underline"></button>
</div>
)},
]}
/>
)}
</>
)}
@@ -291,7 +287,14 @@ export function AdjustmentTab() {
</CollapsibleSection>
<CollapsibleSection title="调整详情" defaultOpen={false}>
<DataTable
<FilterableTable
data={[verifyData.adjustment]}
sortOptions={[
{ key: 'dish_name', label: '菜品' },
{ key: 'effective_date', label: '执行日期' },
]}
defaultSort="dish_name"
defaultOrder="asc"
columns={[
{ key: 'dish_name', label: '菜品' },
{ key: 'adjustment_type', label: '调整类型', render: (r) => ACTION_LABELS[r.adjustment_type] || r.adjustment_type },
@@ -301,7 +304,6 @@ export function AdjustmentTab() {
<span className={`rounded px-2 py-0.5 text-xs font-medium ${statusColor(r.status)}`}>{STATUS_LABELS[r.status] || r.status}</span>
)},
]}
data={[verifyData.adjustment]}
/>
</CollapsibleSection>
</>