e1b5ae9aab
P0紧急修复(6项): - 草稿保存完整恢复所有字段(含socialAvgWage) - 补偿金批次从compensationBreakdown读取 - 违法解除风险确认UI - 合同结束日期前后校验(前后端双保险) P1高优先级(14项): - 离职日期联动社保/公积金截止月(15号规则) - 合规检查+工作交接改为软阻断(生成待办) - 补偿月数(N/N+1/2N/自定义)+计算基数(近12月/合同/自定义) - 解聘并入花名册操作栏(类型选择跳转向导) - 合同续签开始日期自动推导(原合同结束日+1天) - 年龄合规筛查(童工阻断/未成年工/退休警告) - 编辑入职日期后状态联动(待入职↔在职) - 转正移植到花名册操作栏+薪资回写 - 男职工无法选择三期 P2体验优化(9项): - "劳动合同"调整为"用工关系" - 费用结算新增剩余年假折算(300%日工资) - 身份证号全域改为"证件号码"(前后端18个文件) - 手机号查重 - 开具证明+合同续签移植到花名册操作栏 - 批量转正+批量开具证明 - 去掉用工办理模块 P3规划(2项): - 组织架构+审批流(Department/Position/ApprovalFlow/ApprovalInstance) - 客服工作台(Ticket/ChatSession+SUPPORT角色) 新增模型: Department/Position/ApprovalFlow/ApprovalInstance/Ticket/TicketMessage/ChatSession/ChatMessage 新增字段: Employee.departmentId/supervisorId 新增角色: SUPPORT Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
import crypto from 'crypto'
|
|
import prisma from '../lib/prisma'
|
|
|
|
// 从证件号码提取出生日期
|
|
export function extractBirthDateFromIdCard(idCard: string): Date | null {
|
|
// 18位身份证:7-14位为出生日期 YYYYMMDD
|
|
if (idCard.length === 18) {
|
|
const year = parseInt(idCard.substring(6, 10))
|
|
const month = parseInt(idCard.substring(10, 12))
|
|
const day = parseInt(idCard.substring(12, 14))
|
|
if (year && month && day) {
|
|
return new Date(year, month - 1, day)
|
|
}
|
|
}
|
|
// 15位老身份证:7-12位为出生日期 YYMMDD
|
|
if (idCard.length === 15) {
|
|
const year = parseInt('19' + idCard.substring(6, 8))
|
|
const month = parseInt(idCard.substring(8, 10))
|
|
const day = parseInt(idCard.substring(10, 12))
|
|
if (year && month && day) {
|
|
return new Date(year, month - 1, day)
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// 从证件号码提取性别(18位:第17位奇数为男,偶数为女;15位:第15位)
|
|
export function extractGenderFromIdCard(idCard: string): string | null {
|
|
if (idCard.length === 18) {
|
|
const genderCode = parseInt(idCard.substring(16, 17))
|
|
return genderCode % 2 === 1 ? '男' : '女'
|
|
}
|
|
if (idCard.length === 15) {
|
|
const genderCode = parseInt(idCard.substring(14, 15))
|
|
return genderCode % 2 === 1 ? '男' : '女'
|
|
}
|
|
return null
|
|
}
|
|
|
|
// 渐进式延迟退休固定算法
|
|
// 改革起始日:2025-01-01,过渡期15年至2039-12-31
|
|
// 男性:60→63岁,每4个月延迟1个月
|
|
// 女性干部:55→58岁,每4个月延迟1个月
|
|
// 女性工人:50→55岁,每2个月延迟1个月
|
|
const REFORM_START = new Date(2025, 0, 1)
|
|
const BASE_MALE = 60
|
|
const BASE_FEMALE_CADRE = 55
|
|
const BASE_FEMALE_WORKER = 50
|
|
const TARGET_MALE = 63
|
|
const TARGET_FEMALE_CADRE = 58
|
|
const TARGET_FEMALE_WORKER = 55
|
|
const MALE_DELAY_INTERVAL = 4 // 每4个月延迟1个月
|
|
const FEMALE_CADRE_DELAY_INTERVAL = 4
|
|
const FEMALE_WORKER_DELAY_INTERVAL = 2 // 每2个月延迟1个月
|
|
|
|
// 计算从改革起始日到指定日期经过的完整月数
|
|
function calcMonthsSinceReform(date: Date = new Date()): number {
|
|
const months = (date.getFullYear() - REFORM_START.getFullYear()) * 12 + (date.getMonth() - REFORM_START.getMonth())
|
|
return Math.max(0, months)
|
|
}
|
|
|
|
// 根据出生日期和性别计算个人基准退休年龄
|
|
function getBaseRetireAge(gender: string, femaleWorkerType: string | null): number {
|
|
if (gender === '男') return BASE_MALE
|
|
if (gender === '女') {
|
|
return femaleWorkerType === 'WORKER' ? BASE_FEMALE_WORKER : BASE_FEMALE_CADRE
|
|
}
|
|
return BASE_MALE
|
|
}
|
|
|
|
// 根据出生日期和性别计算延迟间隔
|
|
function getDelayInterval(gender: string, femaleWorkerType: string | null): number {
|
|
if (gender === '男') return MALE_DELAY_INTERVAL
|
|
if (gender === '女') {
|
|
return femaleWorkerType === 'WORKER' ? FEMALE_WORKER_DELAY_INTERVAL : FEMALE_CADRE_DELAY_INTERVAL
|
|
}
|
|
return MALE_DELAY_INTERVAL
|
|
}
|
|
|
|
// 根据出生日期和性别计算最大延迟月数
|
|
function getMaxDelayMonths(gender: string, femaleWorkerType: string | null): number {
|
|
if (gender === '男') return (TARGET_MALE - BASE_MALE) * 12
|
|
if (gender === '女') {
|
|
return femaleWorkerType === 'WORKER'
|
|
? (TARGET_FEMALE_WORKER - BASE_FEMALE_WORKER) * 12
|
|
: (TARGET_FEMALE_CADRE - BASE_FEMALE_CADRE) * 12
|
|
}
|
|
return (TARGET_MALE - BASE_MALE) * 12
|
|
}
|
|
|
|
// 计算个人的实际退休年龄(基于出生日期)
|
|
// 原理:先算出此人达到基准退休年龄的日期,再算该日期距改革起始日过了多少个月,
|
|
// 每N个月延迟1个月,得到个人实际退休年龄
|
|
export function calcIndividualRetireAge(birthDate: Date, gender: string, femaleWorkerType: string | null = null): {
|
|
retireAge: number
|
|
retireDate: Date
|
|
delayMonths: number
|
|
} {
|
|
const baseAge = getBaseRetireAge(gender, femaleWorkerType)
|
|
const delayInterval = getDelayInterval(gender, femaleWorkerType)
|
|
const maxDelay = getMaxDelayMonths(gender, femaleWorkerType)
|
|
|
|
// 达到基准退休年龄的日期
|
|
const baseRetireDate = new Date(birthDate)
|
|
baseRetireDate.setFullYear(baseRetireDate.getFullYear() + baseAge)
|
|
|
|
// 从改革起始日到达到基准退休年龄日期,经过的月数
|
|
const monthsSinceReform = calcMonthsSinceReform(baseRetireDate)
|
|
|
|
// 延迟月数 = floor(月数 / 间隔),不超过最大延迟
|
|
const delayMonths = Math.min(maxDelay, Math.floor(monthsSinceReform / delayInterval))
|
|
|
|
// 实际退休年龄 = 基准年龄 + 延迟月数/12
|
|
const retireAge = baseAge + delayMonths / 12
|
|
|
|
// 实际退休日期 = 基准退休日期 + 延迟月数
|
|
const retireDate = new Date(baseRetireDate)
|
|
retireDate.setMonth(retireDate.getMonth() + delayMonths)
|
|
|
|
return { retireAge, retireDate, delayMonths }
|
|
}
|
|
|
|
// 计算当前改革进度(用于政策展示)
|
|
export function calcCurrentRetireAges(date: Date = new Date()): {
|
|
maleRetireAge: number
|
|
femaleRetireAge: number
|
|
femaleWorkerAge: number
|
|
description: string
|
|
} {
|
|
const monthsElapsed = calcMonthsSinceReform(date)
|
|
|
|
const maleDelayMonths = Math.min(36, Math.floor(monthsElapsed / MALE_DELAY_INTERVAL))
|
|
const femaleCadreDelayMonths = Math.min(36, Math.floor(monthsElapsed / FEMALE_CADRE_DELAY_INTERVAL))
|
|
const femaleWorkerDelayMonths = Math.min(60, Math.floor(monthsElapsed / FEMALE_WORKER_DELAY_INTERVAL))
|
|
|
|
const maleRetireAge = BASE_MALE + maleDelayMonths / 12
|
|
const femaleRetireAge = BASE_FEMALE_CADRE + femaleCadreDelayMonths / 12
|
|
const femaleWorkerAge = BASE_FEMALE_WORKER + femaleWorkerDelayMonths / 12
|
|
|
|
const description = `渐进式延迟退休改革(2025年1月1日起实施)
|
|
|
|
【改革规则】
|
|
- 男性职工:60岁 → 63岁,每4个月延迟1个月
|
|
- 女性干部/管理岗:55岁 → 58岁,每4个月延迟1个月
|
|
- 女性工人/操作岗:50岁 → 55岁,每2个月延迟1个月
|
|
|
|
【当前进度】截至${date.getFullYear()}年${date.getMonth() + 1}月,改革已实施${monthsElapsed}个月
|
|
- 男性当月退休年龄:${BASE_MALE}岁${maleDelayMonths > 0 ? `+${maleDelayMonths}个月` : ''}
|
|
- 女性干部当月退休年龄:${BASE_FEMALE_CADRE}岁${femaleCadreDelayMonths > 0 ? `+${femaleCadreDelayMonths}个月` : ''}
|
|
- 女性工人当月退休年龄:${BASE_FEMALE_WORKER}岁${femaleWorkerDelayMonths > 0 ? `+${femaleWorkerDelayMonths}个月` : ''}
|
|
|
|
【个人退休年龄】根据出生年月逐人计算:
|
|
达到基准退休年龄的时间点不同,延迟月数也不同。
|
|
例如:1965年1月出生的男性,2025年1月满60岁,延迟0个月,60岁退休;1965年5月出生的男性,2025年5月满60岁,延迟1个月,60岁1个月退休。
|
|
|
|
改革依据:2024年9月13日全国人大常委会《关于实施渐进式延迟法定退休年龄的决定》
|
|
过渡期:2025年1月1日至2039年12月31日(15年)`
|
|
|
|
return { maleRetireAge, femaleRetireAge, femaleWorkerAge, description }
|
|
}
|
|
|
|
// 计算距退休天数(基于个人出生日期逐人计算)
|
|
export function calcRetirementDaysLeft(birthDate: Date, gender: string, femaleWorkerType: string | null = null): number | null {
|
|
const { retireDate } = calcIndividualRetireAge(birthDate, gender, femaleWorkerType)
|
|
|
|
const now = new Date()
|
|
const diffMs = retireDate.getTime() - now.getTime()
|
|
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
// 检查并获取最新退休政策(懒加载:每月最多获取一次,存为 PENDING 待用户确认)
|
|
export async function checkAndUpdateRetirementPolicy(orgId: string): Promise<void> {
|
|
const now = new Date()
|
|
const latestPolicy = await prisma.retirementPolicy.findFirst({
|
|
where: { orgId },
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
|
|
// 如果本月已获取过(无论是否确认),则跳过
|
|
if (latestPolicy) {
|
|
const updatedThisMonth = latestPolicy.createdAt.getMonth() === now.getMonth() &&
|
|
latestPolicy.createdAt.getFullYear() === now.getFullYear()
|
|
if (updatedThisMonth) return
|
|
}
|
|
|
|
// 用固定算法计算当前退休年龄
|
|
const { maleRetireAge, femaleRetireAge, femaleWorkerAge, description } = calcCurrentRetireAges(now)
|
|
|
|
// 计算内容 hash,判断内容是否变化
|
|
const contentHash = crypto.createHash('sha256').update(description).digest('hex')
|
|
|
|
// 如果最新政策的 hash 相同,则不新增版本
|
|
if (latestPolicy && latestPolicy.contentHash === contentHash) {
|
|
return
|
|
}
|
|
|
|
// 新建版本(PENDING 状态,待用户确认)
|
|
const version = latestPolicy ? latestPolicy.version + 1 : 1
|
|
await prisma.retirementPolicy.create({
|
|
data: {
|
|
orgId,
|
|
version,
|
|
content: description,
|
|
contentHash,
|
|
status: 'PENDING',
|
|
maleRetireAge,
|
|
femaleRetireAge,
|
|
femaleWorkerAge,
|
|
},
|
|
})
|
|
}
|
|
|
|
// 用户确认退休政策,正式生效并更新员工退休天数
|
|
export async function confirmRetirementPolicy(orgId: string, policyId: string): Promise<void> {
|
|
const policy = await prisma.retirementPolicy.findFirst({
|
|
where: { id: policyId, orgId },
|
|
})
|
|
if (!policy) throw new Error('政策不存在')
|
|
if (policy.status === 'CONFIRMED') return
|
|
|
|
// 将旧的 CONFIRMED 政策标记为 SUPERSEDED
|
|
await prisma.retirementPolicy.updateMany({
|
|
where: { orgId, status: 'CONFIRMED', id: { not: policyId } },
|
|
data: { status: 'SUPERSEDED' },
|
|
})
|
|
|
|
// 确认新政策生效
|
|
await prisma.retirementPolicy.update({
|
|
where: { id: policyId },
|
|
data: { status: 'CONFIRMED', confirmedAt: new Date() },
|
|
})
|
|
|
|
// 更新所有正式合同员工的距退休天数
|
|
await updateEmployeesRetirementDays(orgId)
|
|
}
|
|
|
|
// 批量更新员工距退休天数(基于个人出生日期 individually 计算)
|
|
export async function updateEmployeesRetirementDays(orgId: string): Promise<void> {
|
|
// 查询有正式劳动合同的员工
|
|
const employees = await prisma.employee.findMany({
|
|
where: {
|
|
orgId,
|
|
contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } },
|
|
},
|
|
select: { id: true, birthDate: true, gender: true, femaleWorkerType: true },
|
|
})
|
|
|
|
for (const emp of employees) {
|
|
if (!emp.birthDate) continue
|
|
const gender = emp.gender || '男'
|
|
const daysLeft = calcRetirementDaysLeft(emp.birthDate, gender, emp.femaleWorkerType)
|
|
if (daysLeft !== null) {
|
|
await prisma.employee.update({
|
|
where: { id: emp.id },
|
|
data: { retirementDaysLeft: daysLeft },
|
|
})
|
|
}
|
|
}
|
|
}
|