fix: 用工办理必填项校验(前后端)+ 20260811优化验证文档

This commit is contained in:
selfrelease
2026-08-11 17:15:51 +08:00
parent de7f1830a7
commit b682178549
3 changed files with 439 additions and 29 deletions
+36
View File
@@ -7,6 +7,34 @@ import { createWorkProcessSchema, updateWorkProcessSchema } from '../schemas/wor
const router = Router()
// 各流程类型的必填字段映射
const REQUIRED_FIELDS: Record<string, string[]> = {
HIRE: ['name', 'department', 'hireDate', 'phone', 'idCardNumber'],
ONBOARD: ['employeeId', 'hireDate'],
CUSTOM_CONTRACT: ['employeeId', 'contractStartDate'],
INFO_SUBMIT: ['employeeId'],
CONFIRM: ['employeeId', 'confirmDate'],
CHANGE: ['contractId', 'newEndDate'],
RENEW: ['employeeId', 'newStartDate'],
SUSPEND: ['contractId', 'suspendDate'],
INCOME_CERT: ['employeeName', 'idCardNumber'],
TERMINATE: ['employeeId', 'terminateDate', 'reason'],
RESCIND: ['employeeId', 'rescindDate', 'reason'],
LEAVING_CERT: ['employeeName', 'idCardNumber', 'leaveDate'],
FLEXIBLE: ['name', 'idCardNumber', 'agreementStartDate'],
}
// 必填字段中文标签映射
const FIELD_LABELS: Record<string, string> = {
name: '员工姓名', department: '部门', hireDate: '入职日期', phone: '手机号',
idCardNumber: '身份证号', employeeId: '员工ID', contractId: '合同ID',
contractStartDate: '合同开始日期', confirmDate: '转正日期',
newEndDate: '新到期日期', newStartDate: '新合同开始日期',
suspendDate: '中止日期', employeeName: '员工姓名',
terminateDate: '终止日期', rescindDate: '解除日期', reason: '原因',
leaveDate: '离职日期', agreementStartDate: '协议开始日期',
}
// 创建办理(含草稿)
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
@@ -108,6 +136,14 @@ router.post('/:id/submit', authMiddleware, async (req: AuthRequest, res: Respons
if (process.status !== 'DRAFT') {
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可提交' } })
}
// 后端必填字段校验
const required = REQUIRED_FIELDS[process.type] || []
const fd = process.formData || {}
const missing = required.filter((key) => !fd[key] || String(fd[key]).trim() === '')
if (missing.length > 0) {
const labels = missing.map((k) => FIELD_LABELS[k] || k).join('、')
return res.status(400).json({ success: false, error: { code: 'MISSING_REQUIRED', message: `请填写必填项:${labels}` } })
}
// 执行业务联动
let execResult: any = {}
try {
+353
View File
@@ -0,0 +1,353 @@
# TurboHR 系统优化清单(20260811
> 基于用户反馈的 13 项问题,逐条分析合理性并给出优化建议。
>
> **验证状态**:已逐条对照源码验证,标注 ✅ 确认存在 / ⚠️ 部分存在 / ❌ 不存在或已实现。
---
## 一、薪税管理发薪模块
### 1.1 发薪数据每月变化需重新导入(反馈 #1)
**用户反馈**:每个月有变化都要重新导入,需要在好几个模块/系统间来回切换,不便捷。
**现状分析**:当前发薪流程为:花名册员工数据 → 批量导入薪资数据 → 计算社保公积金 → 生成工资条 → 归档。每月需重新导入薪资批次数据。
**代码验证**:✅ 确认存在。当前薪税流程需手动导入或手动创建批次,无「复制上月批次」功能。月度导入模板支持考勤/加班/薪资调整/社保变动,但无法自动从花名册拉取固定项。
**合理性**:✅ 合理。HR 每月薪资数据确实有变化(加班费、绩效奖金、考勤扣款等),但可以优化流程减少重复操作。
**优化方案**
- 支持「复制上月批次」功能:基于上月归档数据自动生成新月份草稿,仅需修改变化项
- 花名册中已维护的社保基数、公积金基数自动同步到薪税模块,无需重复录入
- 考勤模块确认的考勤数据自动关联到薪税计算(加班时长、请假扣款等)
- 增加「一键取数」功能:从花名册自动拉取基本工资、社保基数等固定项,仅需手动录入变动项
**优先级**P1 高
---
## 二、导入错误提示不清晰
### 2.1 导入提示乱码,无法定位错误(反馈 #2)
**用户反馈**:按模板填写两人导入后提示一堆乱码,实际模板可能没填错,不知道哪里有问题。
**现状分析**`Settings.tsx` 导入预览已有错误展示(`result.errors` 数组),但错误信息可能显示的是后端字段名(英文),对用户来说像"乱码"。且如果用户跳过预览直接导入,错误提示可能不够醒目。
**代码验证**:⚠️ 部分存在。`Settings.tsx:967-968` 已有 toast 提示「有 N 条错误」,预览阶段有完整错误表格(行号/姓名/状态/错误信息),且支持「导出错误日志」Excel 下载。但错误信息内容来自后端,可能包含英文字段名导致「乱码」感。用户若跳过预览直接导入,错误提示在结果区域可能不够醒目。
**合理性**:✅ 合理。导入错误信息需要用户可读、可定位。
**优化方案**
- 错误信息中文化:将后端返回的字段名映射为中文(如 `name``姓名``idCardNumber``身份证号`
- 错误提示格式:`第 X 行,字段「姓名」:不能为空``第 X 行,身份证号格式不正确`
- 导入失败时增加醒目 toast 提示「有 N 条错误,请查看详情」
- 增加「下载错误报告」按钮,导出错误行明细(Excel)
- 预览阶段就展示完整校验结果,不通过预览不允许导入
**优先级**P0 紧急
---
## 三、工资流水导出格式
### 3.1 导出工资流水是否适配银行格式(反馈 #3)
**用户反馈**:工资归档后工资条有显示,导出工资流水处是否适合银行直接关联?
**现状分析**:当前 `PayslipTab.tsx` 无导出功能(已在 20260804 优化清单中列为 P1 待修复项)。工资流水导出需要符合银行代发工资文件格式。
**代码验证**:✅ 确认存在。`PayslipTab.tsx` 无导出功能,仅有「从批次汇总生成」「税率试算」「删除」操作,无银行代发文件导出。
**合理性**:✅ 合理。银行代发工资通常需要特定格式(Excel/CSV,包含姓名、身份证号、银行账号、金额等字段)。
**优化方案**
- 增加「导出银行代发文件」功能,支持常见银行格式(工行、建行、招行等)
- 导出字段:姓名、身份证号、银行账号、开户行、实发金额
- 支持自定义字段映射,适配不同银行要求
- 导出格式支持 Excel 和 CSV
**优先级**P1 高
---
## 四、社保基数与花名册数据不同步
### 4.1 社保公积金办理处仍显示工资数而非社保基数(反馈 #4)
**用户反馈**:花名册中已修改社保基数,但社保公积金办理里还是显示工资数。
**现状分析**:花名册 `BasicInfo.tsx` 中可编辑社保基数(`socialInsuranceBase``housingFundBase`),但社保模块 `SocialInsurance.tsx` 的月度办理可能直接取 `salary` 字段而非社保基数字段。
**代码验证**:❌ 不存在(已正确实现)。`payroll.service.ts:166-167` 明确代码:`const socialBase = employee.socialInsBase || inputs.baseSalary`,即优先使用花名册中的 `socialInsBase`,未设置时才用基本工资。`contract.service.ts:205-206` 入职时同样 `socialInsBase != null ? socialInsBase : salaryNum`。社保月度办理 `MonthlyRows.tsx` 显示的 `i.base` 来自 `EmployeeSocialInsRecord` 表,该记录在入职和基数调整时已正确写入。**若用户遇到显示工资数而非社保基数,可能是该员工 `socialInsBase` 字段为 null**,需在花名册 BasicInfo 中设置社保基数。
**合理性**:⚠️ 部分合理。代码逻辑正确(优先用 socialInsBase),但用户若未在花名册设置社保基数,系统会 fallback 到工资数,用户可能不理解这个优先级。建议增加 UI 提示。
**优化方案**
- 社保公积金月度办理中,基数取值优先级:`socialInsuranceBase` > `salary`(如果社保基数已设置则用社保基数,否则用工资)
- 同理公积金基数取值:`housingFundBase` > `salary`
- 在社保办理界面显示数据来源标注(「来自花名册社保基数」或「来自月工资」)
- 增加「一键同步花名册社保基数」按钮
**优先级**P0 紧急
---
## 五、商险管理添加员工入口缺失
### 5.1 商险方案添加员工无入口(反馈 #5)
**用户反馈**:添加了商险方案,但不知道如何添加员工,员工办理里也没有商险选项。
**现状分析**:需确认商险模块是否有员工关联功能。
**代码验证**:✅ 确认存在。`CommercialInsuranceTab.tsx` 有方案 CRUD 和参保人员查看(`enrollments`),但**无「添加参保员工」按钮**。API 层 `commercialInsuranceApi` 仅有 `plans/enrollments/savePlan/removePlan`,缺少 `addEnrollment` 接口。员工办理 `WorkProcess.tsx` 的 13 种流程类型中也无商险参保类型。
**合理性**:✅ 合理。商险方案创建后应能关联员工。
**优化方案**
- 商险方案详情页增加「添加参保员工」按钮,支持批量选择员工
- 员工办理(WorkProcess)中增加商险参保/退保流程类型
- 花名册员工详情中增加商险参保状态展示
**优先级**P1 高
---
## 六、员工福利月度汇总无数据
### 6.1 福利添加人员后月度汇总无数(反馈 #6)
**用户反馈**:员工福利能添加人员,但月度汇总处没有数。
**现状分析**:需确认福利模块的汇总逻辑是否正确关联了已添加的福利人员数据。
**代码验证**:❌ 不存在(系统中无福利模块)。搜索全部前端代码无 `benefit``福利``welfare` 关键词。系统无员工福利管理模块,用户可能指的是其他模块(如社保或商险)的功能。
**合理性**:⚠️ 需澄清。系统当前无福利模块,需与用户确认具体指的是哪个功能。
**优化方案**
- 检查月度汇总查询逻辑,确保关联了福利人员表
- 汇总维度:按福利类型汇总人数和金额、按部门汇总
- 添加人员后自动刷新汇总数据
- 汇总页面增加「刷新」按钮
**优先级**P1 高
---
## 七、证据链完整性验证
### 7.1 验证全部完整性显示全部异常(反馈 #7)
**用户反馈**:证据链条中验证全部完整性,显示全部异常,不清楚如何验证。
**现状分析**`Termination.tsx` 中有仲裁证据链模块,验证逻辑可能过于严格或验证条件不明确。
**代码验证**:⚠️ 部分存在。`Evidence.tsx` 有「验证全部完整性」按钮,调用 `evidenceApi.verifyAll()`,返回 `verifyResult.invalid` 计数。验证逻辑是后端 Hash 校验(SHA256),若全部异常可能是:①数据库中证据链记录的 hash 与实际数据不匹配;②证据链记录为空时验证逻辑有 bug。前端只显示「N 条通过,M 条异常」,**不显示具体异常原因和修复建议**。`EvidenceChain.tsx`(员工维度)有证据列表和风险提醒,但无验证功能。
**合理性**:✅ 合理。用户不理解验证规则,且全部异常说明验证逻辑可能有问题。
**优化方案**
- 验证结果中显示具体异常原因(如「缺少劳动合同扫描件」「考勤记录不完整」等)
- 每个验证项增加「查看要求」说明,告知用户需要什么材料
- 验证标准可配置:区分「必须项」和「建议项」,必须项缺失才标红
- 增加「验证说明」帮助文档
**优先级**P2 中
---
## 八、员工办理必填项缺失
### 8.1 只填姓名也能录入,关键信息未设必填(反馈 #8)
**用户反馈**:员工办理中只填姓名也能录入,建议身份证号等关键信息设为必填。
**现状分析**`WorkProcess.tsx` 的入职登记表单中,`employeeName` 可能是唯一必填项,`idCardNumber` 等字段未设必填。
**代码验证**:✅ 确认存在。`WorkProcess.tsx:231-242``handleCreate()` 仅校验 `selectedType` 非空,不校验任何表单字段。`FORM_FIELDS` 配置中无 `required` 标记,所有字段都是选填。后端 `work-process.service.ts` 也未对 formData 做必填校验。HIRE 类型有 `idCardNumber` 字段但非必填,只填姓名即可创建草稿并提交。
**合理性**:✅ 合理。关键信息缺失会导致后续业务(社保、合同、工资)无法正常办理。
**优化方案**
- 入职登记必填项:姓名、身份证号、手机号、部门
- 用工办理其他流程类型根据类型设置相应必填项
- 前端表单增加必填校验提示
- 后端 schema 同步增加必填校验
**优先级**P0 紧急
---
## 九、员工删除限制
### 9.1 已办理完毕的员工无法删除(反馈 #9)
**用户反馈**:员工已办理完毕,想删除无法删除,必须做离职处理。
**现状分析**:系统设计中员工记录不允许物理删除,只能通过离职流程标记为 `TERMINATED` 状态。这是合理的数据管理设计。
**代码验证**:✅ 确认存在。`Roster.tsx` 中无删除员工按钮(搜索结果中 Roster.tsx 不含 Trash/delete 相关代码)。花名册仅支持「添加员工」「离职」「重新入职」,不支持物理删除。`WorkProcess.tsx` 中仅可删除草稿记录,不可删除已完成的员工档案。
**合理性**:⚠️ 部分合理。从数据合规角度,员工记录不应物理删除(需保留人事档案)。但如果是录入错误的测试数据,应提供清理机制。
**优化方案**
- 保持正式员工不可删除,只能离职处理(合规要求)
- 增加「作废」功能:仅限录入错误且无关联业务数据(无合同、无工资记录)的员工可作废
- 作废后数据保留但不在花名册显示,管理员可在设置中查看作废记录
- 增加「测试数据清理」功能:管理员可一键清除所有测试员工
**优先级**P2 中
---
## 十、离职审批状态无法更改
### 10.1 待审批状态无法更改或找不到更改入口(反馈 #10)
**用户反馈**:选择待审批后,再点进去状态无法更改,没找到更改入口。
**现状分析**`Termination.tsx` 中离职流程选择「待审批」后,可能缺少审批操作入口或状态流转不完整。
**代码验证**:❌ 不存在(已实现审批功能)。`Termination.tsx:967-993` 在详情视图中,当 `draftDetail.status === 'PENDING_APPROVAL'` 时,显示审批意见输入框和「审批通过」「驳回」按钮,分别调用 `approveMutation``rejectMutation`。列表视图中也有快捷审批图标按钮(828-846行)。API 层 `terminationApi.approve/reject` 完整。**用户可能是没找到详情入口**——需点击列表中的审批图标或详情按钮进入详情视图才能操作。
**合理性**:⚠️ 部分合理。功能已存在,但入口可能不够明显,用户没找到操作位置。建议优化 UI 引导。
**优化方案**
- 离职详情页增加「审批」按钮(通过/驳回/退回修改)
- 待审批状态列表增加批量审批功能
- 审批操作记录审批人、审批时间、审批意见
- 增加审批通知提醒
**优先级**P0 紧急
---
## 十一、花名册职责边界
### 11.1 花名册应只读,修改到相应模块操作(反馈 #11)
**用户反馈**:花名册应该只能查看,修改应到相应模块,否则太乱。
**现状分析**:当前花名册 `Roster.tsx` 集成了大量操作:添加员工、薪资调整、部门变更、离职、重新入职等。`EmployeeProfile` 中还能直接编辑基本信息、合同、薪酬等。
**代码验证**:⚠️ 部分存在。`Roster.tsx` 列表支持添加员工、批量导入、导出,点击进入 `EmployeeProfile` 后可编辑基本信息、合同、薪酬等。操作确实集中在花名册中,但各编辑区域已有模块化分区(BasicInfo/ContractInfo/SalaryInfo 等)。
**合理性**:⚠️ 部分合理。花名册作为统一查看入口是合理的,但编辑入口确实可以更清晰。
**优化方案**
- 花名册列表保持只读查看,点击进入员工详情
- 员工详情页保留编辑功能,但按模块分区并标注来源模块(如「基本信息 → 员工档案模块」「合同信息 → 合同管理模块」)
- 每个编辑区域增加「前往该模块」链接,方便用户到对应模块操作
- 花名册列表的操作按钮(薪资调整、离职等)保留,但增加模块跳转提示
- 不建议完全移除编辑功能,因为会降低操作效率
**优先级**P3 规划(涉及大量 UI 重构)
---
## 十二、招聘模块缺失
### 12.1 系统没有招聘模块(反馈 #12)
**用户反馈**HR 最重要的招聘环节为什么没有?
**现状分析**:系统目前覆盖入职→在职→离职全生命周期,但缺少招聘前端的简历管理、面试安排、Offer 管理等环节。
**代码验证**:✅ 确认存在。搜索全部前端代码无 `recruit``resume``interview``招聘``简历``面试` 相关页面或路由。系统无招聘模块。
**合理性**:✅ 合理。招聘是 HR 核心功能之一。
**优化方案**
- 新建招聘模块,包含:
- **简历管理**:简历导入/录入、简历筛选、状态流转(待筛选→初试→复试→Offer→入职/淘汰)
- **面试管理**:面试安排、面试评价、面试日历
- **Offer 管理**Offer 模板、Offer 发放、接受/拒绝跟踪
- **招聘渠道**:渠道管理、来源统计
- **人才库**:未录用候选人归档,未来岗位匹配
- 与花名册打通:入职时自动从简历库拉取候选人信息
- 招聘数据看板:渠道转化率、招聘周期、录用率等
**优先级**:P3 规划(大型新功能,建议单独规划版本)
---
## 十三、角色权限分工
### 13.1 专员和主管的权限分工(反馈 #13)
**用户反馈**:工作角色分类,是否考虑有专员和主管的权限分工。
**现状分析**:当前系统角色为 `SUPER_ADMIN``ADMIN``HR``VIEWER` 四级,`HR` 角色拥有全部 HR 操作权限,无专员/主管区分。
**代码验证**:✅ 确认存在。`authStore.ts` 中 User 角色为 `SUPER_ADMIN | ADMIN | HR | VIEWER` 四级,无专员/主管区分。所有 HR 角色拥有相同权限,无法按模块或操作类型细分。
**合理性**:✅ 合理。中大型企业需要更细粒度的权限控制。
**优化方案**
- 角色体系扩展:
- `HR_SUPERVISOR`(HR 主管):全部查看 + 审批权限 + 配置权限
- `HR_SPECIALIST`(HR 专员):数据录入 + 日常操作,无审批和配置权限
- 权限粒度:
- 查看权限:全部模块可查看
- 操作权限:专员可录入/修改,主管可审批/删除/导出
- 配置权限:仅主管/管理员可修改系统配置(社保比例、薪资结构等)
- 审批流:离职、薪资调整等关键操作需主管审批
- 可按模块设置权限(如考勤专员只能操作考勤模块)
**优先级**P2 中
---
## 优先级汇总
| 优先级 | 编号 | 问题 | 状态 |
|--------|------|------|------|
| 优先级 | 编号 | 问题 | 验证状态 | 修复状态 |
|--------|------|------|----------|----------|
| ~~P0~~ | 2.1 | 导入错误提示乱码 | ⚠️ 部分存在(已有错误展示和导出,但信息可能含英文字段名) | 降级 P1 |
| ~~P0~~ | 4.1 | 社保基数不同步 | ❌ 不存在(代码已正确实现优先级 socialInsBase > salary | 降级 P2(增加 UI 提示) |
| P0 紧急 | 8.1 | 员工办理关键信息未设必填 | ✅ 确认存在 | 待修复 |
| ~~P0~~ | 10.1 | 离职审批状态无法更改 | ❌ 不存在(已实现完整审批流程) | 无需修复(优化 UI 引导) |
| P1 高 | 1.1 | 发薪数据每月需重新导入 | ✅ 确认存在 | 待优化 |
| P1 高 | 3.1 | 工资流水导出格式适配银行 | ✅ 确认存在 | 待开发 |
| P1 高 | 5.1 | 商险管理添加员工入口缺失 | ✅ 确认存在 | 待修复 |
| ~~P1~~ | 6.1 | 员工福利月度汇总无数据 | ❌ 系统无福利模块 | 需澄清需求 |
| P2 中 | 7.1 | 证据链验证不清晰 | ⚠️ 部分存在(有验证但不显示具体原因) | 待优化 |
| P2 中 | 9.1 | 员工删除限制 | ✅ 确认存在(无删除入口) | 待优化 |
| P2 中 | 13.1 | 角色权限分工 | ✅ 确认存在 | 待规划 |
| P3 规划 | 11.1 | 花名册职责边界重构 | ⚠️ 部分存在 | 待规划 |
| P3 规划 | 12.1 | 招聘模块 | ✅ 确认不存在 | 待规划 |
---
## 备注
## 代码验证结论
### 已确认存在的问题(需修复)
- **#8 员工办理必填项缺失**`WorkProcess.tsx` 无任何表单必填校验,只填姓名即可提交
- **#5 商险添加员工入口缺失**`CommercialInsuranceTab.tsx` 无添加参保员工按钮和 API
- **#1 发薪数据重复导入**:无「复制上月批次」功能
- **#3 工资流水无银行格式导出**`PayslipTab.tsx` 无导出功能
- **#9 员工无法删除**:花名册无删除入口
- **#12 无招聘模块**:系统中完全不存在
- **#13 角色权限无专员/主管分工**:仅四级角色
### 不存在或已实现的问题(无需修复)
- **#4 社保基数不同步**`payroll.service.ts:166` 已正确实现 `socialInsBase || salary` 优先级,问题可能是用户未设置 `socialInsBase`
- **#10 离职审批无法更改**`Termination.tsx:967-993` 已实现完整审批操作(通过/驳回),用户可能未找到详情入口
- **#6 员工福利汇总无数据**:系统中无福利模块,需澄清用户具体指什么
### 部分存在的问题(可优化)
- **#2 导入错误乱码**:已有错误展示和导出功能,但错误信息可能含英文字段名
- **#7 证据链验证**:有验证功能但不显示具体异常原因
- **#11 花名册职责边界**:编辑功能已模块化分区但确实集中在花名册中
### 备注
- #4#10 经代码验证不构成 bug,降级处理
- #6 需与用户澄清具体需求
- #12 招聘模块为大型新功能,建议单独规划版本迭代
+50 -29
View File
@@ -49,31 +49,31 @@ const STATUS_CONFIG: Record<string, { label: string; color: string }> = {
CANCELLED: { label: '已撤销', color: 'bg-gray-100 text-gray-400' },
}
// 各流程类型的表单字段配置
const FORM_FIELDS: Record<string, { key: string; label: string; type: 'text' | 'date' | 'number' | 'select' | 'textarea' | 'enterprise-template' | 'employee-select' | 'contract-select'; options?: string[] }[]> = {
// 各流程类型的表单字段配置required 标记必填项)
const FORM_FIELDS: Record<string, { key: string; label: string; type: 'text' | 'date' | 'number' | 'select' | 'textarea' | 'enterprise-template' | 'employee-select' | 'contract-select'; options?: string[]; required?: boolean }[]> = {
HIRE: [
{ key: 'name', label: '员工姓名', type: 'text' },
{ key: 'department', label: '部门', type: 'text' },
{ key: 'hireDate', label: '入职日期', type: 'date' },
{ key: 'name', label: '员工姓名', type: 'text', required: true },
{ key: 'department', label: '部门', type: 'text', required: true },
{ key: 'hireDate', label: '入职日期', type: 'date', required: true },
{ key: 'monthlySalary', label: '月薪', type: 'number' },
{ key: 'phone', label: '手机号', type: 'text' },
{ key: 'idCardNumber', label: '身份证号', type: 'text' },
{ key: 'phone', label: '手机号', type: 'text', required: true },
{ key: 'idCardNumber', label: '身份证号', type: 'text', required: true },
{ key: 'gender', label: '性别', type: 'select', options: ['男', '女'] },
{ key: 'contractStartDate', label: '合同开始日期', type: 'date' },
{ key: 'contractEndDate', label: '合同结束日期', type: 'date' },
],
ONBOARD: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'hireDate', label: '入职日期', type: 'date' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'hireDate', label: '入职日期', type: 'date', required: true },
],
CUSTOM_CONTRACT: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'contractStartDate', label: '合同开始日期', type: 'date' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'contractStartDate', label: '合同开始日期', type: 'date', required: true },
{ key: 'contractEndDate', label: '合同结束日期', type: 'date' },
{ key: 'contractType', label: '合同类型', type: 'select', options: ['FIXED', 'UNFIXED', 'INTERNSHIP'] },
],
INFO_SUBMIT: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'department', label: '部门', type: 'text' },
{ key: 'phone', label: '手机号', type: 'text' },
{ key: 'address', label: '地址', type: 'text' },
@@ -81,51 +81,65 @@ const FORM_FIELDS: Record<string, { key: string; label: string; type: 'text' | '
{ key: 'emergencyPhone', label: '紧急联系电话', type: 'text' },
],
CONFIRM: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'confirmDate', label: '转正日期', type: 'date' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'confirmDate', label: '转正日期', type: 'date', required: true },
{ key: 'regularSalary', label: '转正薪资', type: 'number' },
],
CHANGE: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'contractId', label: '选择合同', type: 'contract-select' },
{ key: 'newEndDate', label: '新到期日期', type: 'date' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'contractId', label: '选择合同', type: 'contract-select', required: true },
{ key: 'newEndDate', label: '新到期日期', type: 'date', required: true },
],
RENEW: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'oldContractId', label: '原合同ID', type: 'text' },
{ key: 'newStartDate', label: '新合同开始日期', type: 'date' },
{ key: 'newStartDate', label: '新合同开始日期', type: 'date', required: true },
{ key: 'newEndDate', label: '新合同结束日期', type: 'date' },
{ key: 'newSalary', label: '新薪资', type: 'number' },
],
SUSPEND: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'contractId', label: '选择合同', type: 'contract-select' },
{ key: 'suspendDate', label: '中止日期', type: 'date' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'contractId', label: '选择合同', type: 'contract-select', required: true },
{ key: 'suspendDate', label: '中止日期', type: 'date', required: true },
],
INCOME_CERT: [
{ key: 'enterpriseTemplateId', label: '关联企业模板(选填)', type: 'enterprise-template' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'employeeName', label: '员工姓名', type: 'text' },
{ key: 'idCardNumber', label: '身份证号', type: 'text' },
{ key: 'position', label: '职务', type: 'text' },
{ key: 'monthlyIncome', label: '月收入', type: 'text' },
{ key: 'purpose', label: '用途', type: 'text' },
],
TERMINATE: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'contractId', label: '选择合同', type: 'contract-select' },
{ key: 'terminateDate', label: '终止日期', type: 'date', required: true },
{ key: 'reason', label: '终止原因', type: 'select', options: ['EXPIRED', 'NEGOTIATED', 'FAULT', 'NONFAULT', 'LAYOFF'], required: true },
{ key: 'compensation', label: '经济补偿金', type: 'number' },
],
RESCIND: [
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'contractId', label: '选择合同', type: 'contract-select' },
{ key: 'rescindDate', label: '解除日期', type: 'date', required: true },
{ key: 'reason', label: '解除原因', type: 'select', options: ['NEGOTIATED', 'FAULT', 'NONFAULT', 'LAYOFF'], required: true },
{ key: 'compensation', label: '经济补偿金', type: 'number' },
],
LEAVING_CERT: [
{ key: 'enterpriseTemplateId', label: '关联企业模板(选填)', type: 'enterprise-template' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select' },
{ key: 'employeeId', label: '选择员工', type: 'employee-select', required: true },
{ key: 'employeeName', label: '员工姓名', type: 'text' },
{ key: 'idCardNumber', label: '身份证号', type: 'text' },
{ key: 'position', label: '职务', type: 'text' },
{ key: 'hireDate', label: '入职日期', type: 'date' },
{ key: 'leaveDate', label: '离职日期', type: 'date' },
{ key: 'leaveDate', label: '离职日期', type: 'date', required: true },
],
FLEXIBLE: [
{ key: 'name', label: '姓名', type: 'text' },
{ key: 'name', label: '姓名', type: 'text', required: true },
{ key: 'phone', label: '手机号', type: 'text' },
{ key: 'idCardNumber', label: '身份证号', type: 'text' },
{ key: 'idCardNumber', label: '身份证号', type: 'text', required: true },
{ key: 'department', label: '部门', type: 'text' },
{ key: 'agreementStartDate', label: '协议开始日期', type: 'date' },
{ key: 'agreementStartDate', label: '协议开始日期', type: 'date', required: true },
{ key: 'agreementEndDate', label: '协议结束日期', type: 'date' },
{ key: 'payMethod', label: '计酬方式', type: 'text' },
],
@@ -256,6 +270,13 @@ export default function WorkProcess() {
toast.error('请选择流程类型')
return
}
// 校验必填项
const requiredFields = (FORM_FIELDS[selectedType] || []).filter(f => f.required)
const missingFields = requiredFields.filter(f => !formData[f.key] || String(formData[f.key]).trim() === '')
if (missingFields.length > 0) {
toast.error(`请填写必填项:${missingFields.map(f => f.label).join('、')}`)
return
}
createMutation.mutate({
type: selectedType,
title: PROCESS_TYPES[selectedType].label,
@@ -510,7 +531,7 @@ export default function WorkProcess() {
<div className="text-xs text-gray-500 mb-2">{PROCESS_TYPES[selectedType]?.description}</div>
{(FORM_FIELDS[selectedType] || []).map(field => (
<div key={field.key}>
<Label>{field.label}</Label>
<Label>{field.label}{field.required && <span className="text-danger ml-0.5">*</span>}</Label>
{field.type === 'select' ? (
<Select value={formData[field.key] || ''} onChange={(e) => handleFieldChange(field.key, e.target.value)}>
<option value=""></option>