import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Link } from 'react-router-dom' import { Search, Plus, Edit2, Trash2, X, LayoutTemplate } from 'lucide-react' import { toast } from 'sonner' import { rosterApi, employeeApi } from '../../lib/api-services' import api from '../../lib/api' import { usePageSize } from '../../hooks/usePageSize' import { Input, Label, Select } from '../../components/ui/Input' import Button from '../../components/ui/Button' import PageGuide from '../../components/ui/PageGuide' const RESULT_LABELS: Record = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' } const RESULT_COLORS: Record = { EXCELLENT: 'bg-green-50 text-green-700', QUALIFIED: 'bg-blue-50 text-blue-700', NEED_IMPROVE: 'bg-amber-50 text-amber-700', UNQUALIFIED: 'bg-red-50 text-red-700' } export default function PerformanceRecords() { const queryClient = useQueryClient() const pageSize = usePageSize() const [page, setPage] = useState(1) const [keyword, setKeyword] = useState('') const [showCreate, setShowCreate] = useState(false) const [editRecord, setEditRecord] = useState(null) const [showTemplateModal, setShowTemplateModal] = useState(false) const { data, isLoading } = useQuery({ queryKey: ['performance-list', page, pageSize, keyword], queryFn: () => rosterApi.performanceList({ page, pageSize, keyword }), }) const { data: employees } = useQuery({ queryKey: ['employees-active'], queryFn: () => employeeApi.list({ status: 'ACTIVE' }), }) const { data: templates } = useQuery({ queryKey: ['performance-templates'], queryFn: () => rosterApi.performanceTemplates(), }) const saveMut = useMutation({ mutationFn: (data: any) => { const empId = data.employeeId delete data.employeeId const isEdit = !!data.recordId const recordId = data.recordId delete data.recordId if (isEdit) { return api.put(`/roster/${empId}/performance/${recordId}`, data) } return api.post(`/roster/${empId}/performance`, data) }, onSuccess: () => { toast.success('绩效记录已保存') queryClient.invalidateQueries({ queryKey: ['performance-list'] }) setShowCreate(false) setEditRecord(null) }, onError: () => toast.error('保存失败'), }) const deleteMut = useMutation({ mutationFn: ({ employeeId, recordId }: { employeeId: string; recordId: string }) => api.delete(`/roster/${employeeId}/performance/${recordId}`), onSuccess: () => { toast.success('记录已删除') queryClient.invalidateQueries({ queryKey: ['performance-list'] }) }, }) const records = data?.records || [] const total = data?.total || 0 const totalPages = Math.ceil(total / pageSize) return (
绩效记录管理,记录员工绩效考核结果,支持季度、年度考核。流程:设定目标 → 周期评估 → 结果归档。关联:绩效数据关联员工档案,影响薪酬调整和晋升。

绩效考核

管理全员绩效考核记录

