fix: 绩效/培训/违纪/工资条/加班/休假/专项扣除/薪酬批次列表姓名下显示完整身份证号
后端: - roster.routes.ts: 绩效/培训/违纪列表查询加上 idCardNumber 并解密 - payroll.routes.ts: 加班/工资条查询加上 idCardNumber 并解密 - payroll2.routes.ts: 薪酬批次详情加上 idCardNumber 并解密 - leave.routes.ts: 休假审批列表加上 idCardNumber 并解密 - social.routes.ts: 专项附加扣除查询加上 idCardNumber 并解密 前端: - PerformanceRecords.tsx: 姓名下显示身份证号 - TrainingRecords.tsx: 姓名下显示身份证号 - DisciplinaryRecords.tsx: 姓名下显示身份证号 - PayslipTab.tsx: 姓名下显示身份证号 - OvertimeTab.tsx: 姓名下显示身份证号 - LeaveApproval.tsx: 姓名下显示身份证号 - SpecialDeductionTab.tsx: 姓名下显示身份证号 - BatchTab.tsx: 姓名下显示身份证号
This commit is contained in:
@@ -2,9 +2,18 @@ import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { parsePagination } from '../lib/pagination'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function safeDecryptStr(encrypted: string | null): string | null {
|
||||
if (!encrypted) return null
|
||||
try {
|
||||
if (!encrypted.includes(':')) return encrypted
|
||||
return decrypt(encrypted)
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
// 休假申请列表
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
@@ -21,14 +30,18 @@ router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: Ne
|
||||
const list = await prisma.leaveRequest.findMany({
|
||||
where,
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, position: true } },
|
||||
employee: { select: { id: true, name: true, department: true, position: true, idCardNumber: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
|
||||
res.json({ success: true, data: { list, total, page, pageSize } })
|
||||
const data = list.map((r: any) => ({
|
||||
...r,
|
||||
employee: r.employee ? { ...r.employee, idCardNumber: safeDecryptStr(r.employee.idCardNumber) } : r.employee,
|
||||
}))
|
||||
res.json({ success: true, data: { list: data, total, page, pageSize } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@ import { isInProbation } from '../services/contract.service'
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
function safeDecryptStr(encrypted: string | null): string | null {
|
||||
if (!encrypted) return null
|
||||
try {
|
||||
if (!encrypted.includes(':')) return encrypted
|
||||
return decrypt(encrypted)
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
// ========== 加班费记录 ==========
|
||||
|
||||
const overtimeSchema = z.object({
|
||||
@@ -29,10 +37,14 @@ router.get('/overtime', async (req: AuthRequest, res: Response, next: NextFuncti
|
||||
...(employeeId ? { employeeId: String(employeeId) } : {}),
|
||||
...(month ? { month: String(month) } : {}),
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
include: { employee: { select: { id: true, name: true, department: true, idCardNumber: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
const data = records.map((r: any) => ({
|
||||
...r,
|
||||
employee: r.employee ? { ...r.employee, idCardNumber: safeDecryptStr(r.employee.idCardNumber) } : r.employee,
|
||||
}))
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
@@ -236,10 +248,14 @@ router.get('/payslip', async (req: AuthRequest, res: Response, next: NextFunctio
|
||||
...(month ? { month: String(month) } : {}),
|
||||
...(employeeId ? { employeeId: String(employeeId) } : {}),
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
include: { employee: { select: { id: true, name: true, department: true, idCardNumber: true } } },
|
||||
orderBy: [{ month: 'desc' }, { employee: { name: 'asc' } }],
|
||||
})
|
||||
res.json({ success: true, data: payslips })
|
||||
const data = payslips.map((p: any) => ({
|
||||
...p,
|
||||
employee: p.employee ? { ...p.employee, idCardNumber: safeDecryptStr(p.employee.idCardNumber) } : p.employee,
|
||||
}))
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@ import {
|
||||
import { isInProbation } from '../services/contract.service'
|
||||
import { getBonusByMonthAndEmployeeIds } from '../services/commission-bonus.service'
|
||||
|
||||
function safeDecryptStr(encrypted: string | null): string | null {
|
||||
if (!encrypted) return null
|
||||
try {
|
||||
if (!encrypted.includes(':')) return encrypted
|
||||
return decrypt(encrypted)
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询上月递延扣款数据(上月已归档批次中该员工的递延金额)
|
||||
* 用于本月创建批次时补扣上月未扣完的社保/公积金/最低工资补齐
|
||||
@@ -209,18 +217,25 @@ router.get('/batches', async (req: AuthRequest, res: Response, next: NextFunctio
|
||||
// 获取批次详情
|
||||
router.get('/batches/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const batch = await prisma.payrollBatch.findFirst({
|
||||
const rawBatch = await prisma.payrollBatch.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
include: {
|
||||
entries: {
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true } },
|
||||
employee: { select: { id: true, name: true, department: true, status: true, bankAccount: true, bankName: true, idCardNumber: true } },
|
||||
},
|
||||
orderBy: { employee: { name: 'asc' } },
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!batch) 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: '批次不存在' } })
|
||||
const batch = {
|
||||
...rawBatch,
|
||||
entries: rawBatch.entries.map((e: any) => ({
|
||||
...e,
|
||||
employee: e.employee ? { ...e.employee, idCardNumber: safeDecryptStr(e.employee.idCardNumber) } : e.employee,
|
||||
})),
|
||||
}
|
||||
res.json({ success: true, data: batch })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
|
||||
@@ -850,16 +850,20 @@ router.get('/training/list', authMiddleware, async (req: AuthRequest, res, next)
|
||||
})
|
||||
where.employeeId = { in: employees.map(e => e.id) }
|
||||
}
|
||||
const [records, total] = await Promise.all([
|
||||
const [rawRecords, total] = await Promise.all([
|
||||
prisma.trainingRecord.findMany({
|
||||
where,
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
include: { employee: { select: { id: true, name: true, department: true, idCardNumber: true } } },
|
||||
orderBy: { trainingDate: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.trainingRecord.count({ where }),
|
||||
])
|
||||
const records = rawRecords.map((r: any) => ({
|
||||
...r,
|
||||
employee: r.employee ? { ...r.employee, idCardNumber: safeDecryptStr(r.employee.idCardNumber) } : r.employee,
|
||||
}))
|
||||
res.json({ success: true, data: { records, total, page, pageSize } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
@@ -908,16 +912,20 @@ router.get('/performance/list', authMiddleware, async (req: AuthRequest, res, ne
|
||||
})
|
||||
where.employeeId = { in: employees.map(e => e.id) }
|
||||
}
|
||||
const [records, total] = await Promise.all([
|
||||
const [rawRecords, total] = await Promise.all([
|
||||
prisma.performanceRecord.findMany({
|
||||
where,
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
include: { employee: { select: { id: true, name: true, department: true, idCardNumber: true } } },
|
||||
orderBy: { period: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.performanceRecord.count({ where }),
|
||||
])
|
||||
const records = rawRecords.map((r: any) => ({
|
||||
...r,
|
||||
employee: r.employee ? { ...r.employee, idCardNumber: safeDecryptStr(r.employee.idCardNumber) } : r.employee,
|
||||
}))
|
||||
res.json({ success: true, data: { records, total, page, pageSize } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
@@ -937,16 +945,20 @@ router.get('/disciplinary/list', authMiddleware, async (req: AuthRequest, res, n
|
||||
})
|
||||
where.employeeId = { in: employees.map(e => e.id) }
|
||||
}
|
||||
const [records, total] = await Promise.all([
|
||||
const [rawRecords, total] = await Promise.all([
|
||||
prisma.disciplinaryRecord.findMany({
|
||||
where,
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
include: { employee: { select: { id: true, name: true, department: true, idCardNumber: true } } },
|
||||
orderBy: { violationDate: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.disciplinaryRecord.count({ where }),
|
||||
])
|
||||
const records = rawRecords.map((r: any) => ({
|
||||
...r,
|
||||
employee: r.employee ? { ...r.employee, idCardNumber: safeDecryptStr(r.employee.idCardNumber) } : r.employee,
|
||||
}))
|
||||
res.json({ success: true, data: { records, total, page, pageSize } })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
@@ -1744,10 +1744,14 @@ router.get('/special-deduction/batch', async (req: AuthRequest, res: Response, n
|
||||
try {
|
||||
const { month } = req.query
|
||||
if (!month) return res.status(400).json({ success: false, message: '缺少 month' })
|
||||
const records = await prisma.specialDeductionRecord.findMany({
|
||||
const rawRecords = await prisma.specialDeductionRecord.findMany({
|
||||
where: { orgId: req.user!.orgId, month: month as string },
|
||||
include: { employee: { select: { name: true, department: true } } },
|
||||
include: { employee: { select: { name: true, department: true, idCardNumber: true } } },
|
||||
})
|
||||
const records = rawRecords.map((r: any) => ({
|
||||
...r,
|
||||
employee: r.employee ? { ...r.employee, idCardNumber: decryptIdCard(r.employee.idCardNumber) } : r.employee,
|
||||
}))
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
|
||||
@@ -167,8 +167,11 @@ export default function LeaveApproval() {
|
||||
return (
|
||||
<tr key={item.id} className="border-b last:border-0 hover:bg-gray-50">
|
||||
<td className="py-2.5 px-3">
|
||||
<div className="text-xs font-medium">{item.employee?.name}</div>
|
||||
<div className="text-xs text-gray-500">{item.employee?.department}</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs font-medium">{item.employee?.name}</span>
|
||||
{item.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{item.employee.idCardNumber}</span>}
|
||||
<span className="text-xs text-gray-500">{item.employee?.department}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2.5 px-3 text-xs">{LEAVE_TYPE_MAP[item.leaveType] || item.leaveType}</td>
|
||||
<td className="py-2.5 px-3 text-xs text-gray-600">
|
||||
|
||||
@@ -1194,11 +1194,14 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
{batch.entries.slice((page - 1) * pageSize, page * pageSize).map((entry: any) => (
|
||||
<tr key={entry.id} className="border-b last:border-0 hover:bg-gray-25">
|
||||
<td className="py-2 px-2">
|
||||
<div className="text-xs font-medium">{entry.employee.name}</div>
|
||||
<div className="text-xs text-gray-500">{entry.employee.department}</div>
|
||||
{entry.employee.status === 'RESIGNED' && (
|
||||
<span className="text-xs text-danger">已离职</span>
|
||||
)}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="text-xs font-medium">{entry.employee.name}</div>
|
||||
{entry.employee.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{entry.employee.idCardNumber}</span>}
|
||||
<div className="text-xs text-gray-500">{entry.employee.department}</div>
|
||||
{entry.employee.status === 'RESIGNED' && (
|
||||
<span className="text-xs text-danger">已离职</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
{renderCell(entry, 'baseSalary')}
|
||||
{renderCell(entry, 'overtimePay')}
|
||||
|
||||
@@ -425,7 +425,12 @@ export function OvertimeCalculator() {
|
||||
<tbody>
|
||||
{overtimeRecords.map((r: any) => (
|
||||
<tr key={r.id} className="border-b last:border-0 hover:bg-gray-50">
|
||||
<td className="py-2">{r.employee?.name}</td>
|
||||
<td className="py-2">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{r.employee?.name}</span>
|
||||
{r.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{r.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 text-gray-500">{r.employee?.department}</td>
|
||||
{editingId === r.id ? (
|
||||
<>
|
||||
|
||||
@@ -194,7 +194,12 @@ export function PayslipManager({ filterEmployeeId }: { filterEmployeeId?: string
|
||||
<tbody>
|
||||
{payslips.slice((page - 1) * pageSize, page * pageSize).map((p: any) => (
|
||||
<tr key={p.id} className="border-b last:border-0 hover:bg-gray-50">
|
||||
<td className="py-2 px-2 text-sm font-medium">{p.employee?.name}</td>
|
||||
<td className="py-2 px-2 text-sm font-medium">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{p.employee?.name}</span>
|
||||
{p.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{p.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-2 text-gray-500">{p.employee?.department}</td>
|
||||
<td className="py-2 px-2 text-right">¥{fmt(p.baseSalary)}</td>
|
||||
<td className="py-2 px-2 text-right">¥{fmt(p.overtimePay)}</td>
|
||||
|
||||
@@ -121,7 +121,10 @@ export default function DisciplinaryRecords() {
|
||||
) : records.map((r: any) => (
|
||||
<tr key={r.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 pr-4">
|
||||
<Link to={`/roster?employeeId=${r.employeeId}`} className="text-primary hover:underline">{r.employee?.name}</Link>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Link to={`/roster?employeeId=${r.employeeId}`} className="text-primary hover:underline">{r.employee?.name}</Link>
|
||||
{r.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{r.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-gray-600">{r.employee?.department || '-'}</td>
|
||||
<td className="py-2 pr-4">{fmtDate(r.violationDate)}</td>
|
||||
|
||||
@@ -124,7 +124,10 @@ export default function PerformanceRecords() {
|
||||
) : records.map((r: any) => (
|
||||
<tr key={r.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 pr-4">
|
||||
<Link to={`/roster?employeeId=${r.employeeId}`} className="text-primary hover:underline">{r.employee?.name}</Link>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Link to={`/roster?employeeId=${r.employeeId}`} className="text-primary hover:underline">{r.employee?.name}</Link>
|
||||
{r.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{r.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-gray-600">{r.employee?.department || '-'}</td>
|
||||
<td className="py-2 pr-4">{r.period}</td>
|
||||
|
||||
@@ -156,7 +156,10 @@ export default function TrainingRecords() {
|
||||
) : records.map((r: any) => (
|
||||
<tr key={r.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-2 pr-4">
|
||||
<Link to={`/roster?employeeId=${r.employeeId}`} className="text-primary hover:underline">{r.employee?.name}</Link>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Link to={`/roster?employeeId=${r.employeeId}`} className="text-primary hover:underline">{r.employee?.name}</Link>
|
||||
{r.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{r.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-gray-600">{r.employee?.department || '-'}</td>
|
||||
<td className="py-2 pr-4">{fmtDate(r.trainingDate)}</td>
|
||||
|
||||
@@ -172,7 +172,12 @@ export default function SpecialDeductionTab({ month, setMonth }: { month: string
|
||||
<tr key={r.id} className="border-b last:border-0 hover:bg-gray-50">
|
||||
{editing === r.employeeId ? (
|
||||
<>
|
||||
<td className="py-1.5">{r.employee?.name}</td>
|
||||
<td className="py-1.5">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{r.employee?.name}</span>
|
||||
{r.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{r.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-1.5 text-gray-500">{r.employee?.department}</td>
|
||||
<td className="py-1"><Input type="number" className="!w-20 !h-8 text-right" value={editForm.children} onChange={(e) => setEditForm({ ...editForm, children: Number(e.target.value) })} /></td>
|
||||
<td className="py-1"><Input type="number" className="!w-20 !h-8 text-right" value={editForm.elderly} onChange={(e) => setEditForm({ ...editForm, elderly: Number(e.target.value) })} /></td>
|
||||
@@ -190,7 +195,12 @@ export default function SpecialDeductionTab({ month, setMonth }: { month: string
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<td className="py-1.5">{r.employee?.name}</td>
|
||||
<td className="py-1.5">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span>{r.employee?.name}</span>
|
||||
{r.employee?.idCardNumber && <span className="text-gray-400 text-[11px] font-mono">{r.employee.idCardNumber}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-1.5 text-gray-500">{r.employee?.department}</td>
|
||||
<td className="py-1.5 text-right">{r.children > 0 ? `¥${fmt(r.children)}` : '-'}</td>
|
||||
<td className="py-1.5 text-right">{r.elderly > 0 ? `¥${fmt(r.elderly)}` : '-'}</td>
|
||||
|
||||
Reference in New Issue
Block a user