feat: 绩效系数配置化 + 工作台社保公积金评分修复

- Organization 新增 defaultGradeCoefficients 字段,系统设置可配置缺省绩效系数
- 绩效记录编辑时自动带出系数(优先模板 gradeRules,回退组织缺省配置)
- 薪资计算优先用记录 coefficient,回退模板/组织缺省/硬编码默认值
- 绩效模板表单新增等级系数设置区域
- 修复工作台社保公积金评分为0:查询从旧表 SocialInsuranceConfig 改为 SocialYearStandard
- 修复 employeesNoSocial 计算逻辑:只统计有正式劳动合同(FIXED/UNFIXED)但无社保记录的员工
- 社保账户未设置 paymentDay 时回退到 Organization.socialInsCutoffDay 生成月度任务
- 合规健康度各维度补充 todoCount 字段

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-19 15:16:06 +08:00
parent 81e136b707
commit f9324d4299
8 changed files with 245 additions and 39 deletions
+21
View File
@@ -108,6 +108,7 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data:
esignTrainingEnabled: false,
esignPerformanceEnabled: false,
esignDisciplinaryEnabled: false,
defaultGradeCoefficients: { A: 1.2, B: 1.0, C: 0.8, D: 0.6 } as Record<string, number>,
})
useEffect(() => {
@@ -125,6 +126,7 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data:
esignTrainingEnabled: orgData.esignTrainingEnabled || false,
esignPerformanceEnabled: orgData.esignPerformanceEnabled || false,
esignDisciplinaryEnabled: orgData.esignDisciplinaryEnabled || false,
defaultGradeCoefficients: orgData.defaultGradeCoefficients || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 },
})
}
}, [orgData])
@@ -180,6 +182,25 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data:
<span className="text-xs text-gray-500"></span>
</div>
</div>
<div>
<Label></Label>
<p className="text-xs text-gray-500 mt-1 mb-2"> = × </p>
<div className="grid grid-cols-4 gap-2">
{(['A', 'B', 'C', 'D'] as const).map(g => (
<div key={g}>
<Label className="text-xs"> {g}</Label>
<Input
type="number"
step="0.1"
min={0}
value={form.defaultGradeCoefficients[g] ?? 1.0}
onChange={(e) => setForm({ ...form, defaultGradeCoefficients: { ...form.defaultGradeCoefficients, [g]: Number(e.target.value) } })}
className="text-sm"
/>
</div>
))}
</div>
</div>
<Button onClick={() => onSave(form)} disabled={saving}>
{saving ? '保存中...' : '保存'}
</Button>
+40 -8
View File
@@ -1,6 +1,6 @@
import { useState } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { rosterApi } from '../../lib/api-services'
import { rosterApi, settingsApi } from '../../lib/api-services'
import Card from "../../components/ui/Card"
import Button from "../../components/ui/Button"
import { Input, Label, Select } from "../../components/ui/Input"
@@ -11,7 +11,7 @@ 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 [form, setForm] = useState({ period: '', periodType: 'MONTHLY' as 'MONTHLY' | 'QUARTERLY' | 'YEARLY', score: 80, grade: 'B', coefficient: 1.0, result: 'QUALIFIED', summary: '', improvementPlan: '', employeeAck: false, ackDate: '', reviewer: '', templateId: '' })
const [dimensionScores, setDimensionScores] = useState<Record<string, number>>({})
const { data: templates } = useQuery({
@@ -19,6 +19,12 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
queryFn: () => rosterApi.performanceTemplates(),
})
// 获取组织缺省绩效系数配置
const { data: orgData } = useQuery<any>({
queryKey: ['org-settings'],
queryFn: () => settingsApi.org(),
})
const createMutation = useMutation({
mutationFn: (data: any) => rosterApi.performance(employeeId, data),
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['roster-profile'] }); setShowForm(false); setDimensionScores({}) },
@@ -31,6 +37,24 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
const resultMap: Record<string, string> = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' }
const selectedTemplate = (templates || []).find((t: any) => t.id === form.templateId)
const dimensions: any[] = selectedTemplate?.dimensions || []
// 等级系数映射:优先从选中模板的 gradeRules 取,回退到组织缺省配置,再回退到硬编码默认值
const SYSTEM_DEFAULT_COEFFICIENTS: Record<string, number> = orgData?.defaultGradeCoefficients || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
const DEFAULT_COEFFICIENTS: Record<string, number> = (() => {
if (selectedTemplate?.gradeRules && Array.isArray(selectedTemplate.gradeRules)) {
const map: Record<string, number> = { ...SYSTEM_DEFAULT_COEFFICIENTS }
for (const rule of selectedTemplate.gradeRules) {
if (rule.grade && rule.coefficient != null) {
map[rule.grade] = Number(rule.coefficient)
}
}
return map
}
return SYSTEM_DEFAULT_COEFFICIENTS
})()
// 根据得分自动计算等级和结果
const scoreToGrade = (score: number): { grade: string; result: string } => {
if (score >= 90) return { grade: 'A', result: 'EXCELLENT' }
@@ -41,11 +65,14 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
const handleScoreChange = (score: number) => {
const { grade, result } = scoreToGrade(score)
setForm({ ...form, score, grade, result })
const newCoeff = grade !== form.grade ? (DEFAULT_COEFFICIENTS[grade] ?? 1.0) : form.coefficient
setForm({ ...form, score, grade, result, coefficient: newCoeff })
}
const selectedTemplate = (templates || []).find((t: any) => t.id === form.templateId)
const dimensions: any[] = selectedTemplate?.dimensions || []
const handleGradeChange = (grade: string) => {
const newCoeff = DEFAULT_COEFFICIENTS[grade] ?? 1.0
setForm({ ...form, grade, coefficient: newCoeff })
}
const handleDimensionChange = (name: string, score: number) => {
const updated = { ...dimensionScores, [name]: score }
@@ -58,7 +85,8 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
return sum + (s / maxScore) * weight * 100
}, 0)
const { grade, result } = scoreToGrade(Math.round(totalScore))
setForm(prev => ({ ...prev, score: Math.round(totalScore), grade, result }))
const newCoeff = grade !== form.grade ? (DEFAULT_COEFFICIENTS[grade] ?? 1.0) : form.coefficient
setForm(prev => ({ ...prev, score: Math.round(totalScore), grade, result, coefficient: newCoeff }))
}
}
@@ -120,12 +148,16 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
<>
<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 })}>
<Select value={form.grade} onChange={(e) => handleGradeChange(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>
<Input type="number" step="0.1" min={0} value={form.coefficient} onChange={(e) => setForm({ ...form, coefficient: Number(e.target.value) })} placeholder="默认 A=1.2, B=1.0, C=0.8, D=0.6" />
<p className="text-xs text-gray-500 mt-1"> = × </p>
</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>)}
@@ -155,7 +187,7 @@ export default function PerformanceInfo({ employeeId, records }: { employeeId: s
<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>
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 text-xs"> {r.score} · {r.grade} · {r.coefficient ?? 1.0}</span>
</div>
{r.dimensionScores && Object.keys(r.dimensionScores).length > 0 && (
<div className="flex flex-wrap gap-1">
@@ -3,7 +3,7 @@ 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 { rosterApi, employeeApi, settingsApi } from '../../lib/api-services'
import api from '../../lib/api'
import { usePageSize } from '../../hooks/usePageSize'
import { Input, Label, Select } from '../../components/ui/Input'
@@ -203,6 +203,12 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
onSubmit: (data: any) => void
onClose: () => void
}) {
// 获取组织缺省绩效系数配置
const { data: orgData } = useQuery<any>({
queryKey: ['org-settings'],
queryFn: () => settingsApi.org(),
})
const detectPeriodType = (period: string, fallback?: string): string => {
if (/^\d{4}-Q[1-4]$/.test(period)) return 'QUARTERLY'
if (/^\d{4}$/.test(period)) return 'YEARLY'
@@ -218,6 +224,7 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
periodType: initialPeriodType,
score: record?.score || 80,
grade: record?.grade || 'B',
coefficient: record?.coefficient ?? 1.0,
result: record?.result || 'QUALIFIED',
summary: record?.summary || '',
improvementPlan: record?.improvementPlan || '',
@@ -229,6 +236,21 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
const selectedTemplate = templates.find((t: any) => t.id === form.templateId)
const dimensions: any[] = selectedTemplate?.dimensions || []
// 等级系数映射:优先从选中模板的 gradeRules 取,回退到组织缺省配置,再回退到硬编码默认值
const SYSTEM_DEFAULT_COEFFICIENTS: Record<string, number> = orgData?.defaultGradeCoefficients || { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
const DEFAULT_COEFFICIENTS: Record<string, number> = (() => {
if (selectedTemplate?.gradeRules && Array.isArray(selectedTemplate.gradeRules)) {
const map: Record<string, number> = { ...SYSTEM_DEFAULT_COEFFICIENTS }
for (const rule of selectedTemplate.gradeRules) {
if (rule.grade && rule.coefficient != null) {
map[rule.grade] = Number(rule.coefficient)
}
}
return map
}
return SYSTEM_DEFAULT_COEFFICIENTS
})()
const scoreToGrade = (score: number): { grade: string; result: string } => {
if (score >= 90) return { grade: 'A', result: 'EXCELLENT' }
if (score >= 80) return { grade: 'B', result: 'QUALIFIED' }
@@ -238,7 +260,15 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
const handleScoreChange = (score: number) => {
const { grade, result } = scoreToGrade(score)
setForm({ ...form, score, grade, result })
// 得分变化导致等级变化时,自动更新系数为该等级的默认值
const newCoeff = grade !== form.grade ? (DEFAULT_COEFFICIENTS[grade] ?? 1.0) : form.coefficient
setForm({ ...form, score, grade, result, coefficient: newCoeff })
}
const handleGradeChange = (grade: string) => {
// 手动切换等级时,自动更新系数为该等级的默认值
const newCoeff = DEFAULT_COEFFICIENTS[grade] ?? 1.0
setForm({ ...form, grade, coefficient: newCoeff })
}
const handleDimensionChange = (name: string, score: number) => {
@@ -253,7 +283,8 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
return sum + (s / maxScore) * weight * 100
}, 0)
const { grade, result } = scoreToGrade(Math.round(totalScore))
setForm(prev => ({ ...prev, score: Math.round(totalScore), grade, result }))
const newCoeff = grade !== form.grade ? (DEFAULT_COEFFICIENTS[grade] ?? 1.0) : form.coefficient
setForm(prev => ({ ...prev, score: Math.round(totalScore), grade, result, coefficient: newCoeff }))
}
}
@@ -345,7 +376,7 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
</div>
<div>
<Label></Label>
<Select value={form.grade} onChange={(e) => setForm({ ...form, grade: e.target.value })}>
<Select value={form.grade} onChange={(e) => handleGradeChange(e.target.value)}>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
@@ -366,6 +397,18 @@ function PerformanceForm({ employees, templates, record, onSubmit, onClose }: {
</div>
</div>
)}
<div>
<Label></Label>
<Input
type="number"
step="0.1"
min={0}
value={form.coefficient}
onChange={(e) => setForm({ ...form, coefficient: Number(e.target.value) })}
placeholder="默认 A=1.2, B=1.0, C=0.8, D=0.6,可手动调整"
/>
<p className="text-xs text-gray-500 mt-1"> = × </p>
</div>
<div>
<Label></Label>
<Select value={form.result} onChange={(e) => setForm({ ...form, result: e.target.value })}>
@@ -504,6 +547,15 @@ function TemplateModal({ templates, onClose }: {
</span>
))}
</div>
{t.gradeRules && Array.isArray(t.gradeRules) && t.gradeRules.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{t.gradeRules.map((r: any) => (
<span key={r.grade} className="text-xs px-2 py-0.5 rounded bg-teal-50 text-teal-600">
{r.grade}×{r.coefficient ?? 1.0}
</span>
))}
</div>
)}
</div>
))}
</div>
@@ -524,6 +576,18 @@ function TemplateForm({ template, onSubmit, onClose }: {
const [dimensions, setDimensions] = useState<any[]>(
template?.dimensions || [{ name: '', weight: 100, maxScore: 100, description: '' }]
)
// 等级系数配置:[{ grade, coefficient }],从模板 gradeRules 中提取 coefficient
const [gradeCoefficients, setGradeCoefficients] = useState<Record<string, number>>(() => {
const defaults: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
if (template?.gradeRules && Array.isArray(template.gradeRules)) {
for (const rule of template.gradeRules) {
if (rule.grade && rule.coefficient != null) {
defaults[rule.grade] = Number(rule.coefficient)
}
}
}
return defaults
})
const addDimension = () => {
setDimensions([...dimensions, { name: '', weight: 0, maxScore: 100, description: '' }])
@@ -582,13 +646,40 @@ function TemplateForm({ template, onSubmit, onClose }: {
<span className={`text-xs ${totalWeight === 100 ? 'text-green-600' : 'text-amber-600'}`}>{totalWeight}%</span>
</div>
</div>
<div>
<Label></Label>
<p className="text-xs text-gray-500 mb-2"> = × </p>
<div className="grid grid-cols-4 gap-2">
{(['A', 'B', 'C', 'D'] as const).map(g => (
<div key={g}>
<Label className="text-xs"> {g}</Label>
<Input
type="number"
step="0.1"
min={0}
value={gradeCoefficients[g]}
onChange={(e) => setGradeCoefficients({ ...gradeCoefficients, [g]: Number(e.target.value) })}
className="text-sm"
/>
</div>
))}
</div>
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={isDefault} onChange={(e) => setIsDefault(e.target.checked)} />
</label>
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" size="sm" onClick={onClose}></Button>
<Button size="sm" onClick={() => onSubmit({ name, description, dimensions, isDefault })} disabled={!canSubmit}>
<Button size="sm" onClick={() => onSubmit({
name, description, dimensions, isDefault,
gradeRules: [
{ grade: 'A', result: 'EXCELLENT', minScore: 90, maxScore: 100, coefficient: gradeCoefficients.A },
{ grade: 'B', result: 'QUALIFIED', minScore: 80, maxScore: 89, coefficient: gradeCoefficients.B },
{ grade: 'C', result: 'NEED_IMPROVE', minScore: 60, maxScore: 79, coefficient: gradeCoefficients.C },
{ grade: 'D', result: 'UNQUALIFIED', minScore: 0, maxScore: 59, coefficient: gradeCoefficients.D },
],
})} disabled={!canSubmit}>
</Button>
</div>