import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import api from '@/lib/api' import { FilterableTable } from '@/components/FilterableTable' import { LoadingSpinner } from '@/components/LoadingSpinner' import { CollapsibleSection } from '@/components/CollapsibleSection' import { MetricCard } from '@/components/MetricCard' import { Tabs } from '@/components/Tabs' import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils' import { useState } from 'react' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts' import { MonthPicker } from '@/components/MonthPicker' const GRADE_COLORS: Record = { A: '#22c55e', B: '#3b82f6', C: '#eab308', D: '#ef4444' } export function TargetManagementPage() { const [tab, setTab] = useState('overview') const [month, setMonth] = useState('2026-04') const year = parseInt(month.split('-')[0]) const queryClient = useQueryClient() const { data: annualData, isLoading: annualLoading } = useQuery({ queryKey: ['target/annual', year], queryFn: () => api.get('/target/annual', { params: { year } }), }) const { data: regionalData, isLoading: regionalLoading } = useQuery({ queryKey: ['target/regional', year], queryFn: () => api.get('/target/regional', { params: { year } }), }) const { data: storeMonthlyData, isLoading: storeLoading } = useQuery({ queryKey: ['target/store-monthly', month], queryFn: () => api.get('/target/store-monthly', { params: { month: month + '-01' } }), }) const { data: achievementData, isLoading: achievementLoading } = useQuery({ queryKey: ['target/achievement', month], queryFn: () => api.get('/target/achievement', { params: { month: month + '-01' } }), }) const decomposeMutation = useMutation({ mutationFn: (data: { year: number; month?: string }) => api.post('/target/decompose/annual-to-store', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['target/store-monthly'] }) queryClient.invalidateQueries({ queryKey: ['target/achievement'] }) }, }) const dailyDecomposeMutation = useMutation({ mutationFn: (data: { month: string }) => api.post('/target/decompose/monthly-to-daily', data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['target/daily'] }) }, }) const annualRows = (annualData as any)?.data || [] const regionalRows = (regionalData as any)?.data || [] const storeRows = (storeMonthlyData as any)?.data || [] const achievementRows = (achievementData as any)?.data || [] const revenueTarget = annualRows.find((r: any) => r.metric === 'revenue') const profitTarget = annualRows.find((r: any) => r.metric === 'profit') const storeCountTarget = annualRows.find((r: any) => r.metric === 'store_count') const memberCountTarget = annualRows.find((r: any) => r.metric === 'member_count') const totalStoreTarget = storeRows.reduce((s: number, r: any) => s + Number(r.revenue_target || 0), 0) const avgAchievement = achievementRows.length > 0 ? achievementRows.reduce((s: number, r: any) => s + Number(r.achievement_rate || 0), 0) / achievementRows.length : 0 const achievedCount = achievementRows.filter((r: any) => Number(r.achievement_rate || 0) >= 100).length const lowAchievementCount = achievementRows.filter((r: any) => Number(r.achievement_rate || 0) < 80).length const gradeDistribution = storeRows.reduce((acc: Record, r: any) => { const g = r.grade || 'B' acc[g] = (acc[g] || 0) + 1 return acc }, {}) const gradePieData = Object.entries(gradeDistribution).map(([name, value]) => ({ name: `${name}级`, value })) const achievementChartData = achievementRows.slice(0, 20).map((r: any) => ({ name: r.store_name?.length > 6 ? r.store_name.slice(0, 6) : r.store_name, 达成率: Number(r.achievement_rate || 0), })) const pageLoading = annualLoading || regionalLoading || storeLoading || achievementLoading if (pageLoading && tab === 'overview') { return } return (

目标管理

年度战略目标 · 区域拆解 · 门店月度 · 达成率追踪

{tab === 'overview' && ( <>
= 90 ? 'good' : avgAchievement >= 70 ? 'warn' : 'bad'} />
0 ? 'bad' : 'good'} />
`${v}%`} tick={{ fontSize: 10 }} /> [`${Number(v).toFixed(1)}%`, '达成率']} /> {`${name}: ${value}`}}> {gradePieData.map((entry, i) => )} [formatNumber(v), '门店数']} />
{decomposeMutation.data && ( 拆解完成: {(decomposeMutation.data as any)?.data?.stores || 0}家门店 )}
)} {tab === 'annual' && ( { const labels: Record = { revenue: '营收', profit: '利润', store_count: '门店数', member_count: '会员数' } return labels[r.metric] || r.metric }}, { key: 'target_value', label: '目标值', align: 'right', render: (r) => { if (r.metric === 'revenue' || r.metric === 'profit') return formatCurrency(r.target_value) return formatNumber(r.target_value) }}, { key: 'description', label: '说明' }, { key: 'created_by', label: '创建人' }, ]} /> )} {tab === 'regional' && ( formatCurrency(r.revenue_target) }, { key: 'profit_target', label: '利润目标', align: 'right', render: (r) => formatCurrency(r.profit_target) }, { key: 'store_count_target', label: '门店数目标', align: 'right', render: (r) => formatNumber(r.store_count_target) }, { key: 'member_count_target', label: '会员数目标', align: 'right', render: (r) => formatNumber(r.member_count_target) }, { key: 'weight_pct', label: '权重', align: 'right', render: (r) => formatPercent(r.weight_pct) }, ]} /> )} {tab === 'store' && ( { const colors: Record = { A: 'text-green-600', B: 'text-blue-600', C: 'text-yellow-600', D: 'text-red-600' } return {r.grade} }}, { key: 'revenue_target', label: '营收目标', align: 'right', render: (r) => formatCurrency(r.revenue_target) }, { key: 'bill_count_target', label: '账单数目标', align: 'right', render: (r) => formatNumber(r.bill_count_target) }, { key: 'avg_bill_value_target', label: '客单价目标', align: 'right', render: (r) => formatCurrency(r.avg_bill_value_target) }, { key: 'member_penetration_target', label: '会员渗透率', align: 'right', render: (r) => formatPercent(r.member_penetration_target) }, { key: 'cost_rate_target', label: '成本率', align: 'right', render: (r) => formatPercent(r.cost_rate_target) }, ]} /> )} {tab === 'achievement' && ( {achievementLoading ? : ( { const colors: Record = { A: 'text-green-600', B: 'text-blue-600', C: 'text-yellow-600', D: 'text-red-600' } return {r.grade} }}, { key: 'revenue_target', label: '营收目标', align: 'right', render: (r) => formatCurrency(r.revenue_target) }, { key: 'actual_revenue', label: '实际营收', align: 'right', render: (r) => formatCurrency(r.actual_revenue) }, { key: 'achievement_rate', label: '达成率', align: 'right', render: (r) => { const v = Number(r.achievement_rate || 0) return = 100 ? 'font-medium text-green-600' : v >= 80 ? 'text-yellow-600' : 'font-medium text-red-600'}>{v.toFixed(1)}% }}, { key: 'bill_count_target', label: '账单目标', align: 'right', render: (r) => formatNumber(r.bill_count_target) }, { key: 'actual_bill_count', label: '实际账单', align: 'right', render: (r) => formatNumber(r.actual_bill_count) }, { key: 'bill_achievement_rate', label: '账单达成率', align: 'right', render: (r) => { const v = Number(r.bill_achievement_rate || 0) return = 100 ? 'text-green-600' : v >= 80 ? 'text-yellow-600' : 'text-red-600'}>{v.toFixed(1)}% }}, ]} /> )} )}
) }