Files
TurboHR/20260816-优化.md
T
selfrelease 8c7fb99219 ux: 待签合同列表新增「待签内容」列,展示场景标签+签署方式
每行员工直接显示待签文件的类型(劳动合同/离职协议/规章制度等)
和签署方式(电子签/线下手签),无需展开即可看出是什么待签。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-16 12:06:15 +08:00

21 KiB
Raw Blame History

20260816 优化清单

来源:用户测试反馈(编号 4-13,共 10 项) 结合 TurboHR 当期代码梳理,按模块归类,标注根因 / 修复方向 / 优先级 / 涉及文件。 优先级:P0 紧急(影响业务正确性/合规)|P1 高(流程阻塞或合规风险)|P2 中(体验/增强)|P3 规划(新模块)


一、花名册 · 添加雇员(问题 4-8)

问题 4:合同状态未签署时仍显示"正常"

  • 现状backend/src/services/contract.service.ts:74 注释明确"有合同记录(FIXED/UNFIXED/LABOR/INTERNSHIP),即使 signDate 为 null 也按正常合同处理",getContractStatus 在有合同记录时直接返回 active/正常,未区分"已签署"与"待签署"。
  • 修复方向
    1. getContractStatus 增加"待签署"判定:有合同记录但 signDate 为空,或关联的电子签署记录状态为 PENDING/未 COMPLETED 时,返回 status: 'pending_sign'statusText: '待签署'riskLevel: 'medium'
    2. 仅当 signDate 已填且(无电子签署或电子签署已 COMPLETED)时才返回 active/正常
    3. 前端花名册列表/详情同步展示"待签署"标签。
  • 优先级P1 高
  • 涉及文件backend/src/services/contract.service.tsfrontend/src/pages/Roster.tsxfrontend/src/pages/roster/ContractInfo.tsx

问题 5:社保状态未办理时仍显示"正常"

  • 现状:花名册添加雇员表单(roster/modals.tsx:719)有 socialInsBase/socialInsStartMonth/housingFundBase/housingFundStartMonth,但无"社保状态"字段;花名册列表/详情也未展示社保办理状态,默认视为"正常"。
  • 修复方向
    1. 新增社保状态字段(派生而非存储):根据 socialInsStartMonth 是否填写、是否已做"办理完成"操作判定。
    2. 状态枚举:待办(未填写基数/起始月或未办理完成)、正常(已办理且在缴)、停缴(已设截止月)。
    3. 后端 employee.service.ts 在花名册列表返回 socialInsStatus 派生字段。
    4. 前端花名册列表/详情展示社保状态标签。
    5. 提供"办理完成"操作入口(可在社保公积金模块或花名册操作栏)。
  • 优先级P1 高
  • 涉及文件backend/src/services/employee.service.tsbackend/src/services/contract.service.ts(或新增 social-insurance.service.ts 派生方法)、frontend/src/pages/Roster.tsxfrontend/src/pages/roster/PayslipSocialInfo.tsx

问题 6:证件号码未验证有效性

  • 现状roster/modals.tsx:886 canSubmit 仅校验 idCardNumber.length >= 18,未做身份证校验位(第 18 位)、出生日期合法性、月份/日期范围等有效性验证;handleIdCardChange(755 行)只做性别推导和查重,未做格式校验。
  • 修复方向
    1. 新增身份证有效性校验:校验位算法(前 17 位加权求和取模映射第 18 位)、出生日期合法、月份 01-12、日期合法。
    2. 校验失败时 ageWarning 返回 BLOCK,阻断保存。
    3. 后端 employee.service.ts 创建/更新员工时同步做校验(双保险)。
    4. 支持非身份证类型(护照/港澳台通行证)时跳过 18 位校验(已部分实现,需确认)。
  • 优先级P1 高
  • 涉及文件frontend/src/pages/roster/modals.tsxhandleIdCardChangecanSubmit)、backend/src/services/employee.service.tsbackend/src/routes/employee.routes.ts

