# 社保公积金优化方案

## 核心原则

- 社保和公积金**完全分离**：独立配置、独立调基、独立增减员、独立申报
- 社保公积金开始/截止年月**必填**，增减变以此为准
- 基数缺省等于工资，可修改
- 调薪后社保公积金基数**不自动调整**（社保基数通常每年7月统一调基，调薪仅影响发薪基数）
- 发薪列表和社保/公积金申报列表中，入离职日期与社保公积金年月不一致时**提醒**
- 所有变更（入职/重新入职/调基/调薪/调部门/离职/解聘）都按**版本记录**保存，算薪和月度处理时按月份获取当前有效版本

---

## 一、Schema 改动

### 1.1 拆分配置模型

现有 `SocialInsuranceConfig`（含社保+公积金比例）拆为：

- **`SocialInsuranceConfig`**（保留，移除公积金字段）：养老/医疗/失业/工伤/生育比例 + 社保基数上下限 + 生效月份 + 版本管理 + `adjustmentDone` 标记
- **`HousingFundConfig`**（新增）：公积金企业/个人比例 + 公积金基数上下限 + 生效月份 + 版本管理 + `adjustmentDone` 标记（字段结构同社保配置）

> Organization 和 Employee 需增加反向关联字段：
> - Organization: `housingFundConfigs HousingFundConfig[]`、`socialInsRecords EmployeeSocialInsRecord[]`、`housingFundRecords EmployeeHousingFundRecord[]`、`departmentRecords EmployeeDepartmentRecord[]`（`salaryChangeRecords` 已存在）
> - Employee: `socialInsRecords EmployeeSocialInsRecord[]`、`housingFundRecords EmployeeHousingFundRecord[]`、`departmentRecords EmployeeDepartmentRecord[]`（`salaryChanges` 已存在）

### 1.2 新增模型：社保/公积金缴费记录（按版本保存）

社保和公积金的基数、起止年月不是 Employee 上的简单字段，而是按**版本记录**保存。每次入职/重新入职/调基/离职/解聘都生成新版本，形成完整变更历史。

#### EmployeeSocialInsRecord（社保缴费记录）

```prisma
model EmployeeSocialInsRecord {
  id           String   @id @default(cuid())
  orgId        String
  org          Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
  employeeId   String
  employee     Employee   @relation(fields: [employeeId], references: [id], onDelete: Cascade)
  startMonth   String   // 开始缴费年月 YYYY-MM
  endMonth     String?  // 截止缴费年月 YYYY-MM（null=至今有效）
  base         Float    // 缴费基数
  // 变更来源
  changeType   String   // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘
  changeRefId  String?  // 关联的 TerminationRecord ID（离职/解聘时）
  remark       String?
  createdBy    String
  createdAt    DateTime @default(now())

  @@index([orgId, employeeId])
  @@index([employeeId, startMonth, endMonth])  // 复合索引：按员工+月份查询有效版本
}
```

#### EmployeeHousingFundRecord（公积金缴费记录）

```prisma
model EmployeeHousingFundRecord {
  id           String   @id @default(cuid())
  orgId        String
  org          Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
  employeeId   String
  employee     Employee   @relation(fields: [employeeId], references: [id], onDelete: Cascade)
  startMonth   String   // 开始缴费年月 YYYY-MM
  endMonth     String?  // 截止缴费年月 YYYY-MM（null=至今有效）
  base         Float    // 缴费基数
  // 变更来源
  changeType   String   // ONBOARDING=入职, REHIRE=重新入职, ADJUST=调基, TERMINATION=离职/解聘
  changeRefId  String?  // 关联的 TerminationRecord ID（离职/解聘时）
  remark       String?
  createdBy    String
  createdAt    DateTime @default(now())

  @@index([orgId, employeeId])
  @@index([employeeId, startMonth, endMonth])  // 复合索引：按员工+月份查询有效版本
}
```

#### Employee 保留便捷字段（当前生效值，由后端同步维护）

