From 13f21252d0edecbcb59f7032f35eeba670d8355f Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Wed, 19 Aug 2026 07:47:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=A9=E6=95=88=E5=B7=A5=E8=B5=84?= =?UTF-8?q?=E4=B8=A4=E8=A1=8C=E6=98=BE=E7=A4=BA+=E7=AD=89=E7=BA=A7?= =?UTF-8?q?=E7=B3=BB=E6=95=B0=E7=9D=80=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端: 批次详情接口附加 perfGrade/perfCoefficient 到每个 entry 前端: 绩效工资列改为两行显示,上行金额,下行等级·系数 - A级(1.2)绿色, C级(0.8)橙色, D级(0.6)红色, B级(1.0)默认色 - 数据从 batch detail 接口获取,刷新不丢失 --- backend/src/routes/payroll2.routes.ts | 27 +++++++++++++--- frontend/src/pages/money/BatchTab.tsx | 46 ++++++++++++--------------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/backend/src/routes/payroll2.routes.ts b/backend/src/routes/payroll2.routes.ts index da12dee..87f29b8 100644 --- a/backend/src/routes/payroll2.routes.ts +++ b/backend/src/routes/payroll2.routes.ts @@ -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: '批次不存在' } }) + + // 查询批次月份的绩效记录,附加到 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 = { A: 1.2, B: 1.0, C: 0.8, D: 0.6 } + const perfMap = new Map() + 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 = { ...rawBatch, - entries: rawBatch.entries.map((e: any) => ({ - ...e, - employee: e.employee ? { ...e.employee, idCardNumber: safeDecryptStr(e.employee.idCardNumber) } : e.employee, - })), + entries: rawBatch.entries.map((e: any) => { + const perf = perfMap.get(e.employeeId) + 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 }) } catch (err) { diff --git a/frontend/src/pages/money/BatchTab.tsx b/frontend/src/pages/money/BatchTab.tsx index d79010e..c142a45 100644 --- a/frontend/src/pages/money/BatchTab.tsx +++ b/frontend/src/pages/money/BatchTab.tsx @@ -489,7 +489,6 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void const [taxDetailFor, setTaxDetailFor] = useState<{ employeeId: string; name: string } | null>(null) const [taxDetailData, setTaxDetailData] = useState(null) const [taxDetailLoading, setTaxDetailLoading] = useState(false) - const [perfDetails, setPerfDetails] = useState>(new Map()) const { data: batch, isLoading } = useQuery({ queryKey: ['batch-detail', batchId], @@ -600,13 +599,6 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void onSuccess: (res: any) => { queryClient.invalidateQueries({ queryKey: ['batch-detail'] }) queryClient.invalidateQueries({ queryKey: ['batches'] }) - if (res.data?.details) { - const map = new Map() - for (const d of res.data.details) { - map.set(d.employeeId, { grade: d.grade, coefficient: d.coefficient }) - } - setPerfDetails(map) - } if (res.data?.filled > 0) { toast.success(res.data.message) } else { @@ -1239,18 +1231,14 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void {renderCell(entry, 'baseSalary')} {(() => { const perfVal = entry.performanceSalary || 0 - const perfInfo = perfDetails.get(entry.employeeId) - const coef = perfInfo?.coefficient - const grade = perfInfo?.grade + const grade = entry.perfGrade || null + const coef = entry.perfCoefficient != null ? entry.perfCoefficient : null // 颜色:A(1.2)=绿色, C(0.8)=橙色, D(0.6)=红色, B(1.0)/无=默认 let colorClass = '' - let title = '' if (coef != null && coef !== 1.0) { - if (coef > 1.0) { colorClass = 'text-green-600'; title = `考核等级 ${grade},系数 ${coef}` } - else if (coef >= 0.8) { colorClass = 'text-orange-600'; title = `考核等级 ${grade},系数 ${coef}` } - else { colorClass = 'text-red-600'; title = `考核等级 ${grade},系数 ${coef}` } - } else if (coef === 1.0 && grade) { - title = `考核等级 ${grade},系数 1.0` + if (coef > 1.0) colorClass = 'text-green-600' + else if (coef >= 0.8) colorClass = 'text-orange-600' + else colorClass = 'text-red-600' } const isEditing = editCell?.employeeId === entry.employeeId && editCell?.field === 'performanceSalary' const canEdit = !isArchived && editableFields.includes('performanceSalary') @@ -1273,17 +1261,23 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void return ( canEdit && startEdit(entry.employeeId, 'performanceSalary', perfVal)} - title={title} > - {canEdit ? ( - - {fmt(perfVal)} - - ) : ( - {fmt(perfVal)} - )} +
+ {canEdit ? ( + + {fmt(perfVal)} + + ) : ( + {fmt(perfVal)} + )} + {grade && ( + + {grade}·{coef != null ? coef.toFixed(1) : '1.0'} + + )} +
) })()}