Files
TurboHR/backend/src/routes/settings.routes.ts
T

346 lines
13 KiB
TypeScript

import { Router } from 'express'
import bcrypt from 'bcryptjs'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { requireAdmin } from '../middleware/rbac'
import { z } from 'zod'
import { checkAndUpdateRetirementPolicy, confirmRetirementPolicy } from '../services/retirement.service'
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(),
role: z.enum(['ADMIN', 'HR', 'VIEWER']).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, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: true, createdAt: true },
})
res.json({ success: true, data: org })
} catch (err) {
next(err)
}
})
// 更新企业信息
router.put('/org', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const { name, payrollDays, payrollReminderDays, city, contactName, contactPhone, retirementReminderEnabled, esignPolicyEnabled, esignPayslipEnabled, esignOnboardingEnabled } = req.body as { name?: string; payrollDays?: number[]; payrollReminderDays?: number; city?: string; contactName?: string; contactPhone?: string; retirementReminderEnabled?: boolean; esignPolicyEnabled?: boolean; esignPayslipEnabled?: boolean; esignOnboardingEnabled?: boolean }
const updateData: any = {}
if (name) updateData.name = name
if (payrollDays !== undefined) updateData.payrollDays = payrollDays
if (payrollReminderDays !== undefined) updateData.payrollReminderDays = payrollReminderDays
if (city !== undefined) updateData.city = city
if (contactName !== undefined) updateData.contactName = contactName
if (contactPhone !== undefined) updateData.contactPhone = contactPhone
if (retirementReminderEnabled !== undefined) updateData.retirementReminderEnabled = retirementReminderEnabled
if (esignPolicyEnabled !== undefined) updateData.esignPolicyEnabled = esignPolicyEnabled
if (esignPayslipEnabled !== undefined) updateData.esignPayslipEnabled = esignPayslipEnabled
if (esignOnboardingEnabled !== undefined) updateData.esignOnboardingEnabled = esignOnboardingEnabled
const org = await prisma.organization.update({
where: { id: req.user!.orgId },
data: updateData,
select: { id: true, name: true, plan: true, maxEmployees: true, city: true, contactName: true, contactPhone: true, payrollDays: true, payrollReminderDays: true, retirementReminderEnabled: true, esignPolicyEnabled: true, esignPayslipEnabled: true, esignOnboardingEnabled: 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, disabled: true, createdAt: true, lastLoginAt: true },
orderBy: { createdAt: 'asc' },
})
res.json({ success: true, data: users })
} catch (err) {
next(err)
}
})
// 添加用户
router.post('/users', requireAdmin, 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', requireAdmin, 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, email: true, role: true, disabled: true },
})
res.json({ success: true, data: user })
} catch (err) {
next(err)
}
})
// 删除用户
router.delete('/users/:id', requireAdmin, 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)
}
})
// 禁用/启用用户
router.patch('/users/:id/toggle-disable', requireAdmin, 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: '不能禁用自己' } })
}
const existing = await prisma.user.findUnique({ where: { id: req.params.id } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '用户不存在' } })
}
const user = await prisma.user.update({
where: { id: req.params.id },
data: { disabled: !existing.disabled },
select: { id: true, name: true, disabled: true },
})
res.json({ success: true, data: user })
} catch (err) {
next(err)
}
})
// 切换套餐
router.put('/plan', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const { plan } = req.body as { plan: 'FREE' | 'PRO' | 'ENTERPRISE' }
if (!['FREE', 'PRO', 'ENTERPRISE'].includes(plan)) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '无效的套餐' } })
}
const maxEmployees = plan === 'FREE' ? 10 : plan === 'PRO' ? 100 : 999999
const org = await prisma.organization.update({
where: { id: req.user!.orgId },
data: { plan, maxEmployees },
select: { id: true, name: true, plan: true, maxEmployees: true },
})
res.json({ success: true, data: org })
} catch (err) {
next(err)
}
})
// 用量统计
router.get('/usage', async (req: AuthRequest, res, next) => {
try {
const orgId = req.user!.orgId
const [employeeCount, aiConversations, contracts] = await Promise.all([
prisma.employee.count({ where: { orgId } }),
prisma.aIConversation.count({ where: { orgId } }),
prisma.laborContract.count({ where: { orgId } }),
])
const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { plan: true, maxEmployees: true } })
res.json({
success: true,
data: {
plan: org?.plan || 'FREE',
maxEmployees: org?.maxEmployees || 10,
employeeCount,
aiConversations,
contracts,
employeeUsage: `${employeeCount}/${org?.maxEmployees || 10}`,
},
})
} catch (err) {
next(err)
}
})
export default router
// ========== 退休政策 ==========
// 获取当前生效的退休政策和待确认政策
router.get('/retirement-policy', async (req: AuthRequest, res, next) => {
try {
const orgId = req.user!.orgId
const org = await prisma.organization.findUnique({ where: { id: orgId }, select: { retirementReminderEnabled: true } })
// 如果开启了退休提醒,懒加载检查是否需要获取新政策
if (org?.retirementReminderEnabled) {
await checkAndUpdateRetirementPolicy(orgId).catch(() => {})
}
// 查询当前生效的政策
const confirmedPolicy = await prisma.retirementPolicy.findFirst({
where: { orgId, status: 'CONFIRMED' },
orderBy: { createdAt: 'desc' },
})
// 查询待确认的政策
const pendingPolicy = await prisma.retirementPolicy.findFirst({
where: { orgId, status: 'PENDING' },
orderBy: { createdAt: 'desc' },
})
res.json({
success: true,
data: {
enabled: org?.retirementReminderEnabled || false,
confirmed: confirmedPolicy,
pending: pendingPolicy,
},
})
} catch (err) {
next(err)
}
})
// 确认退休政策
router.post('/retirement-policy/:id/confirm', requireAdmin, async (req: AuthRequest, res, next) => {
try {
await confirmRetirementPolicy(req.user!.orgId, req.params.id)
res.json({ success: true, message: '退休政策已确认生效' })
} catch (err: any) {
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: err.message || '确认失败' } })
}
})
// ========== 医疗期政策配置 ==========
const DEFAULT_POLICIES = [
{
region: '全国',
legalBasis: '《企业职工患病或非因工负伤医疗期规定》第三条(劳部发[1994]479号)',
rules: [
{ maxYears: 5, months: 3, cycleMonths: 6 },
{ maxYears: 10, months: 6, cycleMonths: 12 },
{ maxYears: 15, months: 9, cycleMonths: 15 },
{ maxYears: 20, months: 12, cycleMonths: 18 },
{ maxYears: 999, months: 24, cycleMonths: 30 },
],
isDefault: true,
},
{
region: '上海',
legalBasis: '《上海市关于本市劳动者在履行劳动合同期间患病或者非因工负伤的医疗期标准的规定》',
rules: [
{ maxYears: 1, months: 3, cycleMonths: 6 },
{ maxYears: 4, months: 3, cycleMonths: 6 },
{ maxYears: 10, months: 6, cycleMonths: 12 },
{ maxYears: 999, months: 9, cycleMonths: 18 },
],
isDefault: false,
},
]
// 获取医疗期政策列表
router.get('/medical-period/policies', async (req: AuthRequest, res, next) => {
try {
let policies = await prisma.medicalPeriodPolicy.findMany({
where: { orgId: req.user!.orgId },
orderBy: [{ isDefault: 'desc' }, { region: 'asc' }],
})
if (policies.length === 0) {
policies = await prisma.$transaction(
DEFAULT_POLICIES.map(p =>
prisma.medicalPeriodPolicy.create({
data: { orgId: req.user!.orgId, ...p },
})
)
)
}
res.json({ success: true, data: policies })
} catch (err) {
next(err)
}
})
// 新增/编辑医疗期政策
const medicalPolicySchema = z.object({
region: z.string().min(1, '地区名称不能为空'),
legalBasis: z.string().min(1, '法律依据不能为空'),
rules: z.array(z.object({
maxYears: z.number().min(0),
months: z.number().min(1),
cycleMonths: z.number().min(1),
})).min(1, '至少需要一条分档规则'),
isDefault: z.boolean().default(false),
})
router.post('/medical-period/policies', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const data = medicalPolicySchema.parse(req.body)
if (data.isDefault) {
await prisma.medicalPeriodPolicy.updateMany({
where: { orgId: req.user!.orgId },
data: { isDefault: false },
})
}
const policy = await prisma.medicalPeriodPolicy.upsert({
where: { orgId_region: { orgId: req.user!.orgId, region: data.region } },
update: { legalBasis: data.legalBasis, rules: data.rules, isDefault: data.isDefault },
create: { orgId: req.user!.orgId, ...data },
})
res.json({ success: true, data: policy })
} catch (err: any) {
if (err.issues) return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: err.issues[0]?.message } })
next(err)
}
})
// 删除医疗期政策
router.delete('/medical-period/policies/:id', requireAdmin, async (req: AuthRequest, res, next) => {
try {
const policy = await prisma.medicalPeriodPolicy.findFirst({
where: { id: req.params.id, orgId: req.user!.orgId },
})
if (!policy) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '政策不存在' } })
if (policy.isDefault) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不能删除默认政策' } })
await prisma.medicalPeriodPolicy.delete({ where: { id: req.params.id } })
res.json({ success: true })
} catch (err) {
next(err)
}
})