125 lines
4.7 KiB
TypeScript
125 lines
4.7 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
||
import api from '@/lib/api'
|
||
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'
|
||
import { DataTable } from '@/components/DataTable'
|
||
import { Badge } from '@/components/Badge'
|
||
import { MetricCard } from '@/components/MetricCard'
|
||
import { Pagination } from '@/components/Pagination'
|
||
import { useState, useMemo } from 'react'
|
||
|
||
const REVIEW_COLORS = { '达标': '#22c55e', '改善中': '#eab308', '未改善': '#ef4444' }
|
||
const PAGE_SIZE = 10
|
||
|
||
export function MonthlyReviewPage() {
|
||
const [month, setMonth] = useState('2026-05')
|
||
const [page, setPage] = useState(1)
|
||
|
||
const { data } = useQuery({
|
||
queryKey: ['monthly-review', month],
|
||
queryFn: () => api.get('/tasks/monthly-review', { params: { month } }),
|
||
})
|
||
|
||
const rd = (data as any)?.data
|
||
const summary = rd?.summary || { total: 0, passed: 0, improving: 0, failed: 0 }
|
||
const details = rd?.details || []
|
||
const pagedDetails = useMemo(() => details.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), [details, page])
|
||
|
||
const pieData = [
|
||
{ name: '达标', value: summary.passed },
|
||
{ name: '改善中', value: summary.improving },
|
||
{ name: '未改善', value: summary.failed },
|
||
].filter((d) => d.value > 0)
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-bold">月度验收</h1>
|
||
<input
|
||
type="month"
|
||
value={month}
|
||
onChange={(e) => setMonth(e.target.value)}
|
||
className="rounded-md border px-3 py-1.5 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
{/* 汇总卡片 */}
|
||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||
<MetricCard title="总任务数" value={summary.total} />
|
||
<MetricCard title="达标" value={summary.passed} />
|
||
<MetricCard title="改善中" value={summary.improving} />
|
||
<MetricCard title="未改善" value={summary.failed} />
|
||
</div>
|
||
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
{/* 饼图 */}
|
||
<div className="rounded-lg border bg-card p-4">
|
||
<h2 className="mb-3 text-sm font-bold">验收结果分布</h2>
|
||
{pieData.length > 0 ? (
|
||
<ResponsiveContainer width="100%" height={250}>
|
||
<PieChart>
|
||
<Pie data={pieData} cx="50%" cy="50%" outerRadius={80} dataKey="value"
|
||
label={({ name, value }: any) => `${name}: ${value}`}>
|
||
{pieData.map((d) => <Cell key={d.name} fill={REVIEW_COLORS[d.name as keyof typeof REVIEW_COLORS]} />)}
|
||
</Pie>
|
||
<Tooltip />
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
) : <div className="py-20 text-center text-muted-foreground">暂无验收数据</div>}
|
||
</div>
|
||
|
||
{/* 经验标准化 */}
|
||
<div className="rounded-lg border bg-card p-4">
|
||
<h2 className="mb-3 text-sm font-bold">经验标准化</h2>
|
||
<p className="text-sm text-muted-foreground mb-3">达标门店的有效经验可标准化推广</p>
|
||
<PracticeSection />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 明细表 */}
|
||
<div className="rounded-lg border bg-card p-4">
|
||
<h2 className="mb-3 text-sm font-bold">验收明细</h2>
|
||
<Pagination page={page} pageSize={PAGE_SIZE} total={details.length} onPageChange={setPage} />
|
||
<div className="mt-3">
|
||
<DataTable
|
||
columns={[
|
||
{ key: 'store_name', label: '门店' },
|
||
{ key: 'problem_indicator', label: '问题指标' },
|
||
{ key: 'baseline_value', label: '基线', align: 'right' },
|
||
{ key: 'target_value', label: '目标', align: 'right' },
|
||
{ key: 'actual_value', label: '实际', align: 'right' },
|
||
{ key: 'review_result', label: '结果', render: (r) => <Badge type="review" text={r.review_result} /> },
|
||
]}
|
||
data={pagedDetails}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PracticeSection() {
|
||
const { data } = useQuery({
|
||
queryKey: ['practices'],
|
||
queryFn: () => api.get('/tasks/practices'),
|
||
})
|
||
|
||
const practices = (data as any)?.data || []
|
||
|
||
return (
|
||
<div className="space-y-2">
|
||
{practices.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">暂无标准化经验</p>
|
||
) : practices.map((p: any) => (
|
||
<div key={p.id} className="rounded-md border p-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm font-medium">{p.practice_module}</span>
|
||
<Badge text={p.status} />
|
||
</div>
|
||
<p className="mt-1 text-xs text-muted-foreground">标杆:{p.benchmark_store_name}</p>
|
||
<p className="mt-1 text-xs">{p.key_actions}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|