From 41a2c665ed8a16f5002f9c005d6532bebe547587 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Sat, 25 Jul 2026 09:27:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=80=80=E4=BC=91?= =?UTF-8?q?=E6=8F=90=E9=86=92=E5=8A=9F=E8=83=BD=20-=20=E4=B8=AA=E6=80=A7?= =?UTF-8?q?=E5=8C=96=E9=80=80=E4=BC=91=E5=B9=B4=E9=BE=84=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 FemaleWorkerType enum (CADRE/WORKER) 和 Employee.femaleWorkerType 字段 - 新增 calcIndividualRetireAge: 按个人出生日期逐人计算退休年龄 - 重写 calcRetirementDaysLeft: 基于个人出生日期+性别+岗位类型 - updateEmployeesRetirementDays 使用 femaleWorkerType 区分女干部/女工人 - 前端新增/编辑员工表单添加女性岗位类型选择器 - 前端政策展示改为改革规则卡片(基准→目标年龄) - 前端距退休天数改为退休日期显示(XXXX年XX月XX日) - 移除AI退休政策计算相关代码和手动刷新路由 - 导入支持女性岗位类型列 --- backend/prisma/schema.prisma | 6 + backend/src/routes/import.routes.ts | 2 + backend/src/routes/settings.routes.ts | 54 ------- backend/src/schemas/contract.schema.ts | 2 + backend/src/services/ai.service.ts | 118 --------------- backend/src/services/contract.service.ts | 2 + backend/src/services/retirement.service.ts | 160 +++++++++++++++++---- frontend/src/pages/Roster.tsx | 35 ++++- frontend/src/pages/Settings.tsx | 106 ++++---------- 9 files changed, 208 insertions(+), 277 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index b455137..69afd5f 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -26,6 +26,11 @@ enum EmployeeStatus { RESIGNED } +enum FemaleWorkerType { + CADRE // 干部/管理岗 + WORKER // 工人/操作岗 +} + enum ContractType { FIXED UNFIXED @@ -203,6 +208,7 @@ model Employee { specialDeduction Float @default(0) // 专项附加扣除(子女教育、赡养老人等,员工portal端填报) city String? // 员工社保参保城市 birthDate DateTime? // 出生日期(从身份证号提取) + femaleWorkerType FemaleWorkerType? // 女性岗位类型(CADRE=干部/WORKER=工人,仅女性需要区分) retirementDaysLeft Int? // 距退休天数(便捷字段,定期计算) createdBy String createdAt DateTime @default(now()) diff --git a/backend/src/routes/import.routes.ts b/backend/src/routes/import.routes.ts index 6f6000b..baae97b 100644 --- a/backend/src/routes/import.routes.ts +++ b/backend/src/routes/import.routes.ts @@ -246,6 +246,8 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async orgId, name, department: dept, hireDate, monthlySalary: encrypt(salary), gender: val(r['性别']) || (idCard ? extractGenderFromIdCard(idCard) : null), + femaleWorkerType: (val(r['女性岗位类型']) === '工人' || val(r['女性岗位类型']) === 'WORKER') ? 'WORKER' + : (val(r['女性岗位类型']) === '干部' || val(r['女性岗位类型']) === 'CADRE') ? 'CADRE' : null, phone: val(r['手机号']) || null, idCardNumber: idCard ? encrypt(idCard) : null, idCardHash: idCard ? sha256(idCard) : null, diff --git a/backend/src/routes/settings.routes.ts b/backend/src/routes/settings.routes.ts index adb5bc1..03d1d5b 100644 --- a/backend/src/routes/settings.routes.ts +++ b/backend/src/routes/settings.routes.ts @@ -241,57 +241,3 @@ router.post('/retirement-policy/:id/confirm', requireAdmin, async (req: AuthRequ } }) -// 手动触发获取最新退休政策(SSE 流式) -router.post('/retirement-policy/refresh', requireAdmin, async (req: AuthRequest, res, next) => { - try { - const orgId = req.user!.orgId - res.setHeader('Content-Type', 'text/event-stream') - res.setHeader('Cache-Control', 'no-cache') - res.setHeader('Connection', 'keep-alive') - - const { fetchRetirementPolicyStream } = await import('../services/ai.service') - const crypto = await import('crypto') - - let fullContent = '' - for await (const chunk of fetchRetirementPolicyStream()) { - fullContent += chunk - res.write(`data: ${JSON.stringify({ type: 'chunk', content: chunk })}\n\n`) - } - - // 解析 JSON - let maleRetireAge = 60 - let femaleRetireAge = 55 - let femaleWorkerAge = 50 - const jsonMatch = fullContent.match(/([\s\S]*?)<\/JSON>/) - if (jsonMatch) { - try { - const parsed = JSON.parse(jsonMatch[1].trim()) - if (parsed.maleRetireAge) maleRetireAge = Number(parsed.maleRetireAge) - if (parsed.femaleRetireAge) femaleRetireAge = Number(parsed.femaleRetireAge) - if (parsed.femaleWorkerAge) femaleWorkerAge = Number(parsed.femaleWorkerAge) - } catch {} - } - - const contentHash = crypto.createHash('sha256').update(fullContent).digest('hex') - const latestPolicy = await prisma.retirementPolicy.findFirst({ where: { orgId }, orderBy: { createdAt: 'desc' } }) - - if (latestPolicy && latestPolicy.contentHash === contentHash) { - res.write(`data: ${JSON.stringify({ type: 'done', message: '政策内容未变化' })}\n\n`) - res.end() - return - } - - const version = latestPolicy ? latestPolicy.version + 1 : 1 - const policy = await prisma.retirementPolicy.create({ - data: { - orgId, version, content: fullContent, contentHash, status: 'PENDING', - maleRetireAge, femaleRetireAge, femaleWorkerAge, - }, - }) - res.write(`data: ${JSON.stringify({ type: 'done', data: policy, message: '已获取新政策,待确认' })}\n\n`) - res.end() - } catch (err) { - res.write(`data: ${JSON.stringify({ type: 'error', message: '获取失败' })}\n\n`) - res.end() - } -}) diff --git a/backend/src/schemas/contract.schema.ts b/backend/src/schemas/contract.schema.ts index bdb2183..9a6473a 100644 --- a/backend/src/schemas/contract.schema.ts +++ b/backend/src/schemas/contract.schema.ts @@ -6,6 +6,7 @@ export const createEmployeeSchema = z.object({ hireDate: z.string().datetime(), monthlySalary: z.string().min(1, '月薪不能为空'), gender: z.enum(['男', '女']).optional(), + femaleWorkerType: z.enum(['CADRE', 'WORKER']).optional(), phone: z.string().regex(/^1[3-9]\d{9}$/).optional(), isPregnant: z.boolean().default(false), isInMedicalPeriod: z.boolean().default(false), @@ -29,6 +30,7 @@ export const updateEmployeeSchema = z.object({ hireDate: z.string().datetime().optional(), monthlySalary: z.string().min(1).optional(), gender: z.enum(['男', '女']).optional(), + femaleWorkerType: z.enum(['CADRE', 'WORKER']).optional(), phone: z.string().regex(/^1[3-9]\d{9}$/).optional(), bankName: z.string().max(50).optional(), bankAccount: z.string().max(30).optional(), diff --git a/backend/src/services/ai.service.ts b/backend/src/services/ai.service.ts index 898a954..ba00d39 100644 --- a/backend/src/services/ai.service.ts +++ b/backend/src/services/ai.service.ts @@ -234,121 +234,3 @@ ${orgContext}` } } -export interface RetirementPolicyResult { - content: string - maleRetireAge: number - femaleRetireAge: number - femaleWorkerAge: number -} - -export async function fetchRetirementPolicy(): Promise { - const today = new Date().toISOString().slice(0, 10) - const prompt = `当前日期是${today}。请提供中国最新的法定退休年龄政策。 - -已知背景信息(请以此为准): -- 2024年9月13日,全国人大常委会通过《关于实施渐进式延迟法定退休年龄的决定》,2025年1月1日起正式实施。 -- 渐进式延迟退休改革方案(15年过渡期,2025年1月1日至2039年12月31日): - - 男性职工:从60岁逐步延迟到63岁,每4个月延迟1个月 - - 女性干部/管理岗:从55岁逐步延迟到58岁,每4个月延迟1个月 - - 女性工人/操作岗:从50岁逐步延迟到55岁,每2个月延迟1个月 -- 改革前基准:男60岁、女干部55岁、女工人50岁(1978年国发〔1978〕104号) - -请根据当前日期${today},计算当前渐进式延迟退休已实施的进度,给出当前实际适用的法定退休年龄(精确到月)。 - -要求: -1. 说明当前政策状态和已实施的延迟进度 -2. 给出截至当前日期,各类人员实际适用的法定退休年龄 -3. 在末尾用以下JSON格式输出结构化数据(用标记包裹): - -{ - "maleRetireAge": <计算后的男性退休年龄,如60.33表示60岁4个月>, - "femaleRetireAge": <计算后的女性干部退休年龄>, - "femaleWorkerAge": <计算后的女性工人退休年龄> -} - -其中: -- maleRetireAge: 当前男性法定退休年龄(含渐进延迟,必须填入计算后的值,不要填基准60) -- femaleRetireAge: 当前女性干部/管理岗法定退休年龄(必须填入计算后的值,不要填基准55) -- femaleWorkerAge: 当前女性工人/操作岗法定退休年龄(必须填入计算后的值,不要填基准50) -- 年龄用小数表示,整数部分为岁,小数部分为月/12,如60岁4个月=60.33,50岁8个月=50.67 - -请确保JSON中的数值与你在正文中计算出的退休年龄完全一致,不要使用改革前的基准值。` - - const response = await client.chat.completions.create({ - model: 'qwen-plus', - messages: [ - { role: 'system', content: '你是劳动政策专家,精通中国退休政策法规。请提供准确的政策信息。' }, - { role: 'user', content: prompt }, - ], - temperature: 0.3, - max_tokens: 2000, - }) - - const text = response.choices[0]?.message?.content || '' - - let maleRetireAge = 60 - let femaleRetireAge = 55 - let femaleWorkerAge = 50 - - const jsonMatch = text.match(/([\s\S]*?)<\/JSON>/) - if (jsonMatch) { - try { - const parsed = JSON.parse(jsonMatch[1].trim()) - if (parsed.maleRetireAge) maleRetireAge = Number(parsed.maleRetireAge) - if (parsed.femaleRetireAge) femaleRetireAge = Number(parsed.femaleRetireAge) - if (parsed.femaleWorkerAge) femaleWorkerAge = Number(parsed.femaleWorkerAge) - } catch {} - } - - return { content: text, maleRetireAge, femaleRetireAge, femaleWorkerAge } -} - -export async function* fetchRetirementPolicyStream(): AsyncGenerator { - const today = new Date().toISOString().slice(0, 10) - const prompt = `当前日期是${today}。请提供中国最新的法定退休年龄政策。 - -已知背景信息(请以此为准): -- 2024年9月13日,全国人大常委会通过《关于实施渐进式延迟法定退休年龄的决定》,2025年1月1日起正式实施。 -- 渐进式延迟退休改革方案(15年过渡期,2025年1月1日至2039年12月31日): - - 男性职工:从60岁逐步延迟到63岁,每4个月延迟1个月 - - 女性干部/管理岗:从55岁逐步延迟到58岁,每4个月延迟1个月 - - 女性工人/操作岗:从50岁逐步延迟到55岁,每2个月延迟1个月 -- 改革前基准:男60岁、女干部55岁、女工人50岁(1978年国发〔1978〕104号) - -请根据当前日期${today},计算当前渐进式延迟退休已实施的进度,给出当前实际适用的法定退休年龄(精确到月)。 - -要求: -1. 说明当前政策状态和已实施的延迟进度 -2. 给出截至当前日期,各类人员实际适用的法定退休年龄 -3. 在末尾用以下JSON格式输出结构化数据(用标记包裹): - -{ - "maleRetireAge": <计算后的男性退休年龄,如60.33表示60岁4个月>, - "femaleRetireAge": <计算后的女性干部退休年龄>, - "femaleWorkerAge": <计算后的女性工人退休年龄> -} - -其中: -- maleRetireAge: 当前男性法定退休年龄(含渐进延迟,必须填入计算后的值,不要填基准60) -- femaleRetireAge: 当前女性干部/管理岗法定退休年龄(必须填入计算后的值,不要填基准55) -- femaleWorkerAge: 当前女性工人/操作岗法定退休年龄(必须填入计算后的值,不要填基准50) -- 年龄用小数表示,整数部分为岁,小数部分为月/12,如60岁4个月=60.33,50岁8个月=50.67 - -请确保JSON中的数值与你在正文中计算出的退休年龄完全一致,不要使用改革前的基准值。` - - const stream = await client.chat.completions.create({ - model: 'qwen-plus', - messages: [ - { role: 'system', content: '你是劳动政策专家,精通中国退休政策法规。请提供准确的政策信息。' }, - { role: 'user', content: prompt }, - ], - temperature: 0.3, - max_tokens: 2000, - stream: true, - }) - - for await (const chunk of stream) { - const delta = chunk.choices[0]?.delta?.content - if (delta) yield delta - } -} diff --git a/backend/src/services/contract.service.ts b/backend/src/services/contract.service.ts index 9ba8ef6..668dbb6 100644 --- a/backend/src/services/contract.service.ts +++ b/backend/src/services/contract.service.ts @@ -199,6 +199,7 @@ export async function createEmployee(orgId: string, userId: string, data: any) { hireDate, monthlySalary: encrypt(data.monthlySalary), gender: data.gender, + femaleWorkerType: data.femaleWorkerType, phone: data.phone, idCardNumber: data.idCardNumber ? encrypt(data.idCardNumber) : null, idCardHash: data.idCardNumber ? sha256(data.idCardNumber) : null, @@ -505,6 +506,7 @@ export async function updateEmployee(orgId: string, id: string, data: any) { } } if (data.gender !== undefined) updateData.gender = data.gender + if (data.femaleWorkerType !== undefined) updateData.femaleWorkerType = data.femaleWorkerType if (data.phone !== undefined) updateData.phone = data.phone if (data.bankName !== undefined) updateData.bankName = data.bankName if (data.bankAccount !== undefined) updateData.bankAccount = encrypt(data.bankAccount) diff --git a/backend/src/services/retirement.service.ts b/backend/src/services/retirement.service.ts index 1aa3817..19aa17a 100644 --- a/backend/src/services/retirement.service.ts +++ b/backend/src/services/retirement.service.ts @@ -1,6 +1,5 @@ import crypto from 'crypto' import prisma from '../lib/prisma' -import { fetchRetirementPolicy } from './ai.service' // 从身份证号提取出生日期 export function extractBirthDateFromIdCard(idCard: string): Date | null { @@ -38,21 +37,132 @@ export function extractGenderFromIdCard(idCard: string): string | null { return null } -// 计算距退休天数(支持小数退休年龄,如 60.33 = 60岁4个月) -export function calcRetirementDaysLeft(birthDate: Date, gender: string, maleRetireAge: number, femaleRetireAge: number, femaleWorkerAge: number): number | null { - const retireAge = gender === '男' ? maleRetireAge : femaleRetireAge - if (!retireAge) 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个月 - // 分离整数年和月数 - const years = Math.floor(retireAge) - const months = Math.round((retireAge - years) * 12) +// 计算从改革起始日到指定日期经过的完整月数 +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) +} - // 计算退休日期:出生日期 + 整数年 + 月数 - const retireDate = new Date(birthDate) - retireDate.setFullYear(retireDate.getFullYear() + years) - retireDate.setMonth(retireDate.getMonth() + 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)) @@ -73,11 +183,11 @@ export async function checkAndUpdateRetirementPolicy(orgId: string): Promise { +// 批量更新员工距退休天数(基于个人出生日期 individually 计算) +export async function updateEmployeesRetirementDays(orgId: string): Promise { // 查询有正式劳动合同的员工 const employees = await prisma.employee.findMany({ where: { orgId, contracts: { some: { contractType: { in: ['FIXED', 'UNFIXED'] } } }, }, - select: { id: true, birthDate: true, gender: true, idCardNumber: true }, + 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, maleRetireAge, femaleRetireAge, femaleWorkerAge) + const daysLeft = calcRetirementDaysLeft(emp.birthDate, gender, emp.femaleWorkerType) if (daysLeft !== null) { await prisma.employee.update({ where: { id: emp.id }, diff --git a/frontend/src/pages/Roster.tsx b/frontend/src/pages/Roster.tsx index 8aa4d03..f2c89bd 100644 --- a/frontend/src/pages/Roster.tsx +++ b/frontend/src/pages/Roster.tsx @@ -844,6 +844,7 @@ function BasicInfo({ profile }: { profile: any }) { const [form, setForm] = useState({ department: profile.department || '', gender: profile.gender || '男', + femaleWorkerType: profile.femaleWorkerType || '', phone: profile.phone || '', hireDate: profile.hireDate?.toString().slice(0, 10) || '', monthlySalary: profile.monthlySalary || '', @@ -873,6 +874,7 @@ function BasicInfo({ profile }: { profile: any }) { const data: any = { department: form.department, gender: form.gender, + femaleWorkerType: form.femaleWorkerType || undefined, phone: form.phone || undefined, hireDate: new Date(form.hireDate).toISOString(), monthlySalary: String(form.monthlySalary), @@ -895,6 +897,9 @@ function BasicInfo({ profile }: { profile: any }) { { label: '姓名', value: profile.name }, { label: '部门', value: profile.department }, { label: '性别', value: profile.gender || '未填写' }, + ...(profile.gender === '女' + ? [{ label: '女性岗位', value: profile.femaleWorkerType === 'CADRE' ? '干部/管理岗' : profile.femaleWorkerType === 'WORKER' ? '工人/操作岗' : '未填写' }] + : []), { label: '身份证号', value: profile.idCardNumber || '未填写' }, { label: '手机号', value: profile.phone || '未填写' }, { label: '入职日期', value: profile.hireDate?.toString().slice(0, 10) }, @@ -906,7 +911,26 @@ function BasicInfo({ profile }: { profile: any }) { { label: '银行账号', value: profile.bankAccount || '未填写' }, { label: '状态', value: profile.status === 'ACTIVE' ? '在职' : '离职' }, ...(profile.retirementDaysLeft != null - ? [{ label: '距退休', value: profile.retirementDaysLeft > 0 ? `${profile.retirementDaysLeft}天` : '已到退休年龄' }] + ? (() => { + if (profile.retirementDaysLeft <= 0) return [{ label: '距退休', value: '已到退休年龄' }] + if (profile.birthDate) { + const bd = new Date(profile.birthDate) + const gender = profile.gender || '男' + const fwt = profile.femaleWorkerType || null + const baseAge = gender === '男' ? 60 : (fwt === 'WORKER' ? 50 : 55) + const delayInterval = gender === '男' ? 4 : (fwt === 'WORKER' ? 2 : 4) + const maxDelay = gender === '男' ? 36 : (fwt === 'WORKER' ? 60 : 36) + const baseRetireDate = new Date(bd) + baseRetireDate.setFullYear(baseRetireDate.getFullYear() + baseAge) + const reformStart = new Date(2025, 0, 1) + const monthsSince = Math.max(0, (baseRetireDate.getFullYear() - reformStart.getFullYear()) * 12 + (baseRetireDate.getMonth() - reformStart.getMonth())) + const delayMonths = Math.min(maxDelay, Math.floor(monthsSince / delayInterval)) + const retireDate = new Date(baseRetireDate) + retireDate.setMonth(retireDate.getMonth() + delayMonths) + return [{ label: '退休日期', value: `${retireDate.getFullYear()}年${retireDate.getMonth() + 1}月${retireDate.getDate()}日` }] + } + return [{ label: '距退休', value: `${profile.retirementDaysLeft}天` }] + })() : []), ...(profile.status !== 'ACTIVE' && profile.terminations && profile.terminations.length > 0 ? [{ label: '离职日期', value: profile.terminations @@ -951,6 +975,9 @@ function BasicInfo({ profile }: { profile: any }) {
setForm({ ...form, department: e.target.value })} />
+ {form.gender === '女' && ( +
+ )}
setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} />
setForm({ ...form, hireDate: e.target.value })} />
setForm({ ...form, monthlySalary: Number(e.target.value) })} />
@@ -1795,7 +1822,7 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: { })() const [form, setForm] = useState({ name: '', department: '', hireDate: todayStr, monthlySalary: '', - idCardNumber: '', gender: '男' as '男' | '女', phone: '', + idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '', city: '北京', contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED', signDate: '', startDate: todayStr, endDate: defaultEndDate, @@ -1895,6 +1922,7 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: { name: form.name, department: form.department, hireDate: new Date(form.hireDate).toISOString(), monthlySalary: form.monthlySalary, gender: form.gender, + femaleWorkerType: form.gender === '女' && form.femaleWorkerType ? form.femaleWorkerType : undefined, idCardNumber: form.idCardNumber || undefined, phone: form.phone || undefined, socialInsBase: form.socialInsBase ? parseFloat(form.socialInsBase) : undefined, @@ -1936,6 +1964,9 @@ function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
setForm({ ...form, department: e.target.value })} placeholder="如:技术部" />
handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} />
{form.idCardNumber.length >= 17 ? form.gender : '自动识别'}
+ {form.gender === '女' && ( +
+ )}
handleHireDateChange(e.target.value)} />
diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 7e945b0..3bfbfe6 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { toast } from 'sonner' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle, RefreshCw } from 'lucide-react' +import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle } from 'lucide-react' import api from '../lib/api' import { useAuthStore } from '../store/authStore' import Card from '../components/ui/Card' @@ -148,8 +148,6 @@ function OrgSettings({ orgData, onSave, saving }: { orgData: any; onSave: (data: function RetirementSection({ enabled, onToggle }: { enabled: boolean; onToggle: (v: boolean) => void }) { const queryClient = useQueryClient() const [confirming, setConfirming] = useState(false) - const [refreshing, setRefreshing] = useState(false) - const [streamContent, setStreamContent] = useState('') const { data: policyData, isLoading } = useQuery({ queryKey: ['retirement-policy'], @@ -170,55 +168,33 @@ function RetirementSection({ enabled, onToggle }: { enabled: boolean; onToggle: onError: () => toast.error('确认失败'), }) - const handleRefresh = async () => { - setRefreshing(true) - setStreamContent('') - try { - const token = useAuthStore.getState().accessToken - const baseUrl = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1' - const res = await fetch(`${baseUrl}/settings/retirement-policy/refresh`, { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - }) - const reader = res.body?.getReader() - const decoder = new TextDecoder() - let buf = '' - while (reader) { - const { done, value } = await reader.read() - if (done) break - buf += decoder.decode(value, { stream: true }) - const lines = buf.split('\n') - buf = lines.pop() || '' - for (const line of lines) { - if (line.startsWith('data: ')) { - try { - const msg = JSON.parse(line.slice(6)) - if (msg.type === 'chunk') { - setStreamContent(prev => prev + msg.content) - } else if (msg.type === 'done') { - toast.success(msg.message || '已获取最新政策') - setRefreshing(false) - setStreamContent('') - queryClient.invalidateQueries({ queryKey: ['retirement-policy'] }) - } else if (msg.type === 'error') { - toast.error(msg.message || '获取失败') - setRefreshing(false) - setStreamContent('') - } - } catch {} - } - } - } - } catch { - toast.error('获取失败') - setRefreshing(false) - setStreamContent('') - } - } - const confirmed = policyData?.confirmed const pending = policyData?.pending + const renderPolicyRules = () => ( +
+
改革规则(基准退休年龄 → 目标退休年龄)
+
+
+
男性职工
+
60岁 → 63岁
+
每4个月延1个月
+
+
+
女性干部
+
55岁 → 58岁
+
每4个月延1个月
+
+
+
女性工人
+
50岁 → 55岁
+
每2个月延1个月
+
+
+

个人退休年龄根据出生年月逐人计算,达到基准退休年龄的时间点不同,延迟月数也不同。

+
+ ) + return (
@@ -232,9 +208,6 @@ function RetirementSection({ enabled, onToggle }: { enabled: boolean; onToggle: {confirming ? '确认中...' : '确认生效'} )} -
)}
@@ -262,13 +235,7 @@ function RetirementSection({ enabled, onToggle }: { enabled: boolean; onToggle: 确认于 {new Date(confirmed.confirmedAt).toLocaleDateString('zh-CN')}
-
-
男性退休年龄:{confirmed.maleRetireAge}岁 女性干部:{confirmed.femaleRetireAge}岁 女性工人:{confirmed.femaleWorkerAge}岁
-
- 查看完整政策内容 -
{confirmed.content}
-
-
+ {renderPolicyRules()} )} @@ -279,35 +246,18 @@ function RetirementSection({ enabled, onToggle }: { enabled: boolean; onToggle: 检测到新政策版本(v{pending.version}),请确认后生效 -
-
男性退休年龄:{pending.maleRetireAge}岁 女性干部:{pending.femaleRetireAge}岁 女性工人:{pending.femaleWorkerAge}岁
-
- 查看完整政策内容 -
{pending.content}
-
-
+ {renderPolicyRules()} )} {/* 无政策 */} {!confirmed && !pending && !isLoading && (
-

暂无退休政策数据,点击上方"重新获取"按钮获取最新政策

+

暂无退休政策数据,系统将自动获取最新政策

)} {isLoading &&

加载中...

} - - {/* 流式输出 */} - {streamContent && ( -
-
- - AI 正在获取最新政策... -
-
{streamContent}
-
- )} )}