```
socialInsStartMonth   String?  // 当前社保开始年月（=最新记录的startMonth）
socialInsBase         Float?   // 当前社保基数（=最新记录的base）
socialInsEndMonth     String?  // 当前社保截止年月（=最新记录的endMonth，null=在保）
housingFundStartMonth String?  // 当前公积金开始年月
housingFundBase       Float?   // 当前公积金基数
housingFundEndMonth   String?  // 当前公积金截止年月
```

> 这些字段是冗余的便捷查询字段，由后端在创建/更新缴费记录时自动同步。增减员和在职申报查询主要使用 Record 表，发薪计算使用 Employee 便捷字段。

### 1.3 TerminationRecord 增加字段

```
socialInsEndMonth     String   // 社保截止缴费年月 YYYY-MM（必填）
housingFundEndMonth   String   // 公积金截止缴费年月 YYYY-MM（必填）
```

> TerminationRecord 保存截止年月的同时，后端自动创建一条 EmployeeSocialInsRecord / EmployeeHousingFundRecord，将上一条有效记录的 endMonth 设为此值，并同步 Employee 便捷字段。

### 1.4 扩展模型：调薪/调部门按版本保存

调薪和调部门也按**版本记录**保存，与社保公积金缴费记录同理。每次变更生成新版本，算薪和社保公积金月度处理时获取当前最新版。

#### 扩展现有 SalaryChangeRecord（增加版本字段）

现有 `SalaryChangeRecord` 已有 `oldSalary`/`newSalary`/`effectiveDate`/`reason`，与其新建模型，直接扩展：

```prisma
// 在现有 SalaryChangeRecord 增加字段：
  effectiveMonth String   // 生效年月 YYYY-MM（从 effectiveDate 转换）
  endMonth       String?  // 失效年月 YYYY-MM（null=至今有效，被新版本覆盖时设置）
  changeType     String   @default("SALARY_CHANGE") // ONBOARDING=入职, REHIRE=重新入职, SALARY_CHANGE=调薪

  @@index([employeeId, effectiveMonth, endMonth])  // 复合索引
```

> 不新建 `EmployeeSalaryRecord`，直接复用 `SalaryChangeRecord`，避免数据分散。入职时也创建一条（oldSalary=0, newSalary=月薪, changeType=ONBOARDING）。

#### EmployeeDepartmentRecord（部门变更记录，新增模型）

```prisma
model EmployeeDepartmentRecord {
  id           String   @id @default(cuid())
  orgId        String
  org          Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
  employeeId   String
  employee     Employee   @relation(fields: [employeeId], references: [id], onDelete: Cascade)
  oldDepartment String  // 调整前部门
  newDepartment String  // 调整后部门
  effectiveMonth String // 生效年月 YYYY-MM
  endMonth     String?  // 失效年月 YYYY-MM（null=至今有效）
  reason       String?  // 调部门原因
  changeType   String   // ONBOARDING=入职, REHIRE=重新入职, TRANSFER=调部门
  createdBy    String
  createdAt    DateTime @default(now())

  @@index([orgId, employeeId])
  @@index([employeeId, effectiveMonth, endMonth])  // 复合索引
}
```

> Employee 上的 `monthlySalary` 和 `department` 作为便捷字段由后端同步维护。

### 1.5 PayrollBatchType 增加枚举

```
SEVERANCE  // 补偿金按月发放（无社保，个税按政策处理）
```

### 1.6 数据迁移策略

Schema 改动后，需要为现有员工创建初始 Record：