问题 7:超龄人员仍可选择劳动合同

  • 现状roster/modals.tsx:784 超龄仅 WARN 提示"已达法定退休年龄,建议确认是否按退休处理",未阻断选择 FIXED/UNFIXED 劳动合同;超龄人员依法应签劳务协议/实习协议,不能签劳动合同。
  • 修复方向
    1. 年龄 ≥ 法定退休年龄(男 60、女干部 55、女工人 50)时,合同类型下拉移除 FIXED/UNFIXED,仅保留 LABOR/INTERNSHIP/UNSIGNED,并提示"超龄人员不可签订劳动合同,请选择劳务协议"。
    2. 强制选 LABOR 时联动问题 8 的社保基数置 0 逻辑。
    3. 后端 employee.service.ts/work-process.service.ts 增加校验:超龄 + 劳动合同类型组合拒绝并返回 400。
    4. femaleWorkerType 字段用于判定女职工退休年龄(干部 55/工人 50),需在年龄计算时引用。
  • 优先级P1 高
  • 涉及文件frontend/src/pages/roster/modals.tsx(合同类型下拉、ageWarning 逻辑)、backend/src/services/employee.service.tsbackend/src/services/work-process.service.ts

问题 8:签署劳务协议员工社保基数未置 0 且仍可缴纳社保

  • 现状roster/modals.tsx:983 社保缴费基数为独立输入框,选择 LABOR 合同类型时未联动置 0 或禁用;劳务协议人员依法不缴纳社保。
  • 修复方向
    1. 合同类型选择 LABOR/INTERNSHIP 时,社保缴费基数、公积金缴费基数自动置 0 且字段禁用,社保开始年月清空。
    2. UI 提示"劳务协议/实习协议人员不缴纳社保公积金"。
    3. 后端 employee.service.ts 保存时校验:contractType === LABORsocialInsBase > 0 时拒绝或强制置 0。
    4. 薪税批次生成时排除劳务协议人员,或其社保公积金项强制为 0。
  • 优先级P1 高
  • 涉及文件frontend/src/pages/roster/modals.tsx(社保公积金区联动)、backend/src/services/employee.service.tsbackend/src/services/payroll.service.ts

二、工作台(问题 9-10

问题 9:待办事项无法点击跳转

  • 现状Dashboard.tsx:1148 已有 <Link to={todo.actionUrl}>,但部分待办的 actionUrl 为空、指向不存在的路由,或后端 dashboard.service.ts 生成待办时未填充 actionUrl,导致点击无反应或跳转 404。
  • 修复方向
    1. 排查后端 dashboard.service.ts 各类待办(CONTRACT/TERMINATION/ONBOARDING/RETIREMENT/MONTHLY/SALARY)的 actionUrl 生成逻辑,确保每条待办都有有效跳转目标。
    2. actionUrl 为空时前端降级为"查看详情"按钮,弹窗展示待办内容,而非死链。
    3. 校验 actionUrl 指向的路由真实存在(如 /roster?employeeId=xxx/termination?draftId=xxx)。
    4. 跳转后自动定位到对应员工/草稿/合同。
  • 优先级P1 高
  • 涉及文件backend/src/services/dashboard.service.tsfrontend/src/pages/Dashboard.tsxTodoIcon/待办列表渲染)