{ setKeyword(e.target.value); setPage(1) }} placeholder="搜索员工姓名" className="pl-9" />
{isLoading ? ( ) : records.length === 0 ? ( ) : records.map((r: any) => ( ))}
员工 部门 考核周期 得分 等级 结果 考评人 签字 操作
加载中...
暂无绩效记录
{r.employee?.name} {r.employee?.department || '-'} {r.period} {r.score} {r.grade} {RESULT_LABELS[r.result] || r.result} {r.reviewer || '-'} {r.employeeAck ? ( 已签字 ) : ( 待签字 )}
{totalPages > 1 && (
共 {total} 条
{page} / {totalPages}
)} {(showCreate || editRecord) && ( saveMut.mutate(data)} onClose={() => { setShowCreate(false); setEditRecord(null) }} /> )} {showTemplateModal && ( setShowTemplateModal(false)} /> )}
) } function PerformanceForm({ employees, templates, record, onSubmit, onClose }: { employees: any[] templates: any[] record: any onSubmit: (data: any) => void onClose: () => void }) { const detectPeriodType = (period: string, fallback?: string): string => { if (/^\d{4}-Q[1-4]$/.test(period)) return 'QUARTERLY' if (/^\d{4}$/.test(period)) return 'YEARLY' if (/^\d{4}-\d{2}$/.test(period)) return 'MONTHLY' return fallback || 'MONTHLY' } const initialPeriodType = record ? detectPeriodType(record.period, record.periodType) : 'MONTHLY' const [form, setForm] = useState({ employeeId: record?.employeeId || '', period: record?.period || new Date().toISOString().slice(0, 7), periodType: initialPeriodType, score: record?.score || 80, grade: record?.grade || 'B', result: record?.result || 'QUALIFIED', summary: record?.summary || '', improvementPlan: record?.improvementPlan || '', reviewer: record?.reviewer || '', templateId: record?.templateId || '', }) const [dimensionScores, setDimensionScores] = useState>(record?.dimensionScores || {}) const selectedTemplate = templates.find((t: any) => t.id === form.templateId) const dimensions: any[] = selectedTemplate?.dimensions || [] const scoreToGrade = (score: number): { grade: string; result: string } => { if (score >= 90) return { grade: 'A', result: 'EXCELLENT' } if (score >= 80) return { grade: 'B', result: 'QUALIFIED' } if (score >= 60) return { grade: 'C', result: 'NEED_IMPROVE' } return { grade: 'D', result: 'UNQUALIFIED' } } const handleScoreChange = (score: number) => { const { grade, result } = scoreToGrade(score) setForm({ ...form, score, grade, result }) } const handleDimensionChange = (name: string, score: number) => { const updated = { ...dimensionScores, [name]: score } setDimensionScores(updated) // 按权重计算总分 if (dimensions.length > 0) { const totalScore = dimensions.reduce((sum: number, d: any) => { const s = updated[d.name] ?? 0 const weight = d.weight || 0 const maxScore = d.maxScore || 100 return sum + (s / maxScore) * weight * 100 }, 0) const { grade, result } = scoreToGrade(Math.round(totalScore)) setForm(prev => ({ ...prev, score: Math.round(totalScore), grade, result })) } } const handleSubmit = () => { const data: any = { ...form } if (form.templateId) { data.templateId = form.templateId data.dimensionScores = dimensionScores } onSubmit(data) } return (
e.stopPropagation()}>

{record ? '编辑绩效记录' : '新增绩效记录'}

{!record && (
)}
setForm({ ...form, period: e.target.value })} placeholder={form.periodType === 'YEARLY' ? '如 2026' : form.periodType === 'QUARTERLY' ? '如 2026-Q1' : undefined} />
{dimensions.length > 0 ? (
考核维度(按权重计算总分)
{dimensions.map((d: any) => (
{d.name} {d.description && ({d.description})} 权重{d.weight}%
handleDimensionChange(d.name, Number(e.target.value))} placeholder={`满分${d.maxScore || 100}`} className="text-sm" />
/{d.maxScore || 100}
))}
系统按各维度得分和权重自动计算总分
) : (
handleScoreChange(Number(e.target.value))} />
)} {dimensions.length > 0 && (
)}
setForm({ ...form, reviewer: e.target.value })} placeholder="请输入考评人姓名" />
setForm({ ...form, summary: e.target.value })} placeholder="考核评语" />
setForm({ ...form, improvementPlan: e.target.value })} placeholder="改进计划(选填)" />
{record && (
{record.employeeAck ? ( 已签字({record.ackDate ? new Date(record.ackDate).toLocaleDateString('zh-CN') : ''}) ) : ( 待签字 由员工在员工端签字确认 )}
)}
) } function TemplateModal({ templates, onClose }: { templates: any[] onClose: () => void }) { const queryClient = useQueryClient() const [editing, setEditing] = useState(null) const [showForm, setShowForm] = useState(false) const createMut = useMutation({ mutationFn: (data: any) => rosterApi.createPerformanceTemplate(data), onSuccess: () => { toast.success('模板已创建') queryClient.invalidateQueries({ queryKey: ['performance-templates'] }) setShowForm(false) }, onError: () => toast.error('创建失败'), }) const updateMut = useMutation({ mutationFn: ({ id, data }: { id: string; data: any }) => rosterApi.updatePerformanceTemplate(id, data), onSuccess: () => { toast.success('模板已更新') queryClient.invalidateQueries({ queryKey: ['performance-templates'] }) setShowForm(false) setEditing(null) }, onError: () => toast.error('更新失败'), }) const deleteMut = useMutation({ mutationFn: (id: string) => rosterApi.deletePerformanceTemplate(id), onSuccess: () => { toast.success('模板已删除') queryClient.invalidateQueries({ queryKey: ['performance-templates'] }) }, }) return (
e.stopPropagation()}>

绩效模板管理

{showForm ? ( { if (editing) { updateMut.mutate({ id: editing.id, data }) } else { createMut.mutate(data) } }} onClose={() => { setShowForm(false); setEditing(null) }} /> ) : (
{templates.length === 0 ? (
暂无绩效模板,点击「新建模板」创建
) : templates.map((t: any) => (
{t.name} {t.isDefault && 默认}
{t.description &&

{t.description}

}
{(t.dimensions as any[]).map((d: any) => ( {d.name}(权重{d.weight}%) ))}
))}
)}
) } function TemplateForm({ template, onSubmit, onClose }: { template: any onSubmit: (data: any) => void onClose: () => void }) { const [name, setName] = useState(template?.name || '') const [description, setDescription] = useState(template?.description || '') const [isDefault, setIsDefault] = useState(template?.isDefault || false) const [dimensions, setDimensions] = useState( template?.dimensions || [{ name: '', weight: 100, maxScore: 100, description: '' }] ) const addDimension = () => { setDimensions([...dimensions, { name: '', weight: 0, maxScore: 100, description: '' }]) } const removeDimension = (idx: number) => { setDimensions(dimensions.filter((_, i) => i !== idx)) } const updateDimension = (idx: number, field: string, value: any) => { setDimensions(dimensions.map((d, i) => i === idx ? { ...d, [field]: value } : d)) } const totalWeight = dimensions.reduce((sum, d) => sum + (Number(d.weight) || 0), 0) const canSubmit = name && dimensions.every(d => d.name) && totalWeight === 100 return (
setName(e.target.value)} placeholder="如:月度绩效考核表" />
setDescription(e.target.value)} placeholder="模板用途说明(选填)" />
{dimensions.map((d, idx) => (
updateDimension(idx, 'name', e.target.value)} placeholder="维度名称" className="text-sm" />
updateDimension(idx, 'weight', Number(e.target.value))} placeholder="权重%" className="text-sm" />
updateDimension(idx, 'maxScore', Number(e.target.value))} placeholder="满分" className="text-sm" />
updateDimension(idx, 'description', e.target.value)} placeholder="说明(选填)" className="text-sm" />
{dimensions.length > 1 && ( )}
))}
权重合计:{totalWeight}%
{!canSubmit && totalWeight !== 100 && (
各维度权重合计必须为100%
)}
) }