- **EmployeeSocialInsRecord**：为每个现有员工创建一条，`startMonth` = 入职日期年月，`endMonth` = 已离职员工的离职日期年月（如有），`base` = 现有 `socialInsBase` 或月薪，`changeType` = 'ONBOARDING'
- **EmployeeHousingFundRecord**：同上，`base` = 现有 `housingFundBase` 或月薪
- **SalaryChangeRecord**：为每个现有员工创建一条初始记录，`oldSalary` = 0, `newSalary` = 当前月薪, `effectiveMonth` = 入职日期年月, `endMonth` = null
- **EmployeeDepartmentRecord**：为每个现有员工创建一条，`oldDepartment` = '', `newDepartment` = 当前部门, `effectiveMonth` = 入职日期年月, `endMonth` = null
- **迁移脚本**：`npx prisma db push` 后执行一次性迁移脚本 `scripts/migrate-records.ts`

---

## 二、需求1：新增/重新入职填写社保公积金开始年月+基数

### 前端 AddEmployeeModal

- 新增4个必填字段（2列布局）：
  - 社保开始年月（type=month，缺省=入职日期年月，可修改）
  - 社保基数（type=number，缺省=月薪，可修改）
  - 公积金开始年月（type=month，缺省=入职日期年月，可修改）
  - 公积金基数（type=number，缺省=月薪，可修改）
- 当入职日期变更时（`handleHireDateChange`），自动同步4个缺省值
- `canSubmit` 增加这4个字段的必填校验

### 前端 RehireModal

- 同 AddEmployeeModal，缺省=新入职日期年月

### 后端

- `createEmployeeSchema` 增加 `socialInsStartMonth`、`socialInsBase`、`housingFundStartMonth`、`housingFundBase`（必填）
- `createEmployee` 存储这些字段到 Employee 便捷字段，**同时创建一条 `EmployeeSocialInsRecord`（changeType=ONBOARDING）和一条 `EmployeeHousingFundRecord`（changeType=ONBOARDING）**
- `rehireEmployee` 接收并更新这些字段，**同时创建新版本缴费记录（changeType=REHIRE）**，并将之前有效记录的 endMonth 设为重新入职前一个月

---

## 二.5 需求补充：花名册增加调薪/调部门操作

### 前端花名册列表

- 每行操作区增加「调薪」「调部门」按钮（与「离职」并列）

### 前端调薪弹窗（SalaryChangeModal）

- 显示：员工姓名、当前月薪、当前部门
- 输入：
  - 新月薪（必填，缺省=当前月薪）
  - 生效年月（type=month，必填，缺省=当月）
  - 调薪原因（选填）
- 提交后：
  - 后端创建 `SalaryChangeRecord`（oldSalary=当前月薪，newSalary=新月薪，effectiveMonth=生效年月, changeType=SALARY_CHANGE）
  - 将之前有效记录的 `endMonth` 设为生效月前一个月
  - 同步 `Employee.monthlySalary` = 新月薪

### 前端调部门弹窗（DepartmentChangeModal）

- 显示：员工姓名、当前部门
- 输入：
  - 新部门（必填，缺省=当前部门）
  - 生效年月（type=month，必填，缺省=当月）
  - 调部门原因（选填）
- 提交后：
  - 后端创建 `EmployeeDepartmentRecord`（oldDepartment=当前部门，newDepartment=新部门，effectiveMonth=生效年月）
  - 将之前有效记录的 `endMonth` 设为生效月前一个月
  - 同步 `Employee.department` = 新部门

### 后端

- `POST /roster/:id/salary-change` — 调薪，创建版本记录 + 同步 Employee
- `POST /roster/:id/department-change` — 调部门，创建版本记录 + 同步 Employee
- `GET /roster/:id/salary-records` — 调薪历史
- `GET /roster/:id/department-records` — 调部门历史

### 算薪和社保公积金月度处理

- 算薪时：根据发薪月份获取该月有效的 `SalaryChangeRecord`（`effectiveMonth <= month` 且 `endMonth == null 或 >= month`），使用该记录的 `newSalary` 作为发薪基数
- 社保公积金月度处理时：根据月份获取该月有效的 `EmployeeSocialInsRecord` / `EmployeeHousingFundRecord`，使用该记录的 `base` 作为缴费基数
- 部门信息：根据月份获取该月有效的 `EmployeeDepartmentRecord`，用于月度报表中的部门归属
- **调薪与社保基数关系**：调薪仅影响发薪基数，**不自动调整**社保公积金基数。社保公积金基数仅在每年7月统一调基时调整