问题 10:风险提醒内容不合理(含反向引导 bug)

  • 现状Dashboard.tsx:202 风险提醒按 todo.type 过滤(CONTRACT/TERMINATION/ONBOARDING/RETIREMENT),具体提醒文案、阈值、优先级、跳转目标生成逻辑在后端 risk.service.ts。存在两类问题:

    问题 A · 反向引导(典型 bugrisk.service.ts:309-337 三期/医疗期/工伤员工的风险提醒,文案是"解聘受限/不得解除劳动合同",但 actionUrl 却指向 /termination?employee=xxx(解聘向导页面)。等于提示"该员工不能解聘,点这里去解聘她"——完全反向引导,可能诱导 HR 误操作违法解除。

    // risk.service.ts:309-317(错误示例)
    if (emp.isPregnant) {
      risks.push({
        type: 'TERMINATION',
        title: `${emp.name}处于孕期/哺乳期,解聘受限`,
        description: '三期女职工不得依非过错理由解除劳动合同...',
        actionUrl: `/termination?employee=${encodeURIComponent(emp.name)}`,  // ← 反向引导
      })
    }
    // isInMedicalPeriod319-328)、isWorkInjured329-338)同样指向 /termination
    

    问题 B · 提醒规则/阈值/去重不合理:部分提醒内容重复、阈值不合理或与实际业务无关(如合同到期 30 天提前量偏短,同一合同到期可能生成多条提醒)。

  • 修复方向

    1. 修正反向引导(问题 AP0 紧急)
      • 三期/医疗期/工伤员工的"解聘受限"提醒,actionUrl 改为指向员工详情或特殊状态页(/roster?employee=xxx/special-status?type=PREGNANCY),让 HR 确认员工状态,而非跳到解聘页面。
      • 这类提醒的本质是"信息提示 + 合规警示",不是"可操作待办"。应在解聘向导 step 2 合规检查时硬阻断(Termination 已实现法定禁止情形阻断),工作台只做信息展示。
      • 可考虑将这类提醒的 typeTERMINATION 改为 COMPLIANCE(合规提示),与可操作的解聘待办区分。
    2. 梳理风险提醒规则清单(问题 B:合同到期(30/60/90 天分级)、未签合同(>30 天/>1 年)、试用期将满、退休预警、社保断缴、离职未办结等。
    3. 每条提醒明确:触发条件、提醒文案模板、优先级、跳转目标、是否可忽略、是"可操作待办"还是"信息提示"。
    4. 去除重复提醒(如同一合同到期生成多条),合并同类项。
    5. 调整阈值至合理区间(如合同到期提醒从 30 天提前到 60 天,给 HR 反应时间)。
    6. 前端按优先级排序展示,高风险置顶;信息提示类提醒不提供"去处理"按钮,仅提供"查看详情"。
  • 优先级:P0 紧急(问题 A 反向引导)/ P2 中(问题 B 规则优化)

  • 涉及文件backend/src/services/risk.service.tsdetectTerminationRisks 309-342 行、detectSpecialStatusRisks)、frontend/src/pages/Dashboard.tsx(风险提醒 Tab 渲染、按 type 区分操作按钮)


三、薪税管理 · 发放批次(问题 11、13)

问题 11:获取工资时自动识别试用期并填充试用期工资

  • 现状:不需要新增"获取试用期工资"按钮。已有的"获取工资"逻辑(创建批次、添加人员时自动填充 baseSalary)应自动识别员工是否处于试用期,是则填 probationSalary,否则填 monthlySalary

    根因:当前 3 处获取工资逻辑用了错误的试用期判定,判定的是"入职不到 1 年"而非"当前在试用期内"

    // payroll2.routes.ts:355-359(创建批次 copy_last 模式)
    // payroll2.routes.ts:610-614(添加人员到批次)
    // payroll.routes.ts:380-388(旧版批量生成工资条)
    if (emp.contracts?.[0]?.probationSalary
        && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) {
      baseSalary = emp.contracts[0].probationSalary  // ← 只要入职不到1年就用试用期工资,转正后仍用
    } else if (emp.monthlySalary) { ... }
    

    问题:员工转正后(如试用期 3 个月,入职 6 个月),仍会被判定为试用期,baseSalary 错误填为 probationSalary 而非转正工资。

    正确逻辑payroll.service.ts:326-331payroll.service.ts:600-603 已实现):

    const probationEnd = new Date(contract.startDate)
    probationEnd.setMonth(probationEnd.getMonth() + contract.probationMonths)
    if (probationEnd > new Date()) { ... }  // 试用期未结束
    
  • 修复方向

    1. 抽取统一的试用期判定工具函数 isInProbation(contract, referenceDate?)contract.startDate + contract.probationMonths > referenceDate(默认当前日期,批次场景按批次月份的月末判定)。
    2. 修正 3 处错误逻辑,统一调用 isInProbation
      • payroll2.routes.ts:355-359(创建批次 copy_last 模式)
      • payroll2.routes.ts:610-614(添加人员到批次)
      • payroll.routes.ts:380-388(旧版批量生成工资条,如仍在用)
    3. 判定为试用期 → baseSalary = contract.probationSalary;已转正 → baseSalary = employee.monthlySalary
    4. 批次场景按批次月份判定(如批次月份是 2026-08,则判定 2026-08-31 时员工是否仍在试用期),避免跨月转正时填错。
    5. probationSalary 为 0 或未填时,即使试用期也回退用 monthlySalary(兼容旧数据)。
  • 优先级P1 高(影响工资发放正确性)

  • 涉及文件backend/src/routes/payroll2.routes.ts355-359、610-614)、backend/src/routes/payroll.routes.ts380-388)、backend/src/services/payroll.service.ts(抽取 isInProbation 工具函数,或放 contract.service.ts

问题 13:编辑薪资新增"获取提成奖金"功能

  • 现状money/BatchTab.tsx 已有 BONUS 批次类型(isBonus 分支,617 行),但普通薪资批次编辑薪资时无"获取提成奖金"按钮,无法从提成奖金数据按月拉取填充 bonus 字段。
  • 修复方向
    1. 编辑薪资步骤新增"获取提成奖金"按钮,调用后端接口按批次月份从提成奖金表拉取每位员工的提成奖金。
    2. 拉取的奖金填入 entry.bonus 字段,支持正负值(奖金/扣款)。
    3. 后端新增接口 POST /payroll2/batches/:id/fetch-bonus,按批次月份查询提成奖金表并返回。
    4. 前端展示获取结果摘要(X 人提成奖金已填充,合计 ¥Y),支持覆盖确认。
    5. 依赖问题 12 的提成奖金数据表先建立。
  • 优先级P2 中(依赖问题 12
  • 涉及文件frontend/src/pages/money/BatchTab.tsx(编辑薪资步骤按钮区)、backend/src/routes/payroll.routes.tsbackend/src/services/payroll.service.tsbackend/src/services/commission-bonus.service.ts(新增)

四、提成奖金 · 新模块(问题 12

问题 12:团队页面下新增提成奖金页面

  • 现状SidebarNav.tsx:44 "团队"分组下仅有花名册,无提成奖金入口;money/BatchTab.tsxBONUS 批次是年终奖发放,非按月提成奖金管理。需新建独立提成奖金模块。
  • 修复方向
    1. 数据模型:新增 CommissionBonus 表(orgIdemployeeIdmonth YYYY-MM、amount 支持正负值、remarkcreatedAtcreatedBy),唯一索引 (orgId, employeeId, month)
    2. 后端:新增 commission-bonus.service.ts + commission-bonus.routes.ts
      • GET /commission-bonus?month=YYYY-MM 按月列表
      • POST /commission-bonus/batch 按月批量导入(JSON 数组或 Excel)
      • PUT /commission-bonus/:id 编辑单条
      • DELETE /commission-bonus/:id 删除单条
      • GET /commission-bonus/template 下载导入模板
    3. 前端:新增 frontend/src/pages/CommissionBonus.tsx 页面:
      • 月份选择器(默认当月)
      • 列表展示该月所有员工提成奖金(员工/部门/金额/备注)
      • 支持单条编辑、删除
      • "批量导入"按钮(上传 Excel,字段:员工姓名/证件号码/月份/金额/备注)
      • 金额支持正负值(正为奖金,负为扣款)
      • 合计展示(总奖金/总扣款/净额)
    4. 菜单SidebarNav.tsx "团队"分组新增"提成奖金"入口,路由 /commission-bonus
    5. 权限:新增权限码 PERM_COMMISSION_BONUS_VIEW/PERM_COMMISSION_BONUS_EDIT/PERM_COMMISSION_BONUS_IMPORT
  • 优先级P3 规划(新模块)
  • 涉及文件backend/prisma/schema.prisma(新增 CommissionBonus 模型 + migration)、backend/src/services/commission-bonus.service.ts(新增)、backend/src/routes/commission-bonus.routes.ts(新增)、frontend/src/pages/CommissionBonus.tsx(新增)、frontend/src/components/layout/SidebarNav.tsxfrontend/src/App.tsx(路由注册)、frontend/src/lib/api-services.tscommissionBonusApi

优先级汇总

优先级 问题 说明
P0 紧急 10A 风险提醒反向引导(三期/医疗期/工伤提醒跳转到解聘页面)
P1 高 4、5、6、7、8、9、11 花名册添加雇员合规校验 + 工作台待办跳转 + 试用期工资判定修正
P2 中 10B、13 风险提醒规则优化 + 薪资批次获取提成奖金(13 依赖 12)
P3 规划 12 提成奖金新模块(需单独排期 + DB migration

建议执行顺序

  1. 第一批(P0 + P1,立即)
    • 问题 10A(风险提醒反向引导,改 actionUrl)← 最先,改动小风险高
    • 问题 11(试用期工资判定修正)← 纯后端逻辑修正,抽取 isInProbation 工具函数
    • 问题 6(证件号码有效性校验)→ 问题 7(超龄人员合同类型限制)→ 问题 8(劳务协议社保联动)→ 问题 4(合同状态待签署)→ 问题 5(社保状态待办)→ 问题 9(待办跳转)
    • 问题 6/7/8 均在 roster/modals.tsx 添加雇员表单,可一并修改;问题 4/5 在后端 contract.service.ts/employee.service.ts 派生字段,前后端协同。
  2. 第二批(P2,本迭代)
    • 问题 10B(风险提醒规则梳理)→ 问题 13(获取提成奖金,依赖问题 12)
  3. 第三批(P3,规划)
    • 问题 12(提成奖金新模块,含 DB migration)→ 问题 13(获取提成奖金,依赖 12 完成)

备注

  • 本清单基于当期代码静态分析,部分"根因"为推断,实际修复前需运行复现确认。
  • 问题 12 涉及数据库 schema 变更(新增 CommissionBonus 表),需走 migration + 回滚方案。
  • 问题 4/5 的"待签署""待办"状态为派生字段,不新增存储字段,避免数据冗余;如需记录"办理完成"操作时间,可新增 socialInsConfirmedAt 等时间戳字段。
  • 问题 7 的超龄判定需引用 femaleWorkerType(干部 55/工人 50),需确认该字段在添加雇员表单已正确填写。
  • 问题 13 依赖问题 12 的提成奖金数据表,不可先行实现。

五、已完成优化(20260816 补充)

以下为本次会话中已完成并部署的优化项。

已完成 1:员工端密码登录完整支持

  • 默认密码:创建员工时自动设置,密码 = 手机号后6位(无手机号则 123456
  • 管理员重置密码:花名册详情 → "重置密码"按钮,重置为手机号后6位
  • 员工修改密码:员工端导航栏 → "修改密码",需验证旧密码,新密码至少6位
  • Excel 批量导入:导入时自动设置默认密码(手机号后6位),与新增员工一致
  • 现有员工补初始化:已通过脚本批量补设 512 名无密码员工(478 人手机号后6位,34 人无手机号用 123456)
  • 涉及文件
    • backend/src/services/contract.service.tscreateEmployee 添加 passwordHash
    • backend/src/routes/import.routes.tsExcel 导入添加 passwordHash
    • backend/src/routes/employee.routes.ts(新增 POST /employees/:id/reset-password
    • backend/src/routes/portal.routes.ts(新增 POST /portal/change-password
    • frontend/src/pages/roster/BasicInfo.tsx(重置密码按钮 + 默认密码提示)
    • frontend/src/pages/portal/PortalNav.tsx(修改密码弹窗)
    • frontend/src/lib/api-services.tsresetPassword / changePassword 接口)

已完成 2:员工端 portalAxios response interceptor 修复

  • 问题portalAxios 缺少 response interceptor,导致 unwrap 取到的是 { success, data: {...} } 而非 { token, employee },密码登录/验证码登录/扫码自动登录全部失败
  • 修复:给 portalAxios 添加与管理端 api 实例一致的 response interceptor (response) => response.data
  • 涉及文件frontend/src/lib/api-services.ts

已完成 3:同企业内员工身份证和手机号唯一性完整校验

  • createEmployee:新增手机号查重硬校验(原有身份证查重)
  • updateEmployee:新增手机号 + 身份证查重(排除自身)
  • Excel 批量导入:新增身份证 + 手机号查重(重复则跳过并记录错误日志)
  • portal 登录安全:密码登录改为 findMany 遍历校验,避免跨组织手机号重复时登录到错误员工
  • 涉及文件
    • backend/src/services/contract.service.tscreateEmployee / updateEmployee 查重)
    • backend/src/routes/import.routes.ts(导入查重)
    • backend/src/routes/portal.routes.tslogin / send-code / verify-code 跨组织安全)

已完成 4:电子签署改为「待签合同」,按员工聚合 + 催办功能

  • 菜单改名:「电子签署」→「待签合同」
  • 页面重构为两个 Tab
    • 待签合同:按员工聚合,每行一个员工,显示待签数,点击展开查看该员工名下所有待签文件(EsignRecord PENDING/SIGNING + LaborContract signDate 为空)
    • 签署记录:原有全部签署记录列表(保留状态/场景筛选)
  • 催办功能:点击催办 → 生成 24 小时有效的一次性自动登录链接(指向员工端签署页)→ 弹窗展示二维码 + 可复制链接 → HR 发给员工扫码直接进入签署
  • 后端新增接口
    • GET /esign/pending — 待签合同按员工聚合列表
    • POST /esign/remind — 催办生成自动登录链接
  • 涉及文件
    • backend/src/routes/esign.routes.tspending + remind 接口)
    • frontend/src/components/layout/SidebarNav.tsx(菜单改名)
    • frontend/src/pages/ESign.tsxTab 切换 + PendingTab 组件 + 催办弹窗)
    • frontend/src/lib/api-services.tspending / remind 接口调用)