feat: 20260809 系统优化 - 全部28项问题修复(P0×6+P1×16+P2×6)
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除 P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量 P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
This commit is contained in:
@@ -279,8 +279,8 @@ export async function manualCorrectAttendance(orgId: string, data: {
|
||||
where: { orgId, employeeId: data.employeeId, date: { gte: day, lt: nextDay } },
|
||||
})
|
||||
|
||||
const checkInTime = data.checkInTime ? new Date(`${data.date}T${data.checkInTime}`).toISOString() : null
|
||||
const checkOutTime = data.checkOutTime ? new Date(`${data.date}T${data.checkOutTime}`).toISOString() : null
|
||||
const checkInTime = data.checkInTime ? new Date(`${data.date}T${data.checkInTime}:00Z`).toISOString() : null
|
||||
const checkOutTime = data.checkOutTime ? new Date(`${data.date}T${data.checkOutTime}:00Z`).toISOString() : null
|
||||
|
||||
let workHours = 0
|
||||
if (checkInTime && checkOutTime) {
|
||||
@@ -388,10 +388,11 @@ export async function getMonthlyReport(orgId: string, month: string) {
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
|
||||
const otMap = new Map<string, number>()
|
||||
const otMap = new Map<string, { hours: number; pay: number }>()
|
||||
for (const ot of overtimes) {
|
||||
const totalHours = (ot.weekdayHours || 0) + (ot.weekendHours || 0) + (ot.holidayHours || 0)
|
||||
otMap.set(ot.employeeId, (otMap.get(ot.employeeId) || 0) + totalHours)
|
||||
const prev = otMap.get(ot.employeeId) || { hours: 0, pay: 0 }
|
||||
otMap.set(ot.employeeId, { hours: prev.hours + totalHours, pay: prev.pay + (ot.totalPay || 0) })
|
||||
}
|
||||
|
||||
const leaveMap = new Map<string, number>()
|
||||
@@ -412,8 +413,8 @@ export async function getMonthlyReport(orgId: string, month: string) {
|
||||
earlyLeaveCount: empRecords.filter(r => r.status === 'EARLY_LEAVE').length,
|
||||
absentDays: empRecords.filter(r => r.status === 'ABSENT').length,
|
||||
leaveDays: leaveMap.get(emp.id) || 0,
|
||||
overtimeHours: confirmation ? (confirmation.weekdayHours + confirmation.weekendHours + confirmation.holidayHours) : (otMap.get(emp.id) || 0),
|
||||
overtimePay: confirmation?.overtimePay || 0,
|
||||
overtimeHours: confirmation ? (confirmation.weekdayHours + confirmation.weekendHours + confirmation.holidayHours) : (otMap.get(emp.id)?.hours || 0),
|
||||
overtimePay: confirmation?.overtimePay || otMap.get(emp.id)?.pay || 0,
|
||||
confirmationStatus: confirmation?.status || null,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,6 +13,24 @@ function dateToMonth(date: Date): string {
|
||||
return `${y}-${m}`
|
||||
}
|
||||
|
||||
async function clampSocialInsBase(orgId: string, base: number, city?: string): Promise<number> {
|
||||
const config = await prisma.socialInsuranceConfig.findFirst({
|
||||
where: { orgId, ...(city ? { city } : {}) },
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (config) return Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
return base
|
||||
}
|
||||
|
||||
async function clampHousingFundBase(orgId: string, base: number, city?: string): Promise<number> {
|
||||
const config = await prisma.housingFundConfig.findFirst({
|
||||
where: { orgId, ...(city ? { city } : {}) },
|
||||
orderBy: { effectiveFrom: 'desc' },
|
||||
})
|
||||
if (config) return Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
return base
|
||||
}
|
||||
|
||||
function prevMonth(month: string): string {
|
||||
const [y, m] = month.split('-').map(Number)
|
||||
const d = new Date(y, m - 2, 1)
|
||||
@@ -191,6 +209,17 @@ export async function getEmployeeDetail(orgId: string, id: string) {
|
||||
}
|
||||
|
||||
export async function createEmployee(orgId: string, userId: string, data: any) {
|
||||
// 身份证号查重
|
||||
if (data.idCardNumber) {
|
||||
const existing = await prisma.employee.findFirst({
|
||||
where: { orgId, idCardHash: sha256(data.idCardNumber) },
|
||||
select: { id: true, name: true, department: true, status: true },
|
||||
})
|
||||
if (existing) {
|
||||
throw { code: 'DUPLICATE_ID_CARD', message: `身份证号已存在:${existing.name}(${existing.department},${existing.status === 'ACTIVE' ? '在职' : '离职'}),请确认是否重复录入` }
|
||||
}
|
||||
}
|
||||
|
||||
const org = await prisma.organization.findUnique({ where: { id: orgId } })
|
||||
if (org && org.maxEmployees > 0) {
|
||||
const activeCount = await prisma.employee.count({ where: { orgId, status: 'ACTIVE' } })
|
||||
@@ -202,8 +231,11 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
|
||||
const hireDate = new Date(data.hireDate)
|
||||
const hireMonth = dateToMonth(hireDate)
|
||||
const salaryNum = Number(data.monthlySalary) || 0
|
||||
const socialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
|
||||
const housingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
|
||||
const city = data.city || '北京'
|
||||
const rawSocialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
|
||||
const rawHousingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
|
||||
const socialInsBase = await clampSocialInsBase(orgId, rawSocialInsBase, city)
|
||||
const housingFundBase = await clampHousingFundBase(orgId, rawHousingFundBase, city)
|
||||
const socialInsStartMonth = data.socialInsStartMonth || hireMonth
|
||||
const housingFundStartMonth = data.housingFundStartMonth || hireMonth
|
||||
|
||||
@@ -230,6 +262,7 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
|
||||
createdBy: userId,
|
||||
city: data.city || '北京',
|
||||
education: data.education || null,
|
||||
position: data.position || null,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -346,8 +379,11 @@ export async function rehireEmployee(orgId: string, userId: string, id: string,
|
||||
|
||||
const newHireMonth = dateToMonth(newHireDate)
|
||||
const salaryNum = Number(decrypt(employee.monthlySalary)) || 0
|
||||
const socialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
|
||||
const housingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
|
||||
const city = data.city || employee.city || '北京'
|
||||
const rawSocialInsBase = data.socialInsBase != null ? Number(data.socialInsBase) : salaryNum
|
||||
const rawHousingFundBase = data.housingFundBase != null ? Number(data.housingFundBase) : salaryNum
|
||||
const socialInsBase = await clampSocialInsBase(orgId, rawSocialInsBase, city)
|
||||
const housingFundBase = await clampHousingFundBase(orgId, rawHousingFundBase, city)
|
||||
const socialInsStartMonth = data.socialInsStartMonth || newHireMonth
|
||||
const housingFundStartMonth = data.housingFundStartMonth || newHireMonth
|
||||
const prevHireMonth = prevMonth(newHireMonth)
|
||||
@@ -543,11 +579,18 @@ export async function updateEmployee(orgId: string, id: string, data: any) {
|
||||
if (data.isPregnant !== undefined) updateData.isPregnant = data.isPregnant
|
||||
if (data.isInMedicalPeriod !== undefined) updateData.isInMedicalPeriod = data.isInMedicalPeriod
|
||||
if (data.isWorkInjured !== undefined) updateData.isWorkInjured = data.isWorkInjured
|
||||
if (data.socialInsBase !== undefined) updateData.socialInsBase = data.socialInsBase
|
||||
if (data.housingFundBase !== undefined) updateData.housingFundBase = data.housingFundBase
|
||||
if (data.socialInsBase !== undefined) {
|
||||
const city = data.city || employee.city || '北京'
|
||||
updateData.socialInsBase = await clampSocialInsBase(orgId, Number(data.socialInsBase), city)
|
||||
}
|
||||
if (data.housingFundBase !== undefined) {
|
||||
const city = data.city || employee.city || '北京'
|
||||
updateData.housingFundBase = await clampHousingFundBase(orgId, Number(data.housingFundBase), city)
|
||||
}
|
||||
if (data.specialDeduction !== undefined) updateData.specialDeduction = data.specialDeduction
|
||||
if (data.city !== undefined) updateData.city = data.city
|
||||
if (data.education !== undefined) updateData.education = data.education
|
||||
if (data.position !== undefined) updateData.position = data.position
|
||||
|
||||
// 参保城市变更:关闭旧城市在保记录,创建新城市记录
|
||||
if (data.city !== undefined && data.city !== employee.city) {
|
||||
|
||||
@@ -185,6 +185,7 @@ export async function verifyAllEvidence(orgId: string) {
|
||||
const records = await prisma.evidenceChain.findMany({ where: { orgId } })
|
||||
let valid = 0
|
||||
let invalid = 0
|
||||
const invalidItems: any[] = []
|
||||
for (const r of records) {
|
||||
const events = r.events as any[]
|
||||
const eventsJson = JSON.stringify(events)
|
||||
@@ -198,8 +199,16 @@ export async function verifyAllEvidence(orgId: string) {
|
||||
})
|
||||
const sortedJson = JSON.stringify(sortedEvents)
|
||||
const sortedHash = sha256(sortedJson + orgId + r.category + (r.refId || ''))
|
||||
if (sortedHash === r.hash) valid++
|
||||
else invalid++
|
||||
if (sortedHash === r.hash) { valid++; continue }
|
||||
invalid++
|
||||
invalidItems.push({
|
||||
id: r.id,
|
||||
category: r.category,
|
||||
refId: r.refId,
|
||||
employeeId: r.employeeId,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
description: `证据链 ${r.category}${r.refId ? `(${r.refId})` : ''} 哈希校验失败,可能被篡改`,
|
||||
})
|
||||
}
|
||||
return { total: records.length, valid, invalid }
|
||||
return { total: records.length, valid, invalid, invalidItems }
|
||||
}
|
||||
|
||||
@@ -260,29 +260,31 @@ export async function generateDocument(type: string, formData: any, orgName: str
|
||||
}
|
||||
}
|
||||
|
||||
const wrapHtml = (title: string, body: string) => `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"><title>${title}</title>
|
||||
<style>
|
||||
body { font-family: SimSun, serif; font-size: 14pt; line-height: 2; text-align: center; }
|
||||
.title { font-size: 22pt; font-weight: bold; margin-bottom: 30pt; }
|
||||
.body { text-align: justify; text-indent: 2em; margin: 0 20pt; }
|
||||
.sign { text-align: right; margin-top: 30pt; margin-right: 20pt; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="title">${title}</div>
|
||||
${body}
|
||||
</body></html>`
|
||||
|
||||
const templates: Record<string, (data: any, org: string) => string> = {
|
||||
INCOME_CERT: (data, org) => `收入证明
|
||||
|
||||
兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})系我单位员工,自 ${data.hireDate || '___'} 起在我单位工作,现任 ${data.position || '___'} 职务。
|
||||
|
||||
该员工近一年平均月收入为人民币 ${data.monthlyIncome || '___'} 元(税前)。
|
||||
|
||||
本证明仅用于 ${data.purpose || '___'},不作其他用途。
|
||||
|
||||
特此证明。
|
||||
|
||||
${org}
|
||||
${new Date().toLocaleDateString('zh-CN')}`,
|
||||
LEAVING_CERT: (data, org) => `离职证明
|
||||
|
||||
兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})自 ${data.hireDate || '___'} 至 ${data.leaveDate || '___'} 在我单位工作,最后职务为 ${data.position || '___'}。
|
||||
|
||||
该员工已于 ${data.leaveDate || '___'} 与我单位解除劳动关系,双方已办妥交接手续。
|
||||
|
||||
特此证明。
|
||||
|
||||
${org}
|
||||
${new Date().toLocaleDateString('zh-CN')}`,
|
||||
INCOME_CERT: (data, org) => wrapHtml('收入证明', `
|
||||
<div class="body">兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})系我单位员工,自 ${data.hireDate || '___'} 起在我单位工作,现任 ${data.position || '___'} 职务。</div>
|
||||
<div class="body">该员工近一年平均月收入为人民币 ${data.monthlyIncome || '___'} 元(税前)。</div>
|
||||
<div class="body">本证明仅用于 ${data.purpose || '___'},不作其他用途。</div>
|
||||
<div class="body">特此证明。</div>
|
||||
<div class="sign">${org}<br/>${new Date().toLocaleDateString('zh-CN')}</div>`),
|
||||
LEAVING_CERT: (data, org) => wrapHtml('离职证明', `
|
||||
<div class="body">兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})自 ${data.hireDate || '___'} 至 ${data.leaveDate || '___'} 在我单位工作,最后职务为 ${data.position || '___'}。</div>
|
||||
<div class="body">该员工已于 ${data.leaveDate || '___'} 与我单位解除劳动关系,双方已办妥交接手续。</div>
|
||||
<div class="body">特此证明。</div>
|
||||
<div class="sign">${org}<br/>${new Date().toLocaleDateString('zh-CN')}</div>`),
|
||||
}
|
||||
const generator = templates[type]
|
||||
if (!generator) return { name: '', content: '' }
|
||||
|
||||
Reference in New Issue
Block a user