---

## 三、需求2：离职/解聘填写社保公积金截止年月

### 前端 ResignModal（Roster.tsx）

- 新增2个必填字段：
  - 社保截止年月（type=month，缺省=离职日期年月，可修改）
  - 公积金截止年月（type=month，缺省=离职日期年月，可修改）
- 当离职日期变更时，自动同步缺省值
- `canSubmit` 增加必填校验

### 前端 Termination.tsx（解聘向导 Step 1）

- 在解聘日期下方增加社保截止年月、公积金截止年月输入
- 缺省=解聘日期年月，可修改

### 后端

- `terminationChecklistSchema` 增加 `socialInsEndMonth`、`housingFundEndMonth`（必填）
- `createTermination` 和 `createResignation` 存储这些字段到 TerminationRecord
- **同时更新 Employee 便捷字段**（`socialInsEndMonth`、`housingFundEndMonth`）
- **同时创建/更新缴费记录**：将当前有效记录的 `endMonth` 设为截止年月，同步 Employee 便捷字段

---

## 四、需求3：社保公积金Tab增加月度增减员+在职申报+导出

### 4.1 前端 SocialInsurance.tsx 改造

增加顶层 Tab 切换：

- **「社保」Tab**：社保配置管理 + 社保调基 + 社保月度增减员 + 社保在职申报
- **「公积金」Tab**：公积金配置管理 + 公积金调基 + 公积金月度增减员 + 公积金在职申报

每个 Tab 内再分子 Tab：

- 配置管理（现有功能，社保/公积金各自独立）
- 月度增减员
- 在职申报

### 4.2 月度增减员

**后端 API**：

- `GET /social/monthly-changes?month=YYYY-MM` — 社保增减员
- `GET /housing/monthly-changes?month=YYYY-MM` — 公积金增减员

**逻辑**（统一使用 Record 表查询，确保历史月份也能查到已离职员工）：

- **增员**：查 `EmployeeSocialInsRecord.startMonth == month`（姓名、部门、基数、开始年月、changeType）
- **减员**：查 `EmployeeSocialInsRecord.endMonth == month` 且 `changeType == 'TERMINATION'`（姓名、部门、基数、截止年月、离职类型）
- 支持导出 CSV（前端生成，无需后端依赖）

**前端**：选择月份 → 显示增员表和减员表（两个表格或折叠分区）→ 导出按钮

### 4.3 在职申报

**后端 API**：

- `GET /social/active-declaration?month=YYYY-MM` — 社保在保人员
- `GET /housing/active-declaration?month=YYYY-MM` — 公积金在保人员

**逻辑**（使用 Record 表查询）：

- 筛选条件：`EmployeeSocialInsRecord.startMonth <= month` 且 `endMonth == null 或 >= month`
- 返回：姓名、身份证号、部门、社保基数、开始年月、截止年月
- 支持导出 CSV（前端生成，无需后端依赖）

**前端**：选择月份 → 显示在保人员表格 → 导出按钮

### 4.4 调基拆分

现有调基操作同时调整社保和公积金基数。改为：

- 社保调基：只调整社保基数，使用 `SocialInsuranceConfig` 的上下限
  - 将当前有效记录的 `endMonth` 设为调基月前一个月
  - 创建新 `EmployeeSocialInsRecord`（changeType=ADJUST），startMonth=调基月，base=新基数
  - 同步 Employee.socialInsBase / socialInsStartMonth
- 公积金调基：只调整公积金基数，使用 `HousingFundConfig` 的上下限
  - 将当前有效记录的 `endMonth` 设为调基月前一个月
  - 创建新 `EmployeeHousingFundRecord`（changeType=ADJUST），startMonth=调基月，base=新基数
  - 同步 Employee.housingFundBase / housingFundStartMonth
