init: AI HR Compliance Assistant
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { chat, reviewContract, matchCase, predictRisks } from '../services/ai.service'
|
||||
import prisma from '../lib/prisma'
|
||||
|
||||
const router = Router()
|
||||
|
||||
async function buildOrgContext(orgId: string): Promise<string> {
|
||||
const [employees, risks] = await Promise.all([
|
||||
prisma.employee.findMany({
|
||||
where: { orgId, status: 'ACTIVE' },
|
||||
include: { contracts: { orderBy: { createdAt: 'desc' }, take: 1 } },
|
||||
}),
|
||||
prisma.riskItem.findMany({
|
||||
where: { orgId, status: 'PENDING' },
|
||||
include: { employee: true },
|
||||
}),
|
||||
])
|
||||
|
||||
const empSummary = employees.map((e) => {
|
||||
const contract = e.contracts[0]
|
||||
return `- ${e.name}(${e.department}),入职${e.hireDate.toISOString().slice(0, 10)},${contract ? `合同类型:${contract.contractType}` : '未签合同'}`
|
||||
}).join('\n')
|
||||
|
||||
const riskSummary = risks.map((r) => `- ${r.title}(${r.level})`).join('\n')
|
||||
|
||||
return `员工列表(${employees.length}人):
|
||||
${empSummary}
|
||||
|
||||
当前风险项(${risks.length}项):
|
||||
${riskSummary}`
|
||||
}
|
||||
|
||||
router.post('/chat', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { messages } = req.body as { messages: { role: 'user' | 'assistant'; content: string }[] }
|
||||
if (!messages || !Array.isArray(messages)) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 messages 参数' } })
|
||||
}
|
||||
const orgContext = await buildOrgContext(req.user!.orgId)
|
||||
const reply = await chat(messages, orgContext)
|
||||
res.json({ success: true, data: { reply } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/review', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { contractText } = req.body as { contractText: string }
|
||||
if (!contractText) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少合同文本' } })
|
||||
}
|
||||
const result = await reviewContract(contractText)
|
||||
res.json({ success: true, data: { result } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/match-case', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { scenario } = req.body as { scenario: string }
|
||||
if (!scenario) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少争议情形描述' } })
|
||||
}
|
||||
const result = await matchCase(scenario)
|
||||
res.json({ success: true, data: { result } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/predict', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const orgContext = await buildOrgContext(req.user!.orgId)
|
||||
const result = await predictRisks(orgContext)
|
||||
res.json({ success: true, data: { result } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
// 获取员工附件列表
|
||||
router.get('/:employeeId', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const attachments = await prisma.employeeAttachment.findMany({
|
||||
where: { orgId: req.user!.orgId, employeeId: req.params.employeeId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: attachments })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加附件记录(文件URL由前端上传后传入)
|
||||
const attachmentSchema = z.object({
|
||||
employeeId: z.string().min(1),
|
||||
fileName: z.string().min(1),
|
||||
fileType: z.enum(['ID_CARD', 'BANK_CARD', 'CONTRACT_SCAN', 'EDUCATION', 'OTHER']),
|
||||
fileUrl: z.string().min(1),
|
||||
fileSize: z.number().int().default(0),
|
||||
})
|
||||
|
||||
router.post('/', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = attachmentSchema.parse(req.body)
|
||||
const attachment = await prisma.employeeAttachment.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
...data,
|
||||
uploadedBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: attachment })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除附件
|
||||
router.delete('/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const attachment = await prisma.employeeAttachment.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!attachment) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '附件不存在' } })
|
||||
}
|
||||
await prisma.employeeAttachment.delete({ where: { id: attachment.id } })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Router } from 'express'
|
||||
import { registerSchema, loginSchema, refreshSchema, resetPasswordSchema } from '../schemas/auth.schema'
|
||||
import { register, login, refresh, resetPassword } from '../services/auth.service'
|
||||
import { authLimiter, loginLimiter } from '../middleware/rateLimit'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/register', authLimiter, async (req, res, next) => {
|
||||
try {
|
||||
const data = registerSchema.parse(req.body)
|
||||
const result = await register(data.orgName, data.phone, data.password)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/login', loginLimiter, async (req, res, next) => {
|
||||
try {
|
||||
const data = loginSchema.parse(req.body)
|
||||
const result = await login(data.phone, data.password)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/refresh', async (req, res, next) => {
|
||||
try {
|
||||
const data = refreshSchema.parse(req.body)
|
||||
const result = await refresh(data.refreshToken)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/reset-password', authLimiter, async (req, res, next) => {
|
||||
try {
|
||||
const data = resetPasswordSchema.parse(req.body)
|
||||
const result = await resetPassword(data.phone, data.newPassword)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { getDashboardData } from '../services/risk.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = await getDashboardData(req.user!.orgId)
|
||||
res.json({ success: true, data })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 标记待办为已完成
|
||||
router.patch('/todos/:id/resolve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const item = await prisma.riskItem.updateMany({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId, status: 'PENDING' },
|
||||
data: { status: 'RESOLVED', resolvedAt: new Date(), resolvedBy: req.user!.id },
|
||||
})
|
||||
if (item.count === 0) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '待办不存在或已处理' } })
|
||||
}
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 忽略待办
|
||||
router.patch('/todos/:id/ignore', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const item = await prisma.riskItem.updateMany({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId, status: 'PENDING' },
|
||||
data: { status: 'IGNORED', resolvedAt: new Date(), resolvedBy: req.user!.id },
|
||||
})
|
||||
if (item.count === 0) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '待办不存在或已处理' } })
|
||||
}
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import {
|
||||
createEmployeeSchema,
|
||||
updateEmployeeSchema,
|
||||
batchRenewSchema,
|
||||
addContractSchema,
|
||||
} from '../schemas/contract.schema'
|
||||
import {
|
||||
getEmployees,
|
||||
getEmployeeDetail,
|
||||
createEmployee,
|
||||
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.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/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
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
// 获取通知设置
|
||||
router.get('/settings', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let setting = await prisma.notificationSetting.findUnique({
|
||||
where: { orgId: req.user!.orgId },
|
||||
})
|
||||
if (!setting) {
|
||||
setting = await prisma.notificationSetting.create({
|
||||
data: { orgId: req.user!.orgId },
|
||||
})
|
||||
}
|
||||
res.json({ success: true, data: setting })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新通知设置
|
||||
const settingSchema = z.object({
|
||||
contractExpiry: z.boolean().optional(),
|
||||
expiryDays: z.number().int().min(1).max(365).optional(),
|
||||
contractUnsigned: z.boolean().optional(),
|
||||
overtimeAlert: z.boolean().optional(),
|
||||
payslipReady: z.boolean().optional(),
|
||||
payrollDay: z.number().int().min(1).max(28).optional(),
|
||||
socialInsDay: z.number().int().min(1).max(28).optional(),
|
||||
housingFundDay: z.number().int().min(1).max(28).optional(),
|
||||
taxDay: z.number().int().min(1).max(28).optional(),
|
||||
wechatWebhook: z.string().url().nullable().optional(),
|
||||
emailNotify: z.boolean().optional(),
|
||||
email: z.string().email().nullable().optional(),
|
||||
})
|
||||
|
||||
router.put('/settings', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = settingSchema.parse(req.body)
|
||||
const setting = await prisma.notificationSetting.upsert({
|
||||
where: { orgId: req.user!.orgId },
|
||||
update: data,
|
||||
create: { orgId: req.user!.orgId, ...data },
|
||||
})
|
||||
res.json({ success: true, data: setting })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取通知列表
|
||||
router.get('/logs', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const [logs, total] = await Promise.all([
|
||||
prisma.notificationLog.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.notificationLog.count({ where: { orgId: req.user!.orgId } }),
|
||||
])
|
||||
res.json({ success: true, data: { items: logs, total, page, pageSize, totalPages: Math.ceil(total / pageSize) } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 手动触发合同到期检查
|
||||
router.post('/check-contracts', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const setting = await prisma.notificationSetting.findUnique({
|
||||
where: { orgId: req.user!.orgId },
|
||||
})
|
||||
const expiryDays = setting?.expiryDays || 30
|
||||
const now = new Date()
|
||||
const threshold = new Date(now.getTime() + expiryDays * 24 * 60 * 60 * 1000)
|
||||
|
||||
const contracts = await prisma.laborContract.findMany({
|
||||
where: {
|
||||
orgId: req.user!.orgId,
|
||||
endDate: { lte: threshold, gte: now },
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
})
|
||||
|
||||
const logs: any[] = []
|
||||
for (const contract of contracts) {
|
||||
const daysLeft = Math.ceil((contract.endDate!.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const title = `${contract.employee.name}的合同将在${daysLeft}天后到期`
|
||||
const content = `员工 ${contract.employee.name}(${contract.employee.department})的合同将于 ${contract.endDate!.toISOString().slice(0, 10)} 到期,请及时处理续签或终止事宜。`
|
||||
|
||||
const log = await prisma.notificationLog.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
type: 'CONTRACT_EXPIRY',
|
||||
title,
|
||||
content,
|
||||
channel: 'IN_APP',
|
||||
employeeId: contract.employeeId,
|
||||
},
|
||||
})
|
||||
logs.push(log)
|
||||
|
||||
if (setting?.wechatWebhook) {
|
||||
try {
|
||||
await fetch(setting.wechatWebhook, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
msgtype: 'text',
|
||||
text: { content: `【合同到期提醒】${title}\n${content}` },
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
// webhook 发送失败不阻断流程
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { checked: contracts.length, notified: logs.length } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,338 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
// ========== 加班费记录 ==========
|
||||
|
||||
const overtimeSchema = z.object({
|
||||
employeeId: z.string().min(1),
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
monthlyWage: z.number().positive(),
|
||||
weekdayHours: z.number().min(0).default(0),
|
||||
weekendHours: z.number().min(0).default(0),
|
||||
holidayHours: z.number().min(0).default(0),
|
||||
})
|
||||
|
||||
// 获取加班费记录列表
|
||||
router.get('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { employeeId, month } = req.query
|
||||
const records = await prisma.overtimeRecord.findMany({
|
||||
where: {
|
||||
orgId: req.user!.orgId,
|
||||
...(employeeId ? { employeeId: String(employeeId) } : {}),
|
||||
...(month ? { month: String(month) } : {}),
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 保存加班费记录
|
||||
router.post('/overtime', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = overtimeSchema.parse(req.body)
|
||||
const hourlyWage = data.monthlyWage / 21.75 / 8
|
||||
const weekdayPay = hourlyWage * 1.5 * data.weekdayHours
|
||||
const weekendPay = hourlyWage * 2.0 * data.weekendHours
|
||||
const holidayPay = hourlyWage * 3.0 * data.holidayHours
|
||||
const totalPay = weekdayPay + weekendPay + holidayPay
|
||||
|
||||
const record = await prisma.overtimeRecord.upsert({
|
||||
where: {
|
||||
employeeId_month: { employeeId: data.employeeId, month: data.month },
|
||||
},
|
||||
update: {
|
||||
weekdayHours: data.weekdayHours,
|
||||
weekendHours: data.weekendHours,
|
||||
holidayHours: data.holidayHours,
|
||||
weekdayPay,
|
||||
weekendPay,
|
||||
holidayPay,
|
||||
totalPay,
|
||||
},
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: data.employeeId,
|
||||
month: data.month,
|
||||
weekdayHours: data.weekdayHours,
|
||||
weekendHours: data.weekendHours,
|
||||
holidayHours: data.holidayHours,
|
||||
weekdayPay,
|
||||
weekendPay,
|
||||
holidayPay,
|
||||
totalPay,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 工资条管理 ==========
|
||||
|
||||
const payslipSchema = z.object({
|
||||
employeeId: z.string().min(1),
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
baseSalary: z.number().min(0).default(0),
|
||||
overtimePay: z.number().min(0).default(0),
|
||||
weekdayOvertimePay: z.number().min(0).default(0),
|
||||
weekendOvertimePay: z.number().min(0).default(0),
|
||||
holidayOvertimePay: z.number().min(0).default(0),
|
||||
allowance: z.number().min(0).default(0),
|
||||
deduction: z.number().min(0).default(0),
|
||||
})
|
||||
|
||||
// 获取工资条列表
|
||||
router.get('/payslip', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { month, employeeId } = req.query
|
||||
const payslips = await prisma.payslip.findMany({
|
||||
where: {
|
||||
orgId: req.user!.orgId,
|
||||
...(month ? { month: String(month) } : {}),
|
||||
...(employeeId ? { employeeId: String(employeeId) } : {}),
|
||||
},
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: [{ month: 'desc' }, { employee: { name: 'asc' } }],
|
||||
})
|
||||
res.json({ success: true, data: payslips })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建/更新工资条
|
||||
router.post('/payslip', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = payslipSchema.parse(req.body)
|
||||
const totalPay = data.baseSalary + data.overtimePay + data.allowance - data.deduction
|
||||
|
||||
const payslip = await prisma.payslip.upsert({
|
||||
where: {
|
||||
employeeId_month: { employeeId: data.employeeId, month: data.month },
|
||||
},
|
||||
update: {
|
||||
baseSalary: data.baseSalary,
|
||||
overtimePay: data.overtimePay,
|
||||
weekdayOvertimePay: data.weekdayOvertimePay,
|
||||
weekendOvertimePay: data.weekendOvertimePay,
|
||||
holidayOvertimePay: data.holidayOvertimePay,
|
||||
allowance: data.allowance,
|
||||
deduction: data.deduction,
|
||||
totalPay,
|
||||
},
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: data.employeeId,
|
||||
month: data.month,
|
||||
baseSalary: data.baseSalary,
|
||||
overtimePay: data.overtimePay,
|
||||
weekdayOvertimePay: data.weekdayOvertimePay,
|
||||
weekendOvertimePay: data.weekendOvertimePay,
|
||||
holidayOvertimePay: data.holidayOvertimePay,
|
||||
allowance: data.allowance,
|
||||
deduction: data.deduction,
|
||||
totalPay,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: payslip })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 从加班费记录自动生成工资条
|
||||
router.post('/payslip/generate', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { month, employeeId, baseSalary, allowance, deduction } = req.body as {
|
||||
month: string
|
||||
employeeId: string
|
||||
baseSalary: number
|
||||
allowance?: number
|
||||
deduction?: number
|
||||
}
|
||||
|
||||
const overtime = await prisma.overtimeRecord.findUnique({
|
||||
where: { employeeId_month: { employeeId, month } },
|
||||
})
|
||||
|
||||
const overtimePay = overtime?.totalPay || 0
|
||||
const totalPay = baseSalary + overtimePay + (allowance || 0) - (deduction || 0)
|
||||
|
||||
const payslip = await prisma.payslip.upsert({
|
||||
where: { employeeId_month: { employeeId, month } },
|
||||
update: {
|
||||
baseSalary,
|
||||
overtimePay,
|
||||
weekdayOvertimePay: overtime?.weekdayPay || 0,
|
||||
weekendOvertimePay: overtime?.weekendPay || 0,
|
||||
holidayOvertimePay: overtime?.holidayPay || 0,
|
||||
allowance: allowance || 0,
|
||||
deduction: deduction || 0,
|
||||
totalPay,
|
||||
},
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId,
|
||||
month,
|
||||
baseSalary,
|
||||
overtimePay,
|
||||
weekdayOvertimePay: overtime?.weekdayPay || 0,
|
||||
weekendOvertimePay: overtime?.weekendPay || 0,
|
||||
holidayOvertimePay: overtime?.holidayPay || 0,
|
||||
allowance: allowance || 0,
|
||||
deduction: deduction || 0,
|
||||
totalPay,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: payslip })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除工资条
|
||||
router.delete('/payslip/:id', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await prisma.payslip.delete({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 批量生成工资条 ==========
|
||||
|
||||
const batchGenerateSchema = z.object({
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
allowances: z.record(z.string(), z.number().default(0)).optional(),
|
||||
deductions: z.record(z.string(), z.number().default(0)).optional(),
|
||||
})
|
||||
|
||||
// 批量生成全员工资条
|
||||
router.post('/payslip/batch-generate', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { month, allowances = {}, deductions = {} } = batchGenerateSchema.parse(req.body)
|
||||
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1 },
|
||||
},
|
||||
})
|
||||
|
||||
const results: any[] = []
|
||||
for (const emp of employees) {
|
||||
const overtime = await prisma.overtimeRecord.findUnique({
|
||||
where: { employeeId_month: { employeeId: emp.id, month } },
|
||||
})
|
||||
|
||||
const overtimePay = overtime?.totalPay || 0
|
||||
const allowance = allowances[emp.id] || 0
|
||||
const deduction = deductions[emp.id] || 0
|
||||
|
||||
let baseSalary = 0
|
||||
if (emp.contracts[0]?.probationSalary && new Date(emp.contracts[0].startDate) > new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)) {
|
||||
baseSalary = emp.contracts[0].probationSalary
|
||||
} else if (emp.monthlySalary) {
|
||||
try {
|
||||
baseSalary = Number(decrypt(emp.monthlySalary)) || 0
|
||||
} catch {
|
||||
baseSalary = Number(emp.monthlySalary) || 0
|
||||
}
|
||||
}
|
||||
|
||||
const totalPay = baseSalary + overtimePay + allowance - deduction
|
||||
|
||||
const payslip = await prisma.payslip.upsert({
|
||||
where: { employeeId_month: { employeeId: emp.id, month } },
|
||||
update: { baseSalary, overtimePay, allowance, deduction, totalPay },
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: emp.id,
|
||||
month,
|
||||
baseSalary,
|
||||
overtimePay,
|
||||
weekdayOvertimePay: overtime?.weekdayPay || 0,
|
||||
weekendOvertimePay: overtime?.weekendPay || 0,
|
||||
holidayOvertimePay: overtime?.holidayPay || 0,
|
||||
allowance,
|
||||
deduction,
|
||||
totalPay,
|
||||
},
|
||||
})
|
||||
results.push(payslip)
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { generated: results.length, payslips: results } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 批量导入加班数据 ==========
|
||||
|
||||
const batchOvertimeSchema = z.array(
|
||||
z.object({
|
||||
employeeId: z.string().min(1),
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
monthlyWage: z.number().positive(),
|
||||
weekdayHours: z.number().min(0).default(0),
|
||||
weekendHours: z.number().min(0).default(0),
|
||||
holidayHours: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
|
||||
router.post('/overtime/batch', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const items = batchOvertimeSchema.parse(req.body)
|
||||
const results: any[] = []
|
||||
|
||||
for (const data of items) {
|
||||
const hourlyWage = data.monthlyWage / 21.75 / 8
|
||||
const weekdayPay = hourlyWage * 1.5 * data.weekdayHours
|
||||
const weekendPay = hourlyWage * 2.0 * data.weekendHours
|
||||
const holidayPay = hourlyWage * 3.0 * data.holidayHours
|
||||
const totalPay = weekdayPay + weekendPay + holidayPay
|
||||
|
||||
const record = await prisma.overtimeRecord.upsert({
|
||||
where: { employeeId_month: { employeeId: data.employeeId, month: data.month } },
|
||||
update: {
|
||||
weekdayHours: data.weekdayHours,
|
||||
weekendHours: data.weekendHours,
|
||||
holidayHours: data.holidayHours,
|
||||
weekdayPay, weekendPay, holidayPay, totalPay,
|
||||
},
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: data.employeeId,
|
||||
month: data.month,
|
||||
weekdayHours: data.weekdayHours,
|
||||
weekendHours: data.weekendHours,
|
||||
holidayHours: data.holidayHours,
|
||||
weekdayPay, weekendPay, holidayPay, totalPay,
|
||||
},
|
||||
})
|
||||
results.push(record)
|
||||
}
|
||||
|
||||
res.json({ success: true, data: { imported: results.length } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,248 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import prisma from '../lib/prisma'
|
||||
import { signAccessToken, verifyAccessToken } from '../lib/jwt'
|
||||
import { portalLoginSchema, portalSendCodeSchema, portalVerifyCodeSchema, onboardingSchema, contractConfirmSchema } from '../schemas/portal.schema'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 验证码临时存储(生产环境应使用 Redis)
|
||||
const codeStore = new Map<string, { code: string; expiresAt: number }>()
|
||||
|
||||
// 员工端认证中间件
|
||||
function portalAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: '未登录' } })
|
||||
}
|
||||
const token = authHeader.substring(7)
|
||||
try {
|
||||
const payload = verifyAccessToken(token)
|
||||
if (!payload || payload.role !== 'EMPLOYEE') {
|
||||
return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: '无效的员工端 Token' } })
|
||||
}
|
||||
;(req as any).employee = { id: payload.id, orgId: payload.orgId }
|
||||
next()
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Token 无效或已过期' } })
|
||||
}
|
||||
}
|
||||
|
||||
// 密码登录
|
||||
router.post('/login', async (req, res, next) => {
|
||||
try {
|
||||
const data = portalLoginSchema.parse(req.body)
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { phone: data.phone, status: 'ACTIVE' },
|
||||
})
|
||||
if (!employee || !employee.passwordHash) {
|
||||
return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '手机号或密码错误' } })
|
||||
}
|
||||
const valid = await bcrypt.compare(data.password, employee.passwordHash)
|
||||
if (!valid) {
|
||||
return res.status(400).json({ success: false, error: { code: 'AUTH_FAILED', message: '手机号或密码错误' } })
|
||||
}
|
||||
const token = signAccessToken({ id: employee.id, orgId: employee.orgId, role: 'EMPLOYEE' })
|
||||
res.json({ success: true, data: { token, employee: { id: employee.id, name: employee.name, department: employee.department } } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 发送验证码(页面内显示)
|
||||
router.post('/send-code', async (req, res, next) => {
|
||||
try {
|
||||
const data = portalSendCodeSchema.parse(req.body)
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { phone: data.phone, status: 'ACTIVE' },
|
||||
})
|
||||
if (!employee) {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '该手机号未在系统中登记' } })
|
||||
}
|
||||
const code = Math.random().toString().slice(2, 8)
|
||||
codeStore.set(data.phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 })
|
||||
res.json({ success: true, data: { code, message: '验证码已生成(开发阶段直接返回,生产环境将发送短信)' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 验证码登录
|
||||
router.post('/verify-code', async (req, res, next) => {
|
||||
try {
|
||||
const data = portalVerifyCodeSchema.parse(req.body)
|
||||
const stored = codeStore.get(data.phone)
|
||||
if (!stored || stored.expiresAt < Date.now()) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_EXPIRED', message: '验证码已过期,请重新获取' } })
|
||||
}
|
||||
if (stored.code !== data.code) {
|
||||
return res.status(400).json({ success: false, error: { code: 'CODE_WRONG', message: '验证码错误' } })
|
||||
}
|
||||
codeStore.delete(data.phone)
|
||||
const employee = await prisma.employee.findFirst({ where: { phone: data.phone, status: 'ACTIVE' } })
|
||||
if (!employee) {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
}
|
||||
const token = signAccessToken({ id: employee.id, orgId: employee.orgId, role: 'EMPLOYEE' })
|
||||
res.json({ success: true, data: { token, employee: { id: employee.id, name: employee.name, department: employee.department } } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 工资条
|
||||
router.get('/payslip', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const month = req.query.month as string || new Date().toISOString().slice(0, 7)
|
||||
const payslip = await prisma.payslip.findFirst({
|
||||
where: { employeeId: req.employee.id, orgId: req.employee.orgId, month },
|
||||
})
|
||||
if (!payslip) {
|
||||
return res.json({ success: true, data: null })
|
||||
}
|
||||
res.json({ success: true, data: payslip })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 工资条确认已阅
|
||||
router.post('/payslip/:id/confirm', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const payslip = await prisma.payslip.findFirst({
|
||||
where: { id: req.params.id, orgId: req.employee.orgId, employeeId: req.employee.id },
|
||||
})
|
||||
if (!payslip) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '工资条不存在' } })
|
||||
}
|
||||
await prisma.payslip.update({
|
||||
where: { id: req.params.id },
|
||||
data: { confirmedAt: new Date(), confirmedIp: req.ip },
|
||||
})
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 我的合同
|
||||
router.get('/contract', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const contract = await prisma.laborContract.findFirst({
|
||||
where: { employeeId: req.employee.id, orgId: req.employee.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
if (!contract) {
|
||||
return res.json({ success: true, data: null })
|
||||
}
|
||||
res.json({ success: true, data: contract })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 入职填报提交
|
||||
router.post('/onboarding', async (req, res, next) => {
|
||||
try {
|
||||
const data = onboardingSchema.parse(req.body)
|
||||
const link = await prisma.onboardingLink.findFirst({
|
||||
where: { token: data.token, status: 'PENDING', expiresAt: { gt: new Date() } },
|
||||
})
|
||||
if (!link) {
|
||||
return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } })
|
||||
}
|
||||
await prisma.onboardingLink.update({
|
||||
where: { id: link.id },
|
||||
data: {
|
||||
employeeName: data.name,
|
||||
phone: data.phone,
|
||||
formData: {
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
idCard: data.idCard,
|
||||
emergencyContact: data.emergencyContact,
|
||||
emergencyPhone: data.emergencyPhone,
|
||||
address: data.address,
|
||||
bankCard: data.bankCard,
|
||||
bankName: data.bankName,
|
||||
},
|
||||
status: 'APPROVED',
|
||||
usedAt: new Date(),
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: { message: '信息提交成功,HR 将审核您的信息' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 合同签署确认
|
||||
router.post('/contract-confirm', async (req, res, next) => {
|
||||
try {
|
||||
const data = contractConfirmSchema.parse(req.body)
|
||||
const link = await prisma.contractConfirmLink.findFirst({
|
||||
where: { token: data.token, status: 'UNCONFIRMED', expiresAt: { gt: new Date() } },
|
||||
})
|
||||
if (!link) {
|
||||
return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } })
|
||||
}
|
||||
await prisma.contractConfirmLink.update({
|
||||
where: { id: link.id },
|
||||
data: { status: 'CONFIRMED', confirmedAt: new Date(), confirmedIp: req.ip },
|
||||
})
|
||||
await prisma.laborContract.update({
|
||||
where: { id: link.contractId },
|
||||
data: { attachmentName: `confirmed:${new Date().toISOString()}` },
|
||||
})
|
||||
res.json({ success: true, data: { message: '合同签署确认成功' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取入职填报信息(通过 token)
|
||||
router.get('/onboarding/:token', async (req, res, next) => {
|
||||
try {
|
||||
const link = await prisma.onboardingLink.findFirst({
|
||||
where: { token: req.params.token, status: 'PENDING', expiresAt: { gt: new Date() } },
|
||||
include: { org: { select: { name: true } } },
|
||||
})
|
||||
if (!link) {
|
||||
return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } })
|
||||
}
|
||||
res.json({ success: true, data: { orgName: link.org.name } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取合同确认信息(通过 token)
|
||||
router.get('/contract-confirm/:token', async (req, res, next) => {
|
||||
try {
|
||||
const link = await prisma.contractConfirmLink.findFirst({
|
||||
where: { token: req.params.token, status: 'UNCONFIRMED', expiresAt: { gt: new Date() } },
|
||||
include: {
|
||||
contract: {
|
||||
include: {
|
||||
employee: { select: { name: true, org: { select: { name: true } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!link) {
|
||||
return res.status(400).json({ success: false, error: { code: 'LINK_INVALID', message: '链接无效或已过期' } })
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
orgName: link.contract.employee.org.name,
|
||||
employeeName: link.contract.employee.name,
|
||||
contract: link.contract,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,533 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
function safeDecrypt(encrypted: string): number {
|
||||
try {
|
||||
if (!encrypted || !encrypted.includes(':')) return Number(encrypted) || 0
|
||||
return Number(decrypt(encrypted))
|
||||
} catch {
|
||||
return Number(encrypted) || 0
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 花名册聚合 API ==========
|
||||
|
||||
// 花名册列表(含汇总信息)
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employees = await prisma.employee.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' }, take: 1 },
|
||||
_count: {
|
||||
select: {
|
||||
disciplinaryRecords: true,
|
||||
attendanceRecords: true,
|
||||
trainingRecords: true,
|
||||
performanceRecords: true,
|
||||
payslips: true,
|
||||
overtimeRecords: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const result = employees.map((e) => ({
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
department: e.department,
|
||||
status: e.status,
|
||||
hireDate: e.hireDate,
|
||||
gender: e.gender,
|
||||
phone: e.phone,
|
||||
monthlySalary: safeDecrypt(e.monthlySalary),
|
||||
latestContract: e.contracts[0] || null,
|
||||
counts: e._count,
|
||||
}))
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 员工完整档案(花名册详情)
|
||||
router.get('/:id/profile', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' } },
|
||||
payslips: { orderBy: { month: 'desc' } },
|
||||
overtimeRecords: { orderBy: { month: 'desc' } },
|
||||
disciplinaryRecords: { orderBy: { violationDate: 'desc' } },
|
||||
attendanceRecords: { orderBy: { date: 'desc' }, take: 90 },
|
||||
trainingRecords: { orderBy: { trainingDate: 'desc' } },
|
||||
performanceRecords: { orderBy: { period: 'desc' } },
|
||||
terminations: { orderBy: { createdAt: 'desc' } },
|
||||
attachments: true,
|
||||
},
|
||||
})
|
||||
if (!employee) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
}
|
||||
const { monthlySalary, ...rest } = employee
|
||||
res.json({
|
||||
success: true,
|
||||
data: { ...rest, monthlySalary: safeDecrypt(monthlySalary) },
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 仲裁证据链导出
|
||||
router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employee = await prisma.employee.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
include: {
|
||||
contracts: { orderBy: { createdAt: 'desc' } },
|
||||
payslips: { orderBy: { month: 'desc' } },
|
||||
overtimeRecords: { orderBy: { month: 'desc' } },
|
||||
disciplinaryRecords: { orderBy: { violationDate: 'desc' } },
|
||||
attendanceRecords: { orderBy: { date: 'desc' } },
|
||||
trainingRecords: { orderBy: { trainingDate: 'desc' } },
|
||||
performanceRecords: { orderBy: { period: 'desc' } },
|
||||
terminations: true,
|
||||
},
|
||||
})
|
||||
if (!employee) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
}
|
||||
|
||||
const evidence: any[] = []
|
||||
const empName = employee.name
|
||||
const empDept = employee.department
|
||||
const hireDate = employee.hireDate.toISOString().slice(0, 10)
|
||||
|
||||
// 1. 劳动关系证据
|
||||
evidence.push({
|
||||
category: '劳动关系',
|
||||
title: '入职登记',
|
||||
date: hireDate,
|
||||
description: `${empName}于${hireDate}入职${empDept},建立劳动关系。`,
|
||||
evidenceType: 'EMPLOYMENT',
|
||||
})
|
||||
employee.contracts.forEach((c) => {
|
||||
evidence.push({
|
||||
category: '劳动关系',
|
||||
title: `劳动合同(${c.contractType === 'FIXED' ? '固定期限' : c.contractType === 'UNFIXED' ? '无固定期限' : '未签订'})`,
|
||||
date: c.signDate ? c.signDate.toISOString().slice(0, 10) : c.startDate.toISOString().slice(0, 10),
|
||||
description: `合同期限:${c.startDate.toISOString().slice(0, 10)} 至 ${c.endDate ? c.endDate.toISOString().slice(0, 10) : '无固定期限'},试用期${c.probationMonths}个月,试用期工资¥${c.probationSalary}。`,
|
||||
evidenceType: 'CONTRACT',
|
||||
signed: !!c.signDate,
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 薪酬证据
|
||||
employee.payslips.forEach((p) => {
|
||||
evidence.push({
|
||||
category: '薪酬发放',
|
||||
title: `${p.month}月工资条`,
|
||||
date: p.month,
|
||||
description: `基本工资¥${p.baseSalary.toFixed(2)},加班费¥${p.overtimePay.toFixed(2)},津贴¥${p.allowance.toFixed(2)},扣款¥${p.deduction.toFixed(2)},应发合计¥${p.totalPay.toFixed(2)}。${p.confirmedAt ? '员工已确认。' : '员工未确认。'}`,
|
||||
evidenceType: 'PAYSLIP',
|
||||
confirmed: !!p.confirmedAt,
|
||||
})
|
||||
})
|
||||
employee.overtimeRecords.forEach((o) => {
|
||||
if (o.totalPay > 0) {
|
||||
evidence.push({
|
||||
category: '薪酬发放',
|
||||
title: `${o.month}月加班费记录`,
|
||||
date: o.month,
|
||||
description: `工作日加班${o.weekdayHours}h,休息日加班${o.weekendHours}h,节假日加班${o.holidayHours}h,加班费合计¥${o.totalPay.toFixed(2)}。`,
|
||||
evidenceType: 'OVERTIME',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 3. 考勤证据
|
||||
const abnormalAttendance = employee.attendanceRecords.filter((a) => a.status !== 'NORMAL')
|
||||
abnormalAttendance.forEach((a) => {
|
||||
const statusMap: Record<string, string> = { LATE: '迟到', EARLY_LEAVE: '早退', ABSENT: '旷工', LEAVE: '请假', BUSINESS_TRIP: '出差' }
|
||||
evidence.push({
|
||||
category: '考勤记录',
|
||||
title: `${a.date.toISOString().slice(0, 10)} 考勤异常`,
|
||||
date: a.date.toISOString().slice(0, 10),
|
||||
description: `状态:${statusMap[a.status] || a.status}${a.lateMinutes ? `,迟到${a.lateMinutes}分钟` : ''}${a.earlyMinutes ? `,早退${a.earlyMinutes}分钟` : ''}。${a.remark || ''}`,
|
||||
evidenceType: 'ATTENDANCE',
|
||||
})
|
||||
})
|
||||
|
||||
// 4. 违纪证据
|
||||
employee.disciplinaryRecords.forEach((d) => {
|
||||
const typeMap: Record<string, string> = { LATE: '迟到', ABSENT: '旷工', INSUBORDINATION: '不服从管理', MISCONDUCT: '违纪', VIOLATE_POLICY: '违反规章制度', OTHER: '其他' }
|
||||
const actionMap: Record<string, string> = { ORAL_WARNING: '口头警告', WRITTEN_WARNING: '书面警告', DEDUCTION: '扣款', DEMOTION: '降职', TERMINATION: '解除劳动合同' }
|
||||
evidence.push({
|
||||
category: '违纪处理',
|
||||
title: `${d.violationDate.toISOString().slice(0, 10)} ${typeMap[d.violationType] || d.violationType}`,
|
||||
date: d.violationDate.toISOString().slice(0, 10),
|
||||
description: `违纪事实:${d.description}。处理结果:${actionMap[d.action] || d.action}。${d.employeeAck ? `员工已签字确认(${d.ackDate ? d.ackDate.toISOString().slice(0, 10) : ''})。` : '员工未签字。'}${d.witness ? `见证人:${d.witness}。` : ''}`,
|
||||
evidenceType: 'DISCIPLINARY',
|
||||
acknowledged: d.employeeAck,
|
||||
})
|
||||
})
|
||||
|
||||
// 5. 培训签收证据
|
||||
employee.trainingRecords.forEach((t) => {
|
||||
const ackMap: Record<string, string> = { PENDING: '待签收', SIGNED: '已签收', REFUSED: '拒绝签收' }
|
||||
evidence.push({
|
||||
category: '培训签收',
|
||||
title: `${t.trainingDate.toISOString().slice(0, 10)} ${t.topic}`,
|
||||
date: t.trainingDate.toISOString().slice(0, 10),
|
||||
description: `培训主题:${t.topic}。时长:${t.duration}小时。${t.content ? `内容:${t.content}。` : ''}签收状态:${ackMap[t.ackStatus] || t.ackStatus}。`,
|
||||
evidenceType: 'TRAINING',
|
||||
acknowledged: t.ackStatus === 'SIGNED',
|
||||
})
|
||||
})
|
||||
|
||||
// 6. 绩效证据
|
||||
employee.performanceRecords.forEach((p) => {
|
||||
const resultMap: Record<string, string> = { EXCELLENT: '优秀', QUALIFIED: '合格', NEED_IMPROVE: '需改进', UNQUALIFIED: '不胜任' }
|
||||
evidence.push({
|
||||
category: '绩效考核',
|
||||
title: `${p.period} 绩效考核`,
|
||||
date: p.period,
|
||||
description: `得分:${p.score},等级:${p.grade},结果:${resultMap[p.result] || p.result}。${p.summary ? `评语:${p.summary}。` : ''}${p.improvementPlan ? `改进计划:${p.improvementPlan}。` : ''}${p.employeeAck ? '员工已签字确认。' : '员工未签字。'}`,
|
||||
evidenceType: 'PERFORMANCE',
|
||||
acknowledged: p.employeeAck,
|
||||
})
|
||||
})
|
||||
|
||||
// 7. 解聘证据
|
||||
employee.terminations.forEach((t) => {
|
||||
const reasonMap: Record<string, string> = { NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期' }
|
||||
evidence.push({
|
||||
category: '解聘记录',
|
||||
title: `${t.terminationDate.toISOString().slice(0, 10)} 解聘记录`,
|
||||
date: t.terminationDate.toISOString().slice(0, 10),
|
||||
description: `解聘原因:${reasonMap[t.reason] || t.reason}。经济补偿金:¥${t.compensation.toFixed(2)}。${t.remark || ''}`,
|
||||
evidenceType: 'TERMINATION',
|
||||
})
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
employee: {
|
||||
name: empName,
|
||||
department: empDept,
|
||||
hireDate,
|
||||
status: employee.status,
|
||||
gender: employee.gender,
|
||||
phone: employee.phone,
|
||||
},
|
||||
evidence,
|
||||
summary: {
|
||||
total: evidence.length,
|
||||
signed: evidence.filter((e) => e.acknowledged === true).length,
|
||||
unsigned: evidence.filter((e) => e.acknowledged === false).length,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 违纪记录 CRUD ==========
|
||||
|
||||
router.get('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const records = await prisma.disciplinaryRecord.findMany({
|
||||
where: { employeeId: req.params.employeeId, orgId: req.user!.orgId },
|
||||
orderBy: { violationDate: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:employeeId/disciplinary', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { violationDate, violationType, description, severity, action, actionDetail, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body
|
||||
const record = await prisma.disciplinaryRecord.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: req.params.employeeId,
|
||||
violationDate: new Date(violationDate),
|
||||
violationType,
|
||||
description,
|
||||
severity: severity || 'WARNING',
|
||||
action: action || 'ORAL_WARNING',
|
||||
actionDetail,
|
||||
employeeAck: employeeAck || false,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
ackMethod,
|
||||
witness,
|
||||
attachmentUrl,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'DISCIPLINARY', record.id, { employeeId: req.params.employeeId })
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.put('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { violationDate, violationType, description, severity, action, actionDetail, employeeAck, ackDate, ackMethod, witness, attachmentUrl } = req.body
|
||||
const record = await prisma.disciplinaryRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
const updated = await prisma.disciplinaryRecord.update({
|
||||
where: { id: req.params.recordId },
|
||||
data: {
|
||||
violationDate: violationDate ? new Date(violationDate) : undefined,
|
||||
violationType,
|
||||
description,
|
||||
severity,
|
||||
action,
|
||||
actionDetail,
|
||||
employeeAck,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
ackMethod,
|
||||
witness,
|
||||
attachmentUrl,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/:employeeId/disciplinary/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const record = await prisma.disciplinaryRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
await prisma.disciplinaryRecord.delete({ where: { id: req.params.recordId } })
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 考勤记录 CRUD ==========
|
||||
|
||||
router.get('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const records = await prisma.attendanceRecord.findMany({
|
||||
where: { employeeId: req.params.employeeId, orgId: req.user!.orgId },
|
||||
orderBy: { date: 'desc' },
|
||||
take: 90,
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:employeeId/attendance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { date, checkInTime, checkOutTime, status, lateMinutes, earlyMinutes, workHours, overtimeHours, remark } = req.body
|
||||
const record = await prisma.attendanceRecord.upsert({
|
||||
where: { employeeId_date: { employeeId: req.params.employeeId, date: new Date(date) } },
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: req.params.employeeId,
|
||||
date: new Date(date),
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
status: status || 'NORMAL',
|
||||
lateMinutes: lateMinutes || 0,
|
||||
earlyMinutes: earlyMinutes || 0,
|
||||
workHours: workHours || 0,
|
||||
overtimeHours: overtimeHours || 0,
|
||||
remark,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
update: {
|
||||
checkInTime,
|
||||
checkOutTime,
|
||||
status,
|
||||
lateMinutes,
|
||||
earlyMinutes,
|
||||
workHours,
|
||||
overtimeHours,
|
||||
remark,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/:employeeId/attendance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const record = await prisma.attendanceRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
await prisma.attendanceRecord.delete({ where: { id: req.params.recordId } })
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 培训签收记录 CRUD ==========
|
||||
|
||||
router.get('/:employeeId/training', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const records = await prisma.trainingRecord.findMany({
|
||||
where: { employeeId: req.params.employeeId, orgId: req.user!.orgId },
|
||||
orderBy: { trainingDate: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:employeeId/training', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { trainingDate, topic, content, trainer, duration, ackStatus, ackDate, attachmentUrl, remark } = req.body
|
||||
const record = await prisma.trainingRecord.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: req.params.employeeId,
|
||||
trainingDate: new Date(trainingDate),
|
||||
topic,
|
||||
content,
|
||||
trainer,
|
||||
duration: duration || 0,
|
||||
ackStatus: ackStatus || 'PENDING',
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
attachmentUrl,
|
||||
remark,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'TRAINING', record.id, { employeeId: req.params.employeeId })
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.put('/:employeeId/training/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { trainingDate, topic, content, trainer, duration, ackStatus, ackDate, attachmentUrl, remark } = req.body
|
||||
const record = await prisma.trainingRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
const updated = await prisma.trainingRecord.update({
|
||||
where: { id: req.params.recordId },
|
||||
data: {
|
||||
trainingDate: trainingDate ? new Date(trainingDate) : undefined,
|
||||
topic,
|
||||
content,
|
||||
trainer,
|
||||
duration,
|
||||
ackStatus,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
attachmentUrl,
|
||||
remark,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/:employeeId/training/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const record = await prisma.trainingRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
await prisma.trainingRecord.delete({ where: { id: req.params.recordId } })
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 绩效记录 CRUD ==========
|
||||
|
||||
router.get('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const records = await prisma.performanceRecord.findMany({
|
||||
where: { employeeId: req.params.employeeId, orgId: req.user!.orgId },
|
||||
orderBy: { period: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/:employeeId/performance', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = req.body
|
||||
const record = await prisma.performanceRecord.upsert({
|
||||
where: { employeeId_period: { employeeId: req.params.employeeId, period } },
|
||||
create: {
|
||||
orgId: req.user!.orgId,
|
||||
employeeId: req.params.employeeId,
|
||||
period,
|
||||
score: score || 0,
|
||||
grade: grade || 'B',
|
||||
result: result || 'QUALIFIED',
|
||||
summary,
|
||||
improvementPlan,
|
||||
employeeAck: employeeAck || false,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
reviewer,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
update: {
|
||||
score,
|
||||
grade,
|
||||
result,
|
||||
summary,
|
||||
improvementPlan,
|
||||
employeeAck,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
reviewer,
|
||||
},
|
||||
})
|
||||
await auditLog(req, 'CREATE', 'PERFORMANCE', record.id, { employeeId: req.params.employeeId })
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.put('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { period, score, grade, result, summary, improvementPlan, employeeAck, ackDate, reviewer } = req.body
|
||||
const record = await prisma.performanceRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
const updated = await prisma.performanceRecord.update({
|
||||
where: { id: req.params.recordId },
|
||||
data: {
|
||||
period,
|
||||
score,
|
||||
grade,
|
||||
result,
|
||||
summary,
|
||||
improvementPlan,
|
||||
employeeAck,
|
||||
ackDate: ackDate ? new Date(ackDate) : null,
|
||||
reviewer,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/:employeeId/performance/:recordId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const record = await prisma.performanceRecord.findFirst({
|
||||
where: { id: req.params.recordId, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '记录不存在' } })
|
||||
await prisma.performanceRecord.delete({ where: { id: req.params.recordId } })
|
||||
res.json({ success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
phone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
|
||||
email: z.string().email().optional(),
|
||||
})
|
||||
|
||||
const createUserSchema = z.object({
|
||||
name: z.string().min(1, '姓名不能为空'),
|
||||
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
|
||||
password: z.string().min(6, '密码至少6位'),
|
||||
role: z.enum(['ADMIN', 'HR', 'VIEWER']).default('HR'),
|
||||
})
|
||||
|
||||
// 获取企业信息
|
||||
router.get('/org', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const org = await prisma.organization.findUnique({
|
||||
where: { id: req.user!.orgId },
|
||||
select: { id: true, name: true, plan: true, maxEmployees: true, createdAt: true },
|
||||
})
|
||||
res.json({ success: true, data: org })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新企业信息
|
||||
router.put('/org', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { name } = req.body as { name?: string }
|
||||
const org = await prisma.organization.update({
|
||||
where: { id: req.user!.orgId },
|
||||
data: name ? { name } : {},
|
||||
select: { id: true, name: true, plan: true, maxEmployees: true },
|
||||
})
|
||||
res.json({ success: true, data: org })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取用户列表
|
||||
router.get('/users', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
select: { id: true, name: true, phone: true, email: true, role: true, createdAt: true },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
res.json({ success: true, data: users })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加用户
|
||||
router.post('/users', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = createUserSchema.parse(req.body)
|
||||
const existing = await prisma.user.findFirst({ where: { phone: data.phone, orgId: req.user!.orgId } })
|
||||
if (existing) {
|
||||
return res.status(400).json({ success: false, error: { code: 'DUPLICATE', message: '该手机号已存在' } })
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(data.password, 10)
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
passwordHash,
|
||||
role: data.role,
|
||||
},
|
||||
select: { id: true, name: true, phone: true, role: true },
|
||||
})
|
||||
res.json({ success: true, data: user })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新用户
|
||||
router.put('/users/:id', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = updateUserSchema.parse(req.body)
|
||||
const user = await prisma.user.update({
|
||||
where: { id: req.params.id },
|
||||
data: data,
|
||||
select: { id: true, name: true, phone: true, role: true },
|
||||
})
|
||||
res.json({ success: true, data: user })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除用户
|
||||
router.delete('/users/:id', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
if (req.params.id === req.user!.id) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不能删除自己' } })
|
||||
}
|
||||
await prisma.user.delete({ where: { id: req.params.id } })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
router.use(authMiddleware)
|
||||
|
||||
// 获取社保配置
|
||||
router.get('/config', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let config = await prisma.socialInsuranceConfig.findUnique({
|
||||
where: { orgId: req.user!.orgId },
|
||||
})
|
||||
if (!config) {
|
||||
config = await prisma.socialInsuranceConfig.create({
|
||||
data: { orgId: req.user!.orgId },
|
||||
})
|
||||
}
|
||||
res.json({ success: true, data: config })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新社保配置
|
||||
const configSchema = z.object({
|
||||
city: z.string().optional(),
|
||||
pensionOrg: z.number().optional(),
|
||||
pensionEmp: z.number().optional(),
|
||||
medicalOrg: z.number().optional(),
|
||||
medicalEmp: z.number().optional(),
|
||||
unemploymentOrg: z.number().optional(),
|
||||
unemploymentEmp: z.number().optional(),
|
||||
injuryOrg: z.number().optional(),
|
||||
maternityOrg: z.number().optional(),
|
||||
housingOrg: z.number().optional(),
|
||||
housingEmp: z.number().optional(),
|
||||
baseMin: z.number().optional(),
|
||||
baseMax: z.number().optional(),
|
||||
})
|
||||
|
||||
router.put('/config', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const data = configSchema.parse(req.body)
|
||||
const config = await prisma.socialInsuranceConfig.upsert({
|
||||
where: { orgId: req.user!.orgId },
|
||||
update: data,
|
||||
create: { orgId: req.user!.orgId, ...data },
|
||||
})
|
||||
res.json({ success: true, data: config })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 社保计算
|
||||
const calcSchema = z.object({
|
||||
base: z.number().positive(),
|
||||
})
|
||||
|
||||
router.post('/calculate', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { base } = calcSchema.parse(req.body)
|
||||
let config = await prisma.socialInsuranceConfig.findUnique({
|
||||
where: { orgId: req.user!.orgId },
|
||||
})
|
||||
if (!config) {
|
||||
config = await prisma.socialInsuranceConfig.create({ data: { orgId: req.user!.orgId } })
|
||||
}
|
||||
|
||||
const actualBase = Math.min(Math.max(base, config.baseMin), config.baseMax)
|
||||
|
||||
const pensionOrg = actualBase * config.pensionOrg / 100
|
||||
const pensionEmp = actualBase * config.pensionEmp / 100
|
||||
const medicalOrg = actualBase * config.medicalOrg / 100
|
||||
const medicalEmp = actualBase * config.medicalEmp / 100
|
||||
const unemploymentOrg = actualBase * config.unemploymentOrg / 100
|
||||
const unemploymentEmp = actualBase * config.unemploymentEmp / 100
|
||||
const injuryOrg = actualBase * config.injuryOrg / 100
|
||||
const maternityOrg = actualBase * config.maternityOrg / 100
|
||||
const housingOrg = actualBase * config.housingOrg / 100
|
||||
const housingEmp = actualBase * config.housingEmp / 100
|
||||
|
||||
const totalOrg = pensionOrg + medicalOrg + unemploymentOrg + injuryOrg + maternityOrg + housingOrg
|
||||
const totalEmp = pensionEmp + medicalEmp + unemploymentEmp + housingEmp
|
||||
const total = totalOrg + totalEmp
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
actualBase,
|
||||
originalBase: base,
|
||||
capped: base > config.baseMax,
|
||||
floored: base < config.baseMin,
|
||||
items: [
|
||||
{ name: '养老保险', orgRate: config.pensionOrg, empRate: config.pensionEmp, orgAmount: pensionOrg, empAmount: pensionEmp },
|
||||
{ name: '医疗保险', orgRate: config.medicalOrg, empRate: config.medicalEmp, orgAmount: medicalOrg, empAmount: medicalEmp },
|
||||
{ name: '失业保险', orgRate: config.unemploymentOrg, empRate: config.unemploymentEmp, orgAmount: unemploymentOrg, empAmount: unemploymentEmp },
|
||||
{ name: '工伤保险', orgRate: config.injuryOrg, empRate: 0, orgAmount: injuryOrg, empAmount: 0 },
|
||||
{ name: '生育保险', orgRate: config.maternityOrg, empRate: 0, orgAmount: maternityOrg, empAmount: 0 },
|
||||
{ name: '住房公积金', orgRate: config.housingOrg, empRate: config.housingEmp, orgAmount: housingOrg, empAmount: housingEmp },
|
||||
],
|
||||
totalOrg,
|
||||
totalEmp,
|
||||
total,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { terminationChecklistSchema } from '../schemas/termination.schema'
|
||||
import { createTermination, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const result = await getTerminations(req.user!.orgId, page, pageSize)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/checklist/:reason', authMiddleware, (req: AuthRequest, res) => {
|
||||
const checklist = getChecklistForReason(req.params.reason)
|
||||
res.json({ success: true, data: checklist })
|
||||
})
|
||||
|
||||
router.get('/assess/:employeeId', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const employee = await prisma.employee.findFirst({ where: { id: req.params.employeeId, orgId: req.user!.orgId } })
|
||||
if (!employee) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '员工不存在' } })
|
||||
}
|
||||
const assessment = assessRisk(employee, req.query.reason as string || '')
|
||||
res.json({ success: true, data: assessment })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const data = terminationChecklistSchema.parse(req.body)
|
||||
const result = await createTermination(req.user!.orgId, req.user!.id, data)
|
||||
await auditLog(req, 'TERMINATE', 'EMPLOYEE', data.employeeId, { reason: data.reason })
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user