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
+18
View File
@@ -166,6 +166,7 @@ model Organization {
departmentRecords EmployeeDepartmentRecord[]
notificationSetting NotificationSetting?
overtimeConfig OvertimeConfig?
holidayConfigs HolidayConfig[]
notificationLogs NotificationLog[]
employeeAttachments EmployeeAttachment[]
disciplinaryRecords DisciplinaryRecord[]
@@ -273,6 +274,8 @@ model Employee {
education String? // 学历(博士/硕士/本科/大专/高中/其他)
femaleWorkerType FemaleWorkerType? // 女性岗位类型(CADRE=干部/WORKER=工人,仅女性需要区分)
retirementDaysLeft Int? // 距退休天数(便捷字段,定期计算)
defaultShiftId String? // 默认班次ID(长期排班=工作日班次,null=未设置)
defaultShift Shift? @relation("EmployeeDefaultShift", fields: [defaultShiftId], references: [id], onDelete: SetNull)
createdBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -637,6 +640,20 @@ model OvertimeConfig {
updatedAt DateTime @updatedAt
}
/// 节假日配置(区分法定节假日和调休工作日)
model HolidayConfig {
id String @id @default(cuid())
orgId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
date DateTime // 日期(仅取日期部分)
type String // HOLIDAY(法定节假日) | WORKDAY(调休工作日,即周末调休上班)
name String? // 节假日名称(如"春节"、"国庆节"
createdAt DateTime @default(now())
@@unique([orgId, date])
@@index([orgId, date])
}
model MedicalPeriodPolicy {
id String @id @default(cuid())
orgId String
@@ -1332,6 +1349,7 @@ model Shift {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assignments ShiftAssignment[]
defaultEmployees Employee[] @relation("EmployeeDefaultShift")
@@index([orgId])
}
@@ -0,0 +1,67 @@
/**
* 迁移脚本:将员工月工资复制到合同的 baseSalary
* 当合同 baseSalary=0 且 performanceSalary=0 时,用员工 monthlySalary 填充 baseSalary
* 月工资 = baseSalary + performanceSalary
*/
import prisma from '../src/lib/prisma'
import { decrypt } from '../src/lib/crypto'
async function main() {
const contracts = await prisma.laborContract.findMany({
where: {
baseSalary: 0,
performanceSalary: 0,
},
include: {
employee: {
select: { monthlySalary: true, baseSalary: true, performanceSalary: true },
},
},
})
console.log(`找到 ${contracts.length} 个合同 baseSalary=0 且 performanceSalary=0`)
let updated = 0
for (const contract of contracts) {
const emp = contract.employee
if (!emp?.monthlySalary) continue
let empBase = 0
let empPerf = 0
try {
empBase = emp.baseSalary ? Number(decrypt(emp.baseSalary)) || 0 : 0
empPerf = emp.performanceSalary ? Number(decrypt(emp.performanceSalary)) || 0 : 0
} catch {}
// 优先用员工的 baseSalary/performanceSalary
let newBase = empBase
let newPerf = empPerf
// 如果员工也没有拆分工资,用 monthlySalary 作为 baseSalary
if (newBase === 0 && newPerf === 0) {
let monthly = 0
try {
monthly = Number(decrypt(emp.monthlySalary)) || 0
} catch {}
if (monthly > 0) {
newBase = monthly
newPerf = 0
}
}
if (newBase > 0 || newPerf > 0) {
await prisma.laborContract.update({
where: { id: contract.id },
data: { baseSalary: newBase, performanceSalary: newPerf },
})
console.log(` ✓ 合同 ${contract.id} (员工 ${contract.employeeId}): base=${newBase}, perf=${newPerf}`)
updated++
}
}
console.log(`\n完成:共更新 ${updated} 个合同`)
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect())
+54
View File
@@ -0,0 +1,54 @@
/**
* 迁移脚本:为现有员工设置默认班次(长期排班=工作日班次)
* 每个员工的默认班次设为该组织的第一个班次
*/
import prisma from '../src/lib/prisma'
async function main() {
// 查找所有没有默认班次的在职员工
const employees = await prisma.employee.findMany({
where: {
status: 'ACTIVE',
defaultShiftId: null,
},
select: { id: true, orgId: true, name: true },
})
console.log(`找到 ${employees.length} 个员工未设置默认班次`)
// 按组织分组
const orgShifts = new Map<string, string>()
let updated = 0
for (const emp of employees) {
// 获取该组织的第一个班次(缓存)
let shiftId = orgShifts.get(emp.orgId)
if (!shiftId) {
const shift = await prisma.shift.findFirst({
where: { orgId: emp.orgId },
orderBy: { createdAt: 'asc' },
})
if (shift) {
shiftId = shift.id
orgShifts.set(emp.orgId, shiftId)
}
}
if (shiftId) {
await prisma.employee.update({
where: { id: emp.id },
data: { defaultShiftId: shiftId },
})
console.log(` ✓ 员工 ${emp.name} → 默认班次已设置`)
updated++
} else {
console.log(` ✗ 员工 ${emp.name} → 该组织无班次配置,跳过`)
}
}
console.log(`\n完成:共更新 ${updated} 个员工`)
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect())
+42
View File
@@ -222,6 +222,48 @@ router.delete('/shift-assignments/:id', authMiddleware, async (req: AuthRequest,
} catch (err) { next(err) }
})
// 设置员工默认班次(长期排班=工作日班次)
router.post('/default-shift', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { employeeId, shiftId } = req.body as { employeeId: string; shiftId: string | null }
if (!employeeId) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 employeeId' } })
// 验证 shiftId 存在且属于该 orgshiftId 为 null 时表示取消默认班次)
if (shiftId) {
const shift = await prisma.shift.findFirst({ where: { id: shiftId, orgId: req.user!.orgId } })
if (!shift) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '班次不存在' } })
}
await prisma.employee.update({
where: { id: employeeId },
data: { defaultShiftId: shiftId || null },
})
res.json({ success: true })
} catch (err) { next(err) }
})
// 批量设置员工默认班次
router.post('/default-shift/batch', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { items } = req.body as { items: { employeeId: string; shiftId: string | null }[] }
if (!items || !Array.isArray(items)) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
let updated = 0
for (const item of items) {
if (item.shiftId) {
const shift = await prisma.shift.findFirst({ where: { id: item.shiftId, orgId: req.user!.orgId } })
if (!shift) continue
}
await prisma.employee.update({
where: { id: item.employeeId },
data: { defaultShiftId: item.shiftId || null },
})
updated++
}
res.json({ success: true, data: { updated } })
} catch (err) { next(err) }
})
// ========== 每日出勤 ==========
router.post('/manual-correct', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
+183 -4
View File
@@ -165,16 +165,34 @@ router.post('/overtime/sync-from-attendance', async (req: AuthRequest, res: Resp
}
// 按员工汇总加班工时,按日期类型分类
// 日期类型判断优先级:HolidayConfig > 周末判断
// HolidayConfig 中 HOLIDAY=法定节假日(3倍), WORKDAY=调休工作日(1.5倍)
const holidays = await prisma.holidayConfig.findMany({
where: { orgId, date: { gte: monthStart, lt: monthEnd } },
})
const holidayMap = new Map<string, string>() // dateStr -> type
for (const h of holidays) {
holidayMap.set(h.date.toISOString().slice(0, 10), h.type)
}
const empMap = new Map<string, { weekday: number; weekend: number; holiday: number }>()
for (const r of records) {
const day = new Date(r.date)
const dateStr = r.date.toISOString().slice(0, 10)
const day = new Date(dateStr + 'T00:00:00')
const dayOfWeek = day.getDay() // 0=周日, 6=周六
let type: 'weekday' | 'weekend' | 'holiday' = 'weekday'
if (dayOfWeek === 0 || dayOfWeek === 6) {
const holidayType = holidayMap.get(dateStr)
if (holidayType === 'HOLIDAY') {
// 法定节假日
type = 'holiday'
} else if (holidayType === 'WORKDAY') {
// 调休工作日(周末调休上班),按工作日算
type = 'weekday'
} else if (dayOfWeek === 0 || dayOfWeek === 6) {
// 普通周末
type = 'weekend'
}
// 简单判断法定节假日:这里使用周末判断,实际法定节假日需要额外配置
// 如果有 holidayHours 字段在 attendanceRecord 中,优先使用
if (!empMap.has(r.employeeId)) {
empMap.set(r.employeeId, { weekday: 0, weekend: 0, holiday: 0 })
@@ -473,6 +491,167 @@ router.post('/overtime/config', async (req: AuthRequest, res: Response, next: Ne
}
})
// ========== 节假日配置 ==========
// 获取节假日列表(支持按年份筛选)
router.get('/holidays', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const year = req.query.year as string | undefined
const where: any = { orgId }
if (year) {
const start = new Date(`${year}-01-01`)
const end = new Date(`${year}-12-31`)
end.setDate(end.getDate() + 1)
where.date = { gte: start, lt: end }
}
const holidays = await prisma.holidayConfig.findMany({
where,
orderBy: { date: 'asc' },
})
res.json({ success: true, data: holidays })
} catch (err) {
next(err)
}
})
// 批量保存节假日(覆盖该年配置)
router.post('/holidays/batch', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { year, items } = req.body as { year: string; items: { date: string; type: string; name?: string }[] }
if (!year || !/^\d{4}$/.test(year)) {
return res.status(400).json({ success: false, message: '请提供有效的年份' })
}
// 删除该年旧数据
const yearStart = new Date(`${year}-01-01`)
const yearEnd = new Date(`${year}-12-31`)
yearEnd.setDate(yearEnd.getDate() + 1)
await prisma.holidayConfig.deleteMany({
where: { orgId, date: { gte: yearStart, lt: yearEnd } },
})
// 批量插入新数据
if (items && items.length > 0) {
await prisma.holidayConfig.createMany({
data: items.map(item => ({
orgId,
date: new Date(item.date),
type: item.type,
name: item.name || null,
})),
})
}
res.json({ success: true, data: { count: items?.length || 0 } })
} catch (err) {
next(err)
}
})
// 预置法定节假日(按年份自动填充国务院发布的节假日)
router.post('/holidays/preset', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { year } = req.body as { year: string }
if (!year || !/^\d{4}$/.test(year)) {
return res.status(400).json({ success: false, message: '请提供有效的年份' })
}
// 2026年法定节假日配置(国务院发布)
const presetData: Record<string, { holidays: { date: string; name: string }[]; workdays: { date: string; name: string }[] }> = {
'2026': {
holidays: [
// 元旦
{ date: '2026-01-01', name: '元旦' },
// 春节
{ date: '2026-02-15', name: '春节' },
{ date: '2026-02-16', name: '春节' },
{ date: '2026-02-17', name: '春节' },
{ date: '2026-02-18', name: '春节' },
{ date: '2026-02-19', name: '春节' },
{ date: '2026-02-20', name: '春节' },
{ date: '2026-02-21', name: '春节' },
// 清明节
{ date: '2026-04-04', name: '清明节' },
{ date: '2026-04-05', name: '清明节' },
{ date: '2026-04-06', name: '清明节' },
// 劳动节
{ date: '2026-05-01', name: '劳动节' },
{ date: '2026-05-02', name: '劳动节' },
{ date: '2026-05-03', name: '劳动节' },
{ date: '2026-05-04', name: '劳动节' },
{ date: '2026-05-05', name: '劳动节' },
// 端午节
{ date: '2026-06-19', name: '端午节' },
{ date: '2026-06-20', name: '端午节' },
{ date: '2026-06-21', name: '端午节' },
// 中秋节
{ date: '2026-09-25', name: '中秋节' },
{ date: '2026-09-26', name: '中秋节' },
{ date: '2026-09-27', name: '中秋节' },
// 国庆节
{ date: '2026-10-01', name: '国庆节' },
{ date: '2026-10-02', name: '国庆节' },
{ date: '2026-10-03', name: '国庆节' },
{ date: '2026-10-04', name: '国庆节' },
{ date: '2026-10-05', name: '国庆节' },
{ date: '2026-10-06', name: '国庆节' },
{ date: '2026-10-07', name: '国庆节' },
],
workdays: [
// 春节调休
{ date: '2026-02-14', name: '春节调休' },
{ date: '2026-02-22', name: '春节调休' },
// 劳动节调休
{ date: '2026-04-26', name: '劳动节调休' },
// 国庆节调休
{ date: '2026-09-27', name: '国庆节调休' }, // 注意:9-27也是中秋,需确认
{ date: '2026-10-10', name: '国庆节调休' },
],
},
}
const preset = presetData[year]
if (!preset) {
return res.status(400).json({ success: false, message: `暂无 ${year} 年预置节假日数据,请手动配置` })
}
// 删除该年旧数据
const yearStart = new Date(`${year}-01-01`)
const yearEnd = new Date(`${year}-12-31`)
yearEnd.setDate(yearEnd.getDate() + 1)
await prisma.holidayConfig.deleteMany({
where: { orgId, date: { gte: yearStart, lt: yearEnd } },
})
// 插入法定节假日
const holidayData = preset.holidays.map(h => ({
orgId, date: new Date(h.date), type: 'HOLIDAY', name: h.name,
}))
// 插入调休工作日
const workdayData = preset.workdays.map(w => ({
orgId, date: new Date(w.date), type: 'WORKDAY', name: w.name,
}))
// 去重(同一日期可能既是中秋又是调休)
const allData = [...holidayData, ...workdayData]
const seen = new Set<string>()
const deduped = allData.filter(d => {
const key = d.date.toISOString().slice(0, 10)
if (seen.has(key)) return false
seen.add(key)
return true
})
await prisma.holidayConfig.createMany({ data: deduped })
res.json({ success: true, data: { holidays: preset.holidays.length, workdays: deduped.length - preset.holidays.length } })
} catch (err) {
next(err)
}
})
// ========== 批量导入加班工时 ==========
const batchOvertimeSchema = z.array(
+66 -2
View File
@@ -257,7 +257,24 @@ router.get('/contract', portalAuth, async (req: any, res, next) => {
if (!contract) {
return res.json({ success: true, data: null })
}
res.json({ success: true, data: contract })
// 补充员工工资结构(合同中 baseSalary/performanceSalary 为 0 时作为 fallback
const employee = await prisma.employee.findFirst({
where: { id: req.employee.id },
select: { monthlySalary: true, baseSalary: true, performanceSalary: true },
})
const empBase = employee?.baseSalary ? Number(decrypt(employee.baseSalary)) || 0 : 0
const empPerf = employee?.performanceSalary ? Number(decrypt(employee.performanceSalary)) || 0 : 0
const empMonthly = employee?.monthlySalary ? Number(decrypt(employee.monthlySalary)) || 0 : 0
res.json({
success: true,
data: {
...contract,
// 合同中工资为 0 时用员工档案的工资补齐
baseSalary: contract.baseSalary || empBase,
performanceSalary: contract.performanceSalary || empPerf,
monthlySalary: empMonthly,
},
})
} catch (err) {
next(err)
}
@@ -730,7 +747,54 @@ router.get('/attendance', portalAuth, async (req: any, res, next) => {
},
orderBy: { date: 'asc' },
})
res.json({ success: true, data: { published: true, records, title: publish.title } })
// 查询节假日配置,判断每日日期类型和加班费率
const holidays = await prisma.holidayConfig.findMany({
where: { orgId: req.employee.orgId, date: { gte: startDate, lt: endDate } },
})
const holidayMap = new Map<string, string>() // dateStr -> type
for (const h of holidays) {
holidayMap.set(h.date.toISOString().slice(0, 10), h.type)
}
// 获取加班费配置
const otConfig = await prisma.overtimeConfig.findUnique({ where: { orgId: req.employee.orgId } })
const rates = {
weekday: otConfig?.weekdayRate ?? 1.5,
weekend: otConfig?.weekendRate ?? 2.0,
holiday: otConfig?.holidayRate ?? 3.0,
}
// 为每条记录附加日期类型和加班费率
const enrichedRecords = records.map(r => {
const dateStr = r.date.toISOString().slice(0, 10)
const day = new Date(dateStr + 'T00:00:00')
const dayOfWeek = day.getDay()
const holidayType = holidayMap.get(dateStr)
let dateType: 'weekday' | 'weekend' | 'holiday' = 'weekday'
let overtimeRate = rates.weekday
if (holidayType === 'HOLIDAY') {
dateType = 'holiday'
overtimeRate = rates.holiday
} else if (holidayType === 'WORKDAY') {
dateType = 'weekday'
overtimeRate = rates.weekday
} else if (dayOfWeek === 0 || dayOfWeek === 6) {
dateType = 'weekend'
overtimeRate = rates.weekend
}
return {
...r,
dateType,
overtimeRate,
hasOvertime: (r.overtimeHours || 0) > 0,
}
})
res.json({ success: true, data: { published: true, records: enrichedRecords, title: publish.title, overtimeRates: rates } })
} catch (err) {
next(err)
}
+2 -2
View File
@@ -335,8 +335,8 @@ router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) =
performanceRecords: { orderBy: { period: 'desc' } },
terminations: { orderBy: { createdAt: 'desc' } },
attachments: true,
socialInsRecords: { orderBy: { startMonth: 'desc' } },
housingFundRecords: { orderBy: { startMonth: 'desc' } },
socialInsRecords: { orderBy: { startMonth: 'desc' }, include: { account: { select: { id: true, name: true, city: true } } } },
housingFundRecords: { orderBy: { startMonth: 'desc' }, include: { account: { select: { id: true, name: true, city: true } } } },
salaryChanges: { orderBy: { effectiveDate: 'desc' } },
departmentRecords: { orderBy: { effectiveMonth: 'desc' } },
},
+2
View File
@@ -1022,6 +1022,8 @@ router.post('/housing-calculate', async (req: AuthRequest, res: Response, next:
configVersion: config.effectiveFrom,
housingOrg,
housingEmp,
orgRate: config.housingOrg,
empRate: config.housingEmp,
total: housingOrg + housingEmp,
},
})
+62 -3
View File
@@ -1,4 +1,5 @@
import prisma from '../lib/prisma'
import { decrypt } from '../lib/crypto'
/**
* 考勤确认服务
@@ -207,7 +208,8 @@ export async function getShiftAssignments(orgId: string, date: string) {
const nextDay = new Date(day)
nextDay.setDate(nextDay.getDate() + 1)
return prisma.shiftAssignment.findMany({
// 查询当天排班记录
const assignments = await prisma.shiftAssignment.findMany({
where: { orgId, date: { gte: day, lt: nextDay } },
include: {
employee: { select: { id: true, name: true, department: true } },
@@ -215,6 +217,57 @@ export async function getShiftAssignments(orgId: string, date: string) {
},
orderBy: { employee: { name: 'asc' } },
})
// 查询所有在职员工的默认班次(长期排班)
const employees = await prisma.employee.findMany({
where: { orgId, status: 'ACTIVE' },
select: { id: true, name: true, department: true, idCardNumber: true, defaultShiftId: true, defaultShift: true },
orderBy: { name: 'asc' },
})
// 合并:有当天排班记录的用排班记录,没有的 fallback 到默认班次
// 如果当天排班和默认班次相同,也标记为 isDefault(视为长期排班)
const assignmentMap = new Map(assignments.map(a => [a.employeeId, a]))
const merged = employees.map(emp => {
// 解密身份证号
let idCardNumber: string | null = null
try { idCardNumber = emp.idCardNumber ? (emp.idCardNumber.includes(':') ? decrypt(emp.idCardNumber) : emp.idCardNumber) : null } catch { idCardNumber = null }
const explicit = assignmentMap.get(emp.id)
if (explicit) {
// 当天排班和默认班次相同,视为长期排班
const sameAsDefault = emp.defaultShiftId && explicit.shiftId === emp.defaultShiftId
return {
...explicit,
isDefault: !!sameAsDefault,
employee: { ...explicit.employee, idCardNumber },
}
}
// 没有当天排班,使用默认班次(长期排班)
if (emp.defaultShift) {
return {
id: `default-${emp.id}`,
employeeId: emp.id,
shiftId: emp.defaultShiftId,
shift: emp.defaultShift,
date: day,
isDefault: true,
employee: { id: emp.id, name: emp.name, department: emp.department, idCardNumber },
}
}
// 无排班
return {
id: `none-${emp.id}`,
employeeId: emp.id,
shiftId: null,
shift: null,
date: day,
isDefault: false,
employee: { id: emp.id, name: emp.name, department: emp.department, idCardNumber },
}
})
return merged
}
export async function batchAssignShifts(orgId: string, userId: string, items: Array<{
@@ -334,7 +387,7 @@ export async function getDailyAttendance(orgId: string, date: string) {
}),
prisma.employee.findMany({
where: { orgId, status: 'ACTIVE' },
select: { id: true, name: true, department: true },
select: { id: true, name: true, department: true, idCardNumber: true, defaultShiftId: true, defaultShift: true },
orderBy: { name: 'asc' },
}),
])
@@ -344,11 +397,17 @@ export async function getDailyAttendance(orgId: string, date: string) {
return employees.map(emp => {
const record = recordMap.get(emp.id)
const shift = shiftMap.get(emp.id)
// 优先使用当天排班,没有则 fallback 到默认班次
const shift = shiftMap.get(emp.id) || emp.defaultShift
// 解密身份证号
let idCardNumber: string | null = null
try { idCardNumber = emp.idCardNumber ? (emp.idCardNumber.includes(':') ? decrypt(emp.idCardNumber) : emp.idCardNumber) : null } catch { idCardNumber = null }
return {
employeeId: emp.id,
name: emp.name,
department: emp.department,
idCardNumber,
shift: shift ? { name: shift.name, startTime: shift.startTime, endTime: shift.endTime, color: shift.color } : null,
checkInTime: record?.checkInTime || null,
checkOutTime: record?.checkOutTime || null,
+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,