feat: 绩效工资两行显示+等级系数着色
后端: 批次详情接口附加 perfGrade/perfCoefficient 到每个 entry 前端: 绩效工资列改为两行显示,上行金额,下行等级·系数 - A级(1.2)绿色, C级(0.8)橙色, D级(0.6)红色, B级(1.0)默认色 - 数据从 batch detail 接口获取,刷新不丢失
This commit is contained in:
@@ -292,12 +292,31 @@ router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFun
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!rawBatch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
if (!rawBatch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||||
|
|
||||||
|
// 查询批次月份的绩效记录,附加到 entry 上
|
||||||
|
const perfRecords = await prisma.performanceRecord.findMany({
|
||||||
|
where: { orgId: req.user!.orgId, period: rawBatch.month, periodType: 'MONTHLY' },
|
||||||
|
select: { employeeId: true, grade: true, score: true },
|
||||||
|
})
|
||||||
|
const gradeCoefficients: Record<string, number> = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 }
|
||||||
|
const perfMap = new Map<string, { grade: string; coefficient: number }>()
|
||||||
|
for (const r of perfRecords) {
|
||||||
|
if (!perfMap.has(r.employeeId)) {
|
||||||
|
perfMap.set(r.employeeId, { grade: r.grade, coefficient: gradeCoefficients[r.grade] || 1.0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const batch = {
|
const batch = {
|
||||||
...rawBatch,
|
...rawBatch,
|
||||||
entries: rawBatch.entries.map((e: any) => ({
|
entries: rawBatch.entries.map((e: any) => {
|
||||||
...e,
|
const perf = perfMap.get(e.employeeId)
|
||||||
employee: e.employee ? { ...e.employee, idCardNumber: safeDecryptStr(e.employee.idCardNumber) } : e.employee,
|
return {
|
||||||
})),
|
...e,
|
||||||
|
employee: e.employee ? { ...e.employee, idCardNumber: safeDecryptStr(e.employee.idCardNumber) } : e.employee,
|
||||||
|
perfGrade: perf?.grade || null,
|
||||||
|
perfCoefficient: perf?.coefficient || null,
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
res.json({ success: true, data: batch })
|
res.json({ success: true, data: batch })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -489,7 +489,6 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
const [taxDetailFor, setTaxDetailFor] = useState<{ employeeId: string; name: string } | null>(null)
|
const [taxDetailFor, setTaxDetailFor] = useState<{ employeeId: string; name: string } | null>(null)
|
||||||
const [taxDetailData, setTaxDetailData] = useState<any>(null)
|
const [taxDetailData, setTaxDetailData] = useState<any>(null)
|
||||||
const [taxDetailLoading, setTaxDetailLoading] = useState(false)
|
const [taxDetailLoading, setTaxDetailLoading] = useState(false)
|
||||||
const [perfDetails, setPerfDetails] = useState<Map<string, { grade: string | null; coefficient: number }>>(new Map())
|
|
||||||
|
|
||||||
const { data: batch, isLoading } = useQuery<any>({
|
const { data: batch, isLoading } = useQuery<any>({
|
||||||
queryKey: ['batch-detail', batchId],
|
queryKey: ['batch-detail', batchId],
|
||||||
@@ -600,13 +599,6 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
onSuccess: (res: any) => {
|
onSuccess: (res: any) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
queryClient.invalidateQueries({ queryKey: ['batch-detail'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
queryClient.invalidateQueries({ queryKey: ['batches'] })
|
||||||
if (res.data?.details) {
|
|
||||||
const map = new Map<string, { grade: string | null; coefficient: number }>()
|
|
||||||
for (const d of res.data.details) {
|
|
||||||
map.set(d.employeeId, { grade: d.grade, coefficient: d.coefficient })
|
|
||||||
}
|
|
||||||
setPerfDetails(map)
|
|
||||||
}
|
|
||||||
if (res.data?.filled > 0) {
|
if (res.data?.filled > 0) {
|
||||||
toast.success(res.data.message)
|
toast.success(res.data.message)
|
||||||
} else {
|
} else {
|
||||||
@@ -1239,18 +1231,14 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
{renderCell(entry, 'baseSalary')}
|
{renderCell(entry, 'baseSalary')}
|
||||||
{(() => {
|
{(() => {
|
||||||
const perfVal = entry.performanceSalary || 0
|
const perfVal = entry.performanceSalary || 0
|
||||||
const perfInfo = perfDetails.get(entry.employeeId)
|
const grade = entry.perfGrade || null
|
||||||
const coef = perfInfo?.coefficient
|
const coef = entry.perfCoefficient != null ? entry.perfCoefficient : null
|
||||||
const grade = perfInfo?.grade
|
|
||||||
// 颜色:A(1.2)=绿色, C(0.8)=橙色, D(0.6)=红色, B(1.0)/无=默认
|
// 颜色:A(1.2)=绿色, C(0.8)=橙色, D(0.6)=红色, B(1.0)/无=默认
|
||||||
let colorClass = ''
|
let colorClass = ''
|
||||||
let title = ''
|
|
||||||
if (coef != null && coef !== 1.0) {
|
if (coef != null && coef !== 1.0) {
|
||||||
if (coef > 1.0) { colorClass = 'text-green-600'; title = `考核等级 ${grade},系数 ${coef}` }
|
if (coef > 1.0) colorClass = 'text-green-600'
|
||||||
else if (coef >= 0.8) { colorClass = 'text-orange-600'; title = `考核等级 ${grade},系数 ${coef}` }
|
else if (coef >= 0.8) colorClass = 'text-orange-600'
|
||||||
else { colorClass = 'text-red-600'; title = `考核等级 ${grade},系数 ${coef}` }
|
else colorClass = 'text-red-600'
|
||||||
} else if (coef === 1.0 && grade) {
|
|
||||||
title = `考核等级 ${grade},系数 1.0`
|
|
||||||
}
|
}
|
||||||
const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === 'performanceSalary'
|
const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === 'performanceSalary'
|
||||||
const canEdit = !isArchived && editableFields.includes('performanceSalary')
|
const canEdit = !isArchived && editableFields.includes('performanceSalary')
|
||||||
@@ -1273,17 +1261,23 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
|||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key="performanceSalary"
|
key="performanceSalary"
|
||||||
className={`py-2 px-2 text-right ${canEdit ? 'cursor-text' : ''}`}
|
className={`py-1 px-2 text-right ${canEdit ? 'cursor-text' : ''}`}
|
||||||
onClick={() => canEdit && startEdit(entry.employeeId, 'performanceSalary', perfVal)}
|
onClick={() => canEdit && startEdit(entry.employeeId, 'performanceSalary', perfVal)}
|
||||||
title={title}
|
|
||||||
>
|
>
|
||||||
{canEdit ? (
|
<div className="flex flex-col items-end leading-tight">
|
||||||
<span className={`border-b border-dashed border-gray-300 hover:border-primary ${colorClass} ${perfVal === 0 ? 'text-gray-300' : ''}`}>
|
{canEdit ? (
|
||||||
{fmt(perfVal)}
|
<span className={`border-b border-dashed border-gray-300 hover:border-primary ${colorClass} ${perfVal === 0 ? 'text-gray-300' : ''}`}>
|
||||||
</span>
|
{fmt(perfVal)}
|
||||||
) : (
|
</span>
|
||||||
<span className={`${colorClass} ${perfVal === 0 ? 'text-gray-300' : ''}`}>{fmt(perfVal)}</span>
|
) : (
|
||||||
)}
|
<span className={`${colorClass} ${perfVal === 0 ? 'text-gray-300' : ''}`}>{fmt(perfVal)}</span>
|
||||||
|
)}
|
||||||
|
{grade && (
|
||||||
|
<span className={`text-[10px] ${colorClass || 'text-gray-400'}`}>
|
||||||
|
{grade}·{coef != null ? coef.toFixed(1) : '1.0'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
Reference in New Issue
Block a user