feat: AIHR 智能人力资源管理系统初始提交
- 员工花名册管理(加密存储、导入导出) - 薪酬管理(发薪批次、薪酬模版、加班费计算、工资条) - 社保公积金(多城市配置、版本管理、基数调整) - 解聘管理(6步流程、证据链、工作交接) - AI 助手(合同审查、风险预测、RAG 知识库) - Dashboard 仪表盘 - 设置与通知
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import prisma from '../lib/prisma'
|
||||
import {
|
||||
createEmployeeSchema,
|
||||
updateEmployeeSchema,
|
||||
batchRenewSchema,
|
||||
addContractSchema,
|
||||
} from '../schemas/contract.schema'
|
||||
import {
|
||||
getEmployees,
|
||||
getEmployeeDetail,
|
||||
createEmployee,
|
||||
rehireEmployee,
|
||||
updateEmployee,
|
||||
deleteEmployee,
|
||||
batchRenew,
|
||||
addContract,
|
||||
} from '../services/contract.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await getEmployees(req.user!.orgId, {
|
||||
page: parseInt(req.query.page as string) || 1,
|
||||
pageSize: parseInt(req.query.pageSize as string) || 20,
|
||||
search: req.query.search as string,
|
||||
department: req.query.department as string,
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employee = await getEmployeeDetail(req.user!.orgId, req.params.id)
|
||||
res.json({ success: true, data: employee })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = createEmployeeSchema.parse(req.body)
|
||||
const result = await createEmployee(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'CREATE', 'EMPLOYEE', result.id, { name: data.name })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.put('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = updateEmployeeSchema.parse(req.body)
|
||||
const result = await updateEmployee(req.user!.orgId, req.params.id, data)
|
||||
await auditLog(req, 'UPDATE', 'EMPLOYEE', req.params.id, data)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:id/rehire', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await rehireEmployee(req.user!.orgId, req.user!.id, req.params.id, req.body)
|
||||
await auditLog(req, 'REHIRE', 'EMPLOYEE', req.params.id, { hireDate: req.body.hireDate })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT') {
|
||||
return res.status(409).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
if (err?.code === 'VALIDATION_ERROR') {
|
||||
return res.status(400).json({ success: false, error: { code: err.code, message: err.message } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await deleteEmployee(req.user!.orgId, req.params.id)
|
||||
await auditLog(req, 'DELETE', 'EMPLOYEE', req.params.id)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 批量续签合规预检
|
||||
router.post('/contracts/preview-renew', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { contractIds } = req.body as { contractIds: string[] }
|
||||
if (!contractIds || !Array.isArray(contractIds) || contractIds.length === 0) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 contractIds' } })
|
||||
}
|
||||
|
||||
const contracts = await prisma.laborContract.findMany({
|
||||
where: { id: { in: contractIds }, orgId: req.user!.orgId },
|
||||
include: { employee: true },
|
||||
orderBy: { startDate: 'asc' },
|
||||
})
|
||||
|
||||
if (contracts.length === 0) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '未找到符合条件的合同' } })
|
||||
}
|
||||
|
||||
// 合规检查:按员工分组,检查历史固定期合同次数
|
||||
const results = []
|
||||
for (const contract of contracts) {
|
||||
const employee = contract.employee
|
||||
|
||||
// 查找该员工所有历史固定期合同(按时间正序,用于判断续签次数)
|
||||
const allFixedContracts = await prisma.laborContract.findMany({
|
||||
where: {
|
||||
employeeId: contract.employeeId,
|
||||
orgId: req.user!.orgId,
|
||||
contractType: 'FIXED',
|
||||
},
|
||||
orderBy: { startDate: 'asc' },
|
||||
})
|
||||
|
||||
// 当前合同是第几次固定期(从1开始计数)
|
||||
const currentIndex = allFixedContracts.findIndex((c) => c.id === contract.id)
|
||||
const renewalCount = currentIndex + 1
|
||||
|
||||
// 判断是否应签无固定期限:
|
||||
// 1. 已连续签订2次以上固定期限合同(第3次应签无固定期限)
|
||||
// 2. 员工连续工作满10年
|
||||
const shouldBeUnfixed = renewalCount >= 2
|
||||
const yearsSinceHire = (Date.now() - new Date(employee.hireDate).getTime()) / (365.25 * 24 * 60 * 60 * 1000)
|
||||
const shouldBeUnfixedByTenure = yearsSinceHire >= 10
|
||||
|
||||
let warning: string | null = null
|
||||
let suggestion: string | null = null
|
||||
|
||||
if (shouldBeUnfixed || shouldBeUnfixedByTenure) {
|
||||
warning = shouldBeUnfixed
|
||||
? `该员工已有 ${renewalCount} 次固定期限合同续签记录(《劳动合同法》第14条),第三次续签应订立无固定期限劳动合同`
|
||||
: `该员工在本公司连续工作 ${Math.floor(yearsSinceHire)} 年(《劳动合同法》第14条),应订立无固定期限劳动合同`
|
||||
suggestion = '建议与员工协商订立无固定期限劳动合同,以规避法律风险'
|
||||
} else {
|
||||
suggestion = `可续签固定期限(当前为第 ${renewalCount} 次续签)`
|
||||
}
|
||||
|
||||
results.push({
|
||||
contractId: contract.id,
|
||||
employeeId: contract.employeeId,
|
||||
employeeName: employee.name,
|
||||
department: employee.department,
|
||||
currentContractType: contract.contractType,
|
||||
renewalCount,
|
||||
yearsSinceHire: Math.floor(yearsSinceHire * 10) / 10,
|
||||
warning,
|
||||
suggestion,
|
||||
canRenewFixed: !warning,
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: results.length,
|
||||
warnings: results.filter((r) => r.warning).length,
|
||||
results,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/contracts/batch-renew', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = batchRenewSchema.parse(req.body)
|
||||
const result = await batchRenew(req.user!.orgId, req.user!.id, data.contractIds, data.years)
|
||||
await auditLog(req, 'BATCH_RENEW', 'CONTRACT', undefined, { count: data.contractIds.length })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/contracts', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = addContractSchema.parse(req.body)
|
||||
const result = await addContract(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'ADD_CONTRACT', 'CONTRACT', result.id, { employeeId: data.employeeId })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user