a2e9ba55c2
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除 P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量 P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
189 lines
11 KiB
TypeScript
189 lines
11 KiB
TypeScript
import { useState } from "react"
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||
import { rosterApi } from '../../lib/api-services'
|
||
import Card from "../../components/ui/Card"
|
||
import Button from "../../components/ui/Button"
|
||
import { Input, Label, Select } from "../../components/ui/Input"
|
||
import { AlertTriangle, Check } from "lucide-react"
|
||
|
||
// ========== 绩效记录管理 ==========
|
||
|
||
export default function PerformanceInfo({ employeeId, records }: { employeeId: string; records: any[] }) {
|
||
const queryClient = useQueryClient()
|
||
const [showForm, setShowForm] = useState(false)
|
||
const [form, setForm] = useState({ period: '', periodType: 'MONTHLY' as 'MONTHLY' | 'QUARTERLY' | 'YEARLY', score: 80, grade: 'B', result: 'QUALIFIED', summary: '', improvementPlan: '', employeeAck: false, ackDate: '', reviewer: '', templateId: '' })
|
||
const [dimensionScores, setDimensionScores] = useState<Record<string, number>>({})
|
||
|
||
const { data: templates } = useQuery({
|
||
queryKey: ['performance-templates'],
|
||
queryFn: () => rosterApi.performanceTemplates(),
|
||
})
|
||
|
||
const createMutation = useMutation({
|
||
mutationFn: (data: any) => rosterApi.performance(employeeId, data),
|
||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false); setDimensionScores({}) },
|
||
})
|
||
|
||
const deleteMutation = useMutation({
|
||
mutationFn: (id: string) => rosterApi.removePerformance(employeeId, id),
|
||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile'] }),
|
||
})
|
||
|
||
const resultMap: Record<string, string> = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' }
|
||
|
||
// 根据得分自动计算等级和结果
|
||
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 selectedTemplate = (templates || []).find((t: any) => t.id === form.templateId)
|
||
const dimensions: any[] = selectedTemplate?.dimensions || []
|
||
|
||
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.dimensionScores = dimensionScores
|
||
}
|
||
createMutation.mutate(data)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<div className="flex justify-between items-center">
|
||
<h2 className="text-xs font-medium">绩效考核记录({records?.length || 0}条)</h2>
|
||
<Button size="sm" onClick={() => setShowForm(!showForm)}>新增绩效记录</Button>
|
||
</div>
|
||
|
||
{showForm && (
|
||
<Card>
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div><Label>考核类型</Label>
|
||
<Select value={form.periodType} onChange={(e) => setForm({ ...form, periodType: e.target.value as any })}>
|
||
<option value="MONTHLY">月度考核</option>
|
||
<option value="QUARTERLY">季度考核</option>
|
||
<option value="YEARLY">年度考核</option>
|
||
</Select>
|
||
</div>
|
||
<div><Label>考核周期</Label><Input value={form.period} onChange={(e) => setForm({ ...form, period: e.target.value })} placeholder={form.periodType === 'MONTHLY' ? '如 2026-07' : form.periodType === 'QUARTERLY' ? '如 2026-Q3' : '如 2026'} /></div>
|
||
<div className="md:col-span-2"><Label>绩效模板(选填)</Label>
|
||
<Select value={form.templateId} onChange={(e) => { setForm({ ...form, templateId: e.target.value }); setDimensionScores({}) }}>
|
||
<option value="">不使用模板(简单评分)</option>
|
||
{(templates || []).map((t: any) => (
|
||
<option key={t.id} value={t.id}>{t.name}{t.isDefault ? '(默认)' : ''}</option>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
{dimensions.length > 0 ? (
|
||
<div className="md:col-span-2 border border-gray-200 rounded-md p-3 space-y-2">
|
||
<div className="text-xs font-medium text-gray-600">考核维度(按权重计算总分)</div>
|
||
{dimensions.map((d: any) => (
|
||
<div key={d.name} className="grid grid-cols-12 gap-2 items-center">
|
||
<div className="col-span-5">
|
||
<span className="text-sm">{d.name}</span>
|
||
<span className="text-xs text-gray-400 ml-1">权重{d.weight}%</span>
|
||
</div>
|
||
<div className="col-span-4">
|
||
<Input type="number" min={0} max={d.maxScore || 100} value={dimensionScores[d.name] ?? ''} onChange={(e) => handleDimensionChange(d.name, Number(e.target.value))} placeholder={`满分${d.maxScore || 100}`} className="text-sm" />
|
||
</div>
|
||
<div className="col-span-3 text-xs text-gray-400">/{d.maxScore || 100}</div>
|
||
</div>
|
||
))}
|
||
<div className="grid grid-cols-2 gap-3 pt-2 border-t">
|
||
<div><Label>总分(自动计算)</Label><Input type="number" value={form.score} readOnly className="bg-gray-50" /></div>
|
||
<div><Label>等级(自动计算)</Label><Input value={form.grade} readOnly className="bg-gray-50" /></div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div><Label>考核得分</Label><Input type="number" value={form.score} onChange={(e) => handleScoreChange(Number(e.target.value))} /></div>
|
||
<div><Label>等级(由得分自动计算)</Label>
|
||
<Select value={form.grade} onChange={(e) => setForm({ ...form, grade: e.target.value })}>
|
||
<option value="A">A</option><option value="B">B</option><option value="C">C</option><option value="D">D</option>
|
||
</Select>
|
||
</div>
|
||
</>
|
||
)}
|
||
<div><Label>考核结果(由得分自动计算)</Label>
|
||
<Select value={form.result} onChange={(e) => setForm({ ...form, result: e.target.value })}>
|
||
{Object.entries(resultMap).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||
</Select>
|
||
</div>
|
||
<div><Label>考核人</Label><Input value={form.reviewer} onChange={(e) => setForm({ ...form, reviewer: e.target.value })} /></div>
|
||
<div className="md:col-span-2"><Label>考核评语</Label><Input value={form.summary} onChange={(e) => setForm({ ...form, summary: e.target.value })} /></div>
|
||
<div className="md:col-span-2"><Label>改进计划(不胜任时填写)</Label><Input value={form.improvementPlan} onChange={(e) => setForm({ ...form, improvementPlan: e.target.value })} placeholder="如:调岗至XX岗位,培训XX技能" /></div>
|
||
<div className="flex items-center gap-2 pt-6">
|
||
<input type="checkbox" id="perfAck" checked={form.employeeAck} onChange={(e) => setForm({ ...form, employeeAck: e.target.checked })} />
|
||
<label htmlFor="perfAck" className="text-xs">员工已签字确认</label>
|
||
</div>
|
||
{form.employeeAck && <div><Label>确认日期</Label><Input type="date" value={form.ackDate} onChange={(e) => setForm({ ...form, ackDate: e.target.value })} /></div>}
|
||
<div className="md:col-span-2 flex gap-2"><Button onClick={handleSubmit} disabled={createMutation.isPending || !form.period}>{createMutation.isPending ? '保存中...' : '保存'}</Button><Button variant="secondary" onClick={() => setShowForm(false)}>取消</Button></div>
|
||
</div>
|
||
</Card>
|
||
)}
|
||
|
||
{records?.length === 0 ? (
|
||
<Card><div className="text-center py-8 text-gray-400">暂无绩效记录</div></Card>
|
||
) : records?.map((r) => (
|
||
<Card key={r.id} className="p-4">
|
||
<div className="flex justify-between items-start gap-3">
|
||
<div className="flex-1 space-y-2">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="text-xs font-medium text-gray-700">{r.period}</span>
|
||
<span className={`px-2 py-0.5 rounded text-xs ${r.result === 'EXCELLENT' ? 'bg-green-50 text-safe' : r.result === 'QUALIFIED' ? 'bg-blue-50 text-blue-600' : r.result === 'NEED_IMPROVE' ? 'bg-amber-50 text-amber-600' : 'bg-red-50 text-danger'}`}>
|
||
{resultMap[r.result] || r.result}
|
||
</span>
|
||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 text-xs">得分 {r.score} · 等级 {r.grade}</span>
|
||
</div>
|
||
{r.dimensionScores && Object.keys(r.dimensionScores).length > 0 && (
|
||
<div className="flex flex-wrap gap-1">
|
||
{Object.entries(r.dimensionScores).map(([name, score]: [string, any]) => (
|
||
<span key={name} className="text-xs px-2 py-0.5 rounded bg-gray-50 text-gray-600">{name}: {score}</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
{r.summary && <div className="text-xs text-gray-600 leading-relaxed">{r.summary}</div>}
|
||
{r.improvementPlan && (
|
||
<div className="text-xs bg-amber-50 text-amber-700 px-2 py-1.5 rounded leading-relaxed">
|
||
<span className="font-medium">改进计划</span>:{r.improvementPlan}
|
||
</div>
|
||
)}
|
||
<div className="flex items-center gap-4 text-xs pt-1 border-t text-gray-400">
|
||
{r.employeeAck ? (
|
||
<span className="text-safe flex items-center gap-1"><Check className="w-3 h-3" />已签字({r.ackDate?.toString().slice(0, 10)})</span>
|
||
) : (
|
||
<span className="text-warning flex items-center gap-1"><AlertTriangle className="w-3 h-3" />未签字</span>
|
||
)}
|
||
{r.reviewer && <span>考核人:{r.reviewer}</span>}
|
||
</div>
|
||
</div>
|
||
<button onClick={() => deleteMutation.mutate(r.id)} className="text-xs text-gray-300 hover:text-danger shrink-0">删除</button>
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|