feat: 节假日配置、排班管理独立页面、考勤加班显示优化

- 新增 HolidayConfig 模型,支持法定节假日和调休工作日配置
- 加班费同步逻辑改用 HolidayConfig 判断日期类型
- 员工端考勤显示加班工时、费率和日期类型
- 周末/节假日出勤状态显示为"周末出勤"/"节假日出勤"
- 新增 Employee.defaultShiftId 字段,支持长期排班(工作日班次)
- 排班管理拆分为独立页面(班次管理+排班),考勤管理保留出勤相关功能
- 排班和每日出勤页面增加身份证号列
- 修复岗位和部门编辑失败问题(POST 改 PUT)
- 新增 backfill 脚本:合同薪资回填、默认班次回填

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-18 11:00:53 +08:00
parent eb91c2d8fb
commit 1feada76d1
20 changed files with 1482 additions and 484 deletions
+24 -2
View File
@@ -401,6 +401,8 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
// 默认密码:手机号后6位(员工可在员工端自行修改)
const defaultPassword = data.phone ? data.phone.slice(-6) : '123456'
const passwordHash = await bcrypt.hash(defaultPassword, 10)
// 查找该组织的第一个班次作为默认排班(长期=工作日班次)
const firstShift = await tx.shift.findFirst({ where: { orgId }, orderBy: { createdAt: 'asc' } })
const emp = await tx.employee.create({
data: {
orgId,
@@ -420,6 +422,7 @@ export async function createEmployee(orgId: string, userId: string, data: any) {
isPregnant: data.isPregnant || false,
isInMedicalPeriod: data.isInMedicalPeriod || false,
isWorkInjured: data.isWorkInjured || false,
defaultShiftId: firstShift?.id || null,
socialInsBase,
housingFundBase,
socialInsStartMonth,
@@ -1027,6 +1030,25 @@ export async function addContract(orgId: string, userId: string, data: any) {
throw { code: 'DUPLICATE', message: '该员工已存在相同日期的合同,请勿重复添加' }
}
// 工资结构:baseSalary 为 0 时,用员工档案月工资填充
let contractBase = Number(data.baseSalary) || 0
let contractPerf = Number(data.performanceSalary) || 0
if (contractBase === 0 && contractPerf === 0) {
const emp = await prisma.employee.findFirst({
where: { id: data.employeeId, orgId },
select: { monthlySalary: true, baseSalary: true, performanceSalary: true },
})
if (emp) {
try {
contractBase = emp.baseSalary ? Number(decrypt(emp.baseSalary)) || 0 : 0
contractPerf = emp.performanceSalary ? Number(decrypt(emp.performanceSalary)) || 0 : 0
} catch {}
if (contractBase === 0 && contractPerf === 0) {
try { contractBase = Number(decrypt(emp.monthlySalary)) || 0 } catch {}
}
}
}
const contractMonths = data.endDate
? Math.ceil(daysBetween(new Date(data.endDate), new Date(data.startDate)) / 30.44)
: data.contractYears * 12
@@ -1048,8 +1070,8 @@ export async function addContract(orgId: string, userId: string, data: any) {
contractYears: data.contractYears || 3,
probationMonths: data.probationMonths || 0,
probationSalary: data.probationSalary || 0,
baseSalary: data.baseSalary || 0,
performanceSalary: data.performanceSalary || 0,
baseSalary: contractBase,
performanceSalary: contractPerf,
attachmentName: data.attachmentUrl ? '合同扫描件' : null,
attachmentUrl: data.attachmentUrl || null,
electronicContractNo: data.electronicContractNo || null,