- 两个调基操作独立执行，各自有 `adjustmentDone` 标记

---

## 五、需求4：已离职员工按月发放补偿金

### 后端

- `PayrollBatchType` 增加 `SEVERANCE`
- `calcBatchEntry`：当 `batchType === 'SEVERANCE'` 时：
  - `socialEmp=0`、`housingEmp=0`、`socialOrg=0`、`housingOrg=0`（无社保公积金）
  - `tax`：经济补偿金在当地社平工资3倍以内免征个税，超过部分按单独税率计税。简化处理：`tax=0`，备注注明「补偿金免征个税（社平3倍以内）」，如超过3倍需手动计算
- 允许 `status === 'RESIGNED'` 的员工加入 `SEVERANCE` 批次
- 补偿金发放可设置**发放月数**（如约定发放6个月），到期后自动标记为已完成
- 也可手动停止发放

### 前端 Money.tsx

- 批次类型下拉增加「补偿金发放」选项
- `SEVERANCE` 批次：员工选择列表包含已离职员工
- 输入项简化：只有补偿金金额（baseSalary），无加班/津贴/扣款
- 可设置发放月数
- 工资条显示：社保=0、公积金=0、个税=0（备注：补偿金免征）

---

## 六、需求5：日期不一致提醒

### 发薪列表提醒

在发薪批次详情中，对每个员工检查发薪月份与入离职日期的一致性：

- 发薪月份 < 入职日期年月 → ⚠️ "该员工2025-07入职，当前发薪月份2025-06尚未入职"
- 发薪月份 > 离职日期年月 → ⚠️ "该员工已于2025-06离职，当前发薪月份2025-07已离职"
- 同时也检查社保公积金年月范围，如有不一致也提醒

### 社保/公积金申报列表提醒

- **增员**：`socialInsStartMonth` 与 `hireDate` 年月不一致 → ⚠️ "社保开始年月与入职日期不一致"
- **减员**：`socialInsEndMonth` 与 `terminationDate` 年月不一致 → ⚠️ "社保截止年月与离职日期不一致"
- **在职申报**：`hireDate` 年月与 `socialInsStartMonth` 不一致、`terminationDate` 年月与 `socialInsEndMonth` 不一致 → ⚠️ 提醒

---

## 七、实施顺序

| 步骤 | 内容 | 涉及 |
|------|------|------|
| 1 | Schema 改动（拆分配置、新增缴费/部门记录模型、扩展SalaryChangeRecord、增加字段、增加枚举）+ `prisma db push` | 后端 |
| 1.5 | 数据迁移脚本：为现有员工创建初始 Record | 后端 |
| 2 | 后端：`createEmployee`/`rehireEmployee` 接收社保公积金字段 + 创建缴费记录版本 | 后端 |
| 3 | 后端：`createTermination`/`createResignation` 接收截止年月 + 更新缴费记录版本 | 后端 |
| 3.5 | 后端：调薪/调部门 API + 创建/扩展版本记录 + 同步 Employee | 后端 |
| 4 | 前端：AddEmployeeModal 增加社保公积金输入 | 前端 |
| 5 | 前端：RehireModal 同步 | 前端 |
| 6 | 前端：ResignModal 增加截止年月 | 前端 |
| 6.5 | 前端：花名册增加调薪/调部门弹窗 | 前端 |
| 7 | 前端：Termination.tsx 解聘向导增加截止年月 | 前端 |
| 8 | 后端：月度增减员 + 在职申报 API | 后端 |
| 9 | 后端：`SEVERANCE` 批次类型 + `calcBatchEntry` 修改 | 后端 |
| 10 | 前端：SocialInsurance.tsx 改造（Tab拆分+增减员+申报+导出） | 前端 |
| 11 | 前端：Money.tsx 增加补偿金批次 | 前端 |
| 12 | 前端：日期不一致提醒 | 前端 |
| 13 | 编译验证 + git 推送 | 全部 |
