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

275 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.ts``frontend/src/pages/Roster.tsx``frontend/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.ts``backend/src/services/contract.service.ts`(或新增 `social-insurance.service.ts` 派生方法)、`frontend/src/pages/Roster.tsx``frontend/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.tsx``handleIdCardChange``canSubmit`)、`backend/src/services/employee.service.ts``backend/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.ts``backend/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 === LABOR``socialInsBase > 0` 时拒绝或强制置 0。
4. 薪税批次生成时排除劳务协议人员,或其社保公积金项强制为 0。
- **优先级**P1 高
- **涉及文件**`frontend/src/pages/roster/modals.tsx`(社保公积金区联动)、`backend/src/services/employee.service.ts``backend/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.ts``frontend/src/pages/Dashboard.tsx``TodoIcon`/待办列表渲染)
### 问题 10:风险提醒内容不合理(含反向引导 bug)
- **现状**`Dashboard.tsx:202` 风险提醒按 `todo.type` 过滤(CONTRACT/TERMINATION/ONBOARDING/RETIREMENT),具体提醒文案、阈值、优先级、跳转目标生成逻辑在后端 `risk.service.ts`。存在两类问题:
**问题 A · 反向引导(典型 bug)**`risk.service.ts:309-337` 三期/医疗期/工伤员工的风险提醒,文案是"解聘受限/不得解除劳动合同",但 `actionUrl` 却指向 `/termination?employee=xxx`(解聘向导页面)。等于提示"该员工不能解聘,点这里去解聘她"——完全反向引导,可能诱导 HR 误操作违法解除。
```ts
// 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. **修正反向引导(问题 A,P0 紧急)**:
- 三期/医疗期/工伤员工的"解聘受限"提醒,`actionUrl` 改为指向员工详情或特殊状态页(`/roster?employee=xxx` 或 `/special-status?type=PREGNANCY`),让 HR 确认员工状态,而非跳到解聘页面。
- 这类提醒的本质是"信息提示 + 合规警示",不是"可操作待办"。应在解聘向导 step 2 合规检查时硬阻断(Termination 已实现法定禁止情形阻断),工作台只做信息展示。
- 可考虑将这类提醒的 `type` 从 `TERMINATION` 改为 `COMPLIANCE`(合规提示),与可操作的解聘待办区分。
2. **梳理风险提醒规则清单(问题 B)**:合同到期(30/60/90 天分级)、未签合同(>30 天/>1 年)、试用期将满、退休预警、社保断缴、离职未办结等。
3. **每条提醒明确**:触发条件、提醒文案模板、优先级、跳转目标、是否可忽略、是"可操作待办"还是"信息提示"。
4. **去除重复提醒**(如同一合同到期生成多条),合并同类项。
5. **调整阈值至合理区间**(如合同到期提醒从 30 天提前到 60 天,给 HR 反应时间)。
6. **前端按优先级排序展示**,高风险置顶;信息提示类提醒不提供"去处理"按钮,仅提供"查看详情"。
- **优先级**:P0 紧急(问题 A 反向引导)/ P2 中(问题 B 规则优化)
- **涉及文件**`backend/src/services/risk.service.ts``detectTerminationRisks` 309-342 行、`detectSpecialStatusRisks`)、`frontend/src/pages/Dashboard.tsx`(风险提醒 Tab 渲染、按 type 区分操作按钮)
---
## 三、薪税管理 · 发放批次(问题 11、13)
### 问题 11:获取工资时自动识别试用期并填充试用期工资
- **现状**:不需要新增"获取试用期工资"按钮。已有的"获取工资"逻辑(创建批次、添加人员时自动填充 `baseSalary`)应自动识别员工是否处于试用期,是则填 `probationSalary`,否则填 `monthlySalary`。
**根因**:当前 3 处获取工资逻辑用了**错误的试用期判定**,判定的是"入职不到 1 年"而非"当前在试用期内"
```ts
// 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-331`、`payroll.service.ts:600-603` 已实现):
```ts
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.ts`355-359、610-614)、`backend/src/routes/payroll.routes.ts`380-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.ts`、`backend/src/services/payroll.service.ts`、`backend/src/services/commission-bonus.service.ts`(新增)
---
## 四、提成奖金 · 新模块(问题 12)
### 问题 12:团队页面下新增提成奖金页面
- **现状**`SidebarNav.tsx:44` "团队"分组下仅有花名册,无提成奖金入口;`money/BatchTab.tsx` 的 `BONUS` 批次是年终奖发放,非按月提成奖金管理。需新建独立提成奖金模块。
- **修复方向**
1. **数据模型**:新增 `CommissionBonus` 表(`orgId`、`employeeId`、`month` YYYY-MM、`amount` 支持正负值、`remark`、`createdAt`、`createdBy`),唯一索引 `(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.tsx`、`frontend/src/App.tsx`(路由注册)、`frontend/src/lib/api-services.ts``commissionBonusApi`
---
## 优先级汇总
| 优先级 | 问题 | 说明 |
|--------|------|------|
| **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.ts`createEmployee 添加 passwordHash
- `backend/src/routes/import.routes.ts`Excel 导入添加 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.ts`resetPassword / 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.ts`createEmployee / updateEmployee 查重)
- `backend/src/routes/import.routes.ts`(导入查重)
- `backend/src/routes/portal.routes.ts`login / send-code / verify-code 跨组织安全)
### ✅ 已完成 4:电子签署改为「待签合同」,按员工聚合 + 催办功能
- **菜单改名**:「电子签署」→「待签合同」
- **页面重构为两个 Tab**
- **待签合同**:按员工聚合,每行一个员工,显示待签数,点击展开查看该员工名下所有待签文件(EsignRecord PENDING/SIGNING + LaborContract signDate 为空)
- **签署记录**:原有全部签署记录列表(保留状态/场景筛选)
- **催办功能**:点击催办 → 生成 24 小时有效的一次性自动登录链接(指向员工端签署页)→ 弹窗展示二维码 + 可复制链接 → HR 发给员工扫码直接进入签署
- **后端新增接口**
- `GET /esign/pending` — 待签合同按员工聚合列表
- `POST /esign/remind` — 催办生成自动登录链接
- **涉及文件**
- `backend/src/routes/esign.routes.ts`pending + remind 接口)
- `frontend/src/components/layout/SidebarNav.tsx`(菜单改名)
- `frontend/src/pages/ESign.tsx`Tab 切换 + PendingTab 组件 + 催办弹窗)
- `frontend/src/lib/api-services.ts`pending / remind 接口调用)