优化: 大文件拆分+代码分割+按需加载+console清理+any类型替换

- Money.tsx (2260行→54行): 拆分为 money/ 子目录4个组件, React.lazy二级分割
- AIAssistant.tsx (2038行→63行): 拆分为 ai-assistant/ 子目录6个组件, React.lazy二级分割
- xlsx改为动态导入, OvertimeTab从345KB降至12.7KB
- api-services.ts: 请求参数 any→Record<string,unknown>
- 移除前端3处console.log残留
- 后端console替换为pino logger
- 前后端未使用import/变量清理
- Zod schema验证: termination/platform/special-status/work-process
- 新增 leave.routes.ts, acceptance-test.routes.ts
- UI组件: PageGuide, QueryError, Stepper
This commit is contained in:
freedakgmail
2026-08-04 07:53:37 +08:00
parent 1da385cd5d
commit 2968484d2d
109 changed files with 8950 additions and 5926 deletions
@@ -0,0 +1,142 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
const router = Router()
// 保存(创建或更新)
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { verifierName, results, remarks, conclusionName, conclusionDate, conclusionResult, conclusionIssues, screenshots, signature1, signature2 } = req.body
if (!verifierName || !verifierName.trim()) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请输入验收人姓名' } })
}
const orgId = req.user!.orgId
const data = {
verifierName: verifierName.trim(),
results: results || {},
remarks: remarks || {},
conclusionName: conclusionName || null,
conclusionDate: conclusionDate || null,
conclusionResult: conclusionResult || null,
conclusionIssues: conclusionIssues || null,
screenshots: screenshots || undefined,
signature1: signature1 || undefined,
signature2: signature2 || undefined,
}
const record = await prisma.acceptanceTest.upsert({
where: { orgId_verifierName: { orgId, verifierName: verifierName.trim() } },
create: { ...data, orgId, createdBy: req.user!.id },
update: data,
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 获取单个验收记录
router.get('/:verifierName', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await prisma.acceptanceTest.findUnique({
where: {
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
},
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '未找到保存记录' } })
}
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 列表(所有验收记录摘要)
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const records = await prisma.acceptanceTest.findMany({
where: { orgId: req.user!.orgId },
select: {
id: true,
verifierName: true,
status: true,
conclusionResult: true,
conclusionDate: true,
updatedAt: true,
results: true,
},
orderBy: { updatedAt: 'desc' },
})
const list = records.map((r: any) => {
const vals = Object.values(r.results as any)
const pass = vals.filter((v: any) => v === 'pass').length
const total = vals.length
const tested = vals.filter((v: any) => v !== 'untested').length
return {
id: r.id,
verifierName: r.verifierName,
status: r.status,
conclusionResult: r.conclusionResult,
conclusionDate: r.conclusionDate,
updatedAt: r.updatedAt,
testedCount: tested,
passCount: pass,
totalCount: total,
}
})
res.json({ success: true, data: list })
} catch (err) {
next(err)
}
})
// 提交验收报告
router.post('/:verifierName/submit', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { conclusionResult, conclusionIssues } = req.body
const record = await prisma.acceptanceTest.findUnique({
where: {
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
},
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '请先保存再提交' } })
}
const updated = await prisma.acceptanceTest.update({
where: { id: record.id },
data: {
status: 'SUBMITTED',
submittedAt: new Date(),
conclusionResult: conclusionResult || record.conclusionResult,
conclusionIssues: conclusionIssues || record.conclusionIssues,
},
})
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 删除
router.delete('/:verifierName', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
await prisma.acceptanceTest.delete({
where: {
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
},
})
res.json({ success: true })
} catch (err) {
next(err)
}
})
export default router
-1
View File
@@ -2,7 +2,6 @@ import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { z } from 'zod'
import {
createAttendanceConfirmation,
batchCreateAttendanceConfirmations,
getAttendanceConfirmations,
confirmAttendance,
+1 -1
View File
@@ -1,7 +1,7 @@
import { Router, Response, NextFunction } from 'express'
import prisma from '../lib/prisma'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getComplianceScore, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
import { z } from 'zod'
const router = Router()
@@ -1,7 +1,6 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { renderTemplate } from '../services/template.service'
const router = Router()
-1
View File
@@ -255,7 +255,6 @@ router.get('/roster', authMiddleware, async (req: AuthRequest, res: Response, ne
const search = req.query.search as string | undefined
const status = req.query.status as string | undefined
const department = req.query.department as string | undefined
const contractStatus = req.query.contractStatus as string | undefined
const where: any = { orgId }
if (department) where.department = department
+1 -2
View File
@@ -324,7 +324,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
})
result.employees++
result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'success', message: '导入成功' })
result.details.push({ sheet: '员工信息', row: i + 2, name, employeeId: emp.id, status: 'success', message: '导入成功' })
} catch (e: any) {
const msg = e?.message || ''
if (msg.includes('Unique constraint')) {
@@ -720,7 +720,6 @@ router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthR
try {
if (!req.file) return res.status(400).json({ success: false, message: '请上传文件' })
const orgId = req.user!.orgId
const userId = req.user!.id
const batchId = req.body.batchId as string
if (!batchId) return res.status(400).json({ success: false, message: '缺少批次ID' })
+198
View File
@@ -0,0 +1,198 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { parsePagination } from '../lib/pagination'
const router = Router()
// 休假申请列表
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { status, leaveType, employeeId } = req.query
const { page, pageSize } = parsePagination(req.query)
const where: any = { orgId }
if (status) where.status = status
if (leaveType) where.leaveType = leaveType
if (employeeId) where.employeeId = employeeId
const total = await prisma.leaveRequest.count({ where })
const list = await prisma.leaveRequest.findMany({
where,
include: {
employee: { select: { id: true, name: true, department: true, position: true } },
},
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
})
res.json({ success: true, data: { list, total, page, pageSize } })
} catch (err) {
next(err)
}
})
// 创建休假申请
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { employeeId, leaveType, startDate, endDate, days, reason, attachment } = req.body
if (!employeeId || !leaveType || !startDate || !endDate) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少必填字段' } })
}
const record = await prisma.leaveRequest.create({
data: {
orgId,
employeeId,
leaveType,
startDate: new Date(startDate),
endDate: new Date(endDate),
days: days || 1,
reason: reason || null,
attachment: attachment || null,
createdBy: req.user!.id,
},
include: {
employee: { select: { id: true, name: true, department: true, position: true } },
},
})
res.json({ success: true, data: record })
} catch (err) {
next(err)
}
})
// 审批(批准/驳回)
router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const { action, remark } = req.body // action: 'APPROVED' | 'REJECTED'
const orgId = req.user!.orgId
if (!['APPROVED', 'REJECTED'].includes(action)) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '无效的审批操作' } })
}
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (existing.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '该申请已处理' } })
}
const updated = await prisma.leaveRequest.update({
where: { id },
data: {
status: action,
approverId: req.user!.id,
approvedAt: new Date(),
approveRemark: remark || null,
},
include: {
employee: { select: { id: true, name: true, department: true, position: true } },
},
})
// 如果批准,自动创建 LeaveRecord
if (action === 'APPROVED') {
await prisma.leaveRecord.create({
data: {
orgId,
employeeId: existing.employeeId,
leaveType: existing.leaveType,
startDate: existing.startDate,
endDate: existing.endDate,
days: existing.days,
reason: existing.reason || '',
remark: `休假审批通过(申请ID: ${id}`,
createdBy: req.user!.id,
},
})
}
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 撤回申请
router.post('/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const orgId = req.user!.orgId
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (existing.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可撤回' } })
}
const updated = await prisma.leaveRequest.update({
where: { id },
data: { status: 'CANCELLED' },
})
res.json({ success: true, data: updated })
} catch (err) {
next(err)
}
})
// 删除申请(仅待审批状态可删)
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
const orgId = req.user!.orgId
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
if (!existing) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (existing.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可删除' } })
}
await prisma.leaveRequest.delete({ where: { id } })
res.json({ success: true })
} catch (err) {
next(err)
}
})
// 统计
router.get('/stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const { month } = req.query
const where: any = { orgId }
if (month) {
const start = new Date(`${month}-01`)
const end = new Date(`${month}-31T23:59:59`)
where.startDate = { gte: start, lte: end }
}
const [pending, approved, rejected, cancelled, total] = await Promise.all([
prisma.leaveRequest.count({ where: { ...where, status: 'PENDING' } }),
prisma.leaveRequest.count({ where: { ...where, status: 'APPROVED' } }),
prisma.leaveRequest.count({ where: { ...where, status: 'REJECTED' } }),
prisma.leaveRequest.count({ where: { ...where, status: 'CANCELLED' } }),
prisma.leaveRequest.count({ where }),
])
res.json({ success: true, data: { pending, approved, rejected, cancelled, total } })
} catch (err) {
next(err)
}
})
export default router
+7 -6
View File
@@ -132,9 +132,13 @@ router.post('/check-contracts', async (req: AuthRequest, res: Response, next: Ne
})
// 测试通知渠道
const testChannelSchema = z.object({
channel: z.enum(['wechat', 'email']),
})
router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { channel } = req.body as { channel: 'wechat' | 'email' }
const { channel } = testChannelSchema.parse(req.body)
const setting = await prisma.notificationSetting.findUnique({ where: { orgId: req.user!.orgId } })
if (!setting) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '通知设置不存在' } })
@@ -154,12 +158,9 @@ router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction)
} catch (e: any) {
res.json({ success: false, error: { code: 'TEST_FAILED', message: `发送失败: ${e?.message || '网络错误'}` } })
}
} else if (channel === 'email') {
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
// 邮件发送(开发阶段仅返回成功)
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
} else {
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不支持的通知渠道' } })
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
}
} catch (err) {
next(err)
+39 -1
View File
@@ -504,7 +504,45 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res
},
})
res.json({ success: true, data: updated })
res.json({ success: true, data: updated, taxBreakdown: calcResult.taxBreakdown })
} catch (err) {
next(err)
}
})
// 获取条目个税计算明细
router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { batchId, employeeId } = req.params
const orgId = req.user!.orgId
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
const entry = await prisma.batchEntry.findUnique({
where: { batchId_employeeId: { batchId, employeeId } },
})
if (!entry) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '条目不存在' } })
const inputs = {
baseSalary: entry.baseSalary,
overtimePay: entry.overtimePay,
allowance: entry.allowance,
deduction: entry.deduction,
bonus: entry.bonus,
}
const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type)
const taxBreakdown = calcResult.taxBreakdown || {}
// 加入社保公积金系统计算值 vs 实际值对比
taxBreakdown.systemSocialEmp = calcResult.systemSocialEmp
taxBreakdown.systemSocialOrg = calcResult.systemSocialOrg
taxBreakdown.systemHousingEmp = calcResult.systemHousingEmp
taxBreakdown.systemHousingOrg = calcResult.systemHousingOrg
taxBreakdown.actualSocialEmp = entry.socialEmp
taxBreakdown.actualSocialOrg = entry.socialOrg
taxBreakdown.actualHousingEmp = entry.housingEmp
taxBreakdown.actualHousingOrg = entry.housingOrg
res.json({ success: true, data: taxBreakdown })
} catch (err) {
next(err)
}
+8 -30
View File
@@ -6,7 +6,8 @@
import { Router } from 'express'
import prisma from '../lib/prisma'
import { AuthRequest, authMiddleware, platformAdminMiddleware } from '../middleware/auth'
import { loginLimiter } from '../middleware/rateLimit'
import { parsePagination } from '../lib/pagination'
import { createOrgSchema, updateOrgSchema, updateOrgAdminSchema, createPlatformAdminSchema } from '../schemas/platform.schema'
const router = Router()
@@ -88,8 +89,7 @@ router.get('/dashboard', async (_req: AuthRequest, res, next) => {
*/
router.get('/orgs', 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 { page, pageSize } = parsePagination(req.query)
const search = (req.query.search as string) || ''
const planFilter = (req.query.plan as string) || ''
@@ -154,19 +154,7 @@ router.post('/orgs', async (req: AuthRequest, res, next) => {
const {
name, plan, maxEmployees, city, contactName, contactPhone,
adminName, adminPhone, adminPassword,
} = req.body as {
name: string; plan?: string; maxEmployees?: number
city?: string; contactName?: string; contactPhone?: string
adminName?: string; adminPhone: string; adminPassword: string
}
if (!name || !adminPhone || !adminPassword) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '企业名称、管理员手机号、密码不能为空' } })
}
if (adminPassword.length < 8) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '密码至少8位' } })
}
} = createOrgSchema.parse(req.body)
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
if (existing) {
@@ -244,10 +232,7 @@ router.get('/orgs/:id', async (req: AuthRequest, res, next) => {
*/
router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
try {
const { name, plan, maxEmployees, city, contactName, contactPhone } = req.body as {
name?: string; plan?: string; maxEmployees?: number;
city?: string; contactName?: string; contactPhone?: string
}
const { name, plan, maxEmployees, city, contactName, contactPhone } = updateOrgSchema.parse(req.body)
const updateData: any = {}
if (name) updateData.name = name
@@ -274,9 +259,7 @@ router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
*/
router.put('/orgs/:id/admin', async (req: AuthRequest, res, next) => {
try {
const { adminName, adminPhone, adminPassword } = req.body as {
adminName?: string; adminPhone?: string; adminPassword?: string
}
const { adminName, adminPhone, adminPassword } = updateOrgAdminSchema.parse(req.body)
// 找到该企业的 ADMIN 角色用户(第一个管理员)
const admin = await prisma.user.findFirst({
@@ -335,8 +318,7 @@ router.delete('/orgs/:id', async (req: AuthRequest, res, next) => {
*/
router.get('/users', 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 { page, pageSize } = parsePagination(req.query)
const search = (req.query.search as string) || ''
const orgId = (req.query.orgId as string) || ''
@@ -432,11 +414,7 @@ router.get('/admins', async (_req: AuthRequest, res, next) => {
*/
router.post('/admins', async (req: AuthRequest, res, next) => {
try {
const { name, phone, password } = req.body as { name: string; phone: string; password: string }
if (!name || !phone || !password) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '姓名、手机号、密码不能为空' } })
}
const { name, phone, password } = createPlatformAdminSchema.parse(req.body)
const existing = await prisma.user.findUnique({ where: { phone } })
if (existing) {
+60
View File
@@ -838,4 +838,64 @@ router.post('/resignation/:id/withdraw', portalAuth, async (req: any, res, next)
} catch (err) { next(err) }
})
// ========== 员工端:休假申请 ==========
// 查看自己的休假申请列表
router.get('/leaves', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const list = await prisma.leaveRequest.findMany({
where: { employeeId, orgId },
orderBy: { createdAt: 'desc' },
})
res.json({ success: true, data: list })
} catch (err) { next(err) }
})
// 提交休假申请
router.post('/leaves', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const { leaveType, startDate, endDate, days, reason } = req.body
if (!leaveType || !startDate || !endDate) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少必填字段' } })
}
const record = await prisma.leaveRequest.create({
data: {
orgId,
employeeId,
leaveType,
startDate: new Date(startDate),
endDate: new Date(endDate),
days: days || 1,
reason: reason || null,
createdBy: employeeId,
},
})
res.json({ success: true, data: record })
} catch (err) { next(err) }
})
// 撤回休假申请(仅待审批可撤回)
router.post('/leaves/:id/cancel', portalAuth, async (req: any, res, next) => {
try {
const { id: employeeId, orgId } = req.employee
const record = await prisma.leaveRequest.findFirst({
where: { id: req.params.id, employeeId, orgId },
})
if (!record) {
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
}
if (record.status !== 'PENDING') {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可撤回' } })
}
const updated = await prisma.leaveRequest.update({
where: { id: record.id },
data: { status: 'CANCELLED' },
})
res.json({ success: true, data: updated })
} catch (err) { next(err) }
})
export default router
+7 -5
View File
@@ -5,6 +5,7 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { parsePagination } from '../lib/pagination'
import {
createSpecialStatus,
updateSpecialStatus,
@@ -13,6 +14,7 @@ import {
STATUS_TYPES,
getAlertLevel,
} from '../services/special-status.service'
import { createSpecialStatusSchema, updateSpecialStatusSchema } from '../schemas/special-status.schema'
const router = Router()
@@ -22,8 +24,7 @@ const router = Router()
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const orgId = req.user!.orgId
const page = parseInt(req.query.page as string) || 1
const pageSize = parseInt(req.query.pageSize as string) || 20
const { page, pageSize } = parsePagination(req.query)
const type = (req.query.type as string) || ''
const status = (req.query.status as string) || ''
const search = (req.query.search as string) || ''
@@ -97,7 +98,8 @@ router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next:
*/
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, req.body)
const data = createSpecialStatusSchema.parse(req.body)
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, data)
res.json({ success: true, data: record })
} catch (err: any) {
if (err.code) {
@@ -112,7 +114,8 @@ router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: N
*/
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, req.body)
const data = updateSpecialStatusSchema.parse(req.body)
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, data)
res.json({ success: true, data: record })
} catch (err: any) {
if (err.code) {
@@ -167,7 +170,6 @@ router.get('/stats/overview', authMiddleware, async (req: AuthRequest, res: Resp
])
// 预警统计
const now = new Date()
const soon7 = new Date()
soon7.setDate(soon7.getDate() + 7)
const soon30 = new Date()
+13 -24
View File
@@ -1,7 +1,7 @@
import { Router } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import { auditLog } from '../middleware/auditLog'
import { terminationChecklistSchema } from '../schemas/termination.schema'
import { terminationChecklistSchema, resignationSchema, batchTerminatePreviewSchema, batchTerminateSchema, createTerminationDraftSchema, updateTerminationDraftSchema } from '../schemas/termination.schema'
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems, validateTerminationStep } from '../services/termination.service'
import prisma from '../lib/prisma'
import { createEvidence } from '../services/evidence.service'
@@ -80,10 +80,7 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { employeeId, terminationDate, resignationReason, remark } = req.body
if (!employeeId || !terminationDate) {
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '缺少必填字段' } })
}
const { employeeId, terminationDate, resignationReason, remark } = resignationSchema.parse(req.body)
const result = await createResignation(req.user!.orgId, req.user!.id, { employeeId, terminationDate, resignationReason, remark })
await auditLog(req, 'RESIGN', 'EMPLOYEE', employeeId, { resignationReason })
res.json({ success: true, data: result })
@@ -120,12 +117,7 @@ router.delete('/:id/revoke', authMiddleware, async (req: AuthRequest, res, next)
// 批量解聘预检
router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { items } = req.body as {
items: Array<{ employeeId: string; reason: string; terminationDate: string }>
}
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
}
const { items } = batchTerminatePreviewSchema.parse(req.body)
const results = await batchTerminatePreview(req.user!.orgId, items)
res.json({ success: true, data: { total: results.length, warnings: results.filter(r => r.warnings.length > 0).length, results } })
} catch (err) {
@@ -136,12 +128,7 @@ router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next
// 批量解聘执行
router.post('/batch', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const { items } = req.body as {
items: Array<{ employeeId: string; reason: string; terminationDate: string; compensation?: number }>
}
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
}
const { items } = batchTerminateSchema.parse(req.body)
const result = await batchTerminate(req.user!.orgId, req.user!.id, items)
for (const id of result.success) {
await auditLog(req, 'TERMINATE', 'EMPLOYEE', id, { batch: true })
@@ -192,15 +179,16 @@ router.get('/handover-template', authMiddleware, async (req: AuthRequest, res) =
// 创建草稿
router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await createDraft(req.user!.orgId, req.user!.id, req.body)
const emp = await prisma.employee.findFirst({ where: { id: req.body.employeeId }, select: { name: true, department: true } })
const data = createTerminationDraftSchema.parse(req.body)
const result = await createDraft(req.user!.orgId, req.user!.id, data)
const emp = await prisma.employee.findFirst({ where: { id: data.employeeId }, select: { name: true, department: true } })
await auditLog(req, 'CREATE_DRAFT', 'TERMINATION_RECORD', result.id, {
employeeName: emp?.name || '',
department: emp?.department || '',
reason: req.body.reason || '',
type: req.body.type || 'TERMINATION',
terminationDate: req.body.terminationDate || '',
compensation: req.body.compensation || 0,
reason: data.reason || '',
type: data.type || 'TERMINATION',
terminationDate: data.terminationDate || '',
compensation: data.compensation || 0,
})
res.json({ success: true, data: result })
} catch (err: any) {
@@ -214,7 +202,8 @@ router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
// 更新草稿
router.put('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) => {
try {
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, req.body)
const data = updateTerminationDraftSchema.parse(req.body)
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, data)
res.json({ success: true, data: result })
} catch (err: any) {
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
+13 -12
View File
@@ -1,17 +1,17 @@
import { Router, Response, NextFunction } from 'express'
import { authMiddleware, AuthRequest } from '../middleware/auth'
import prisma from '../lib/prisma'
import { parsePagination } from '../lib/pagination'
import { executeWorkProcess, generateDocument, PROCESS_TYPES, PROCESS_STATUS } from '../services/work-process.service'
import { createWorkProcessSchema, updateWorkProcessSchema } from '../schemas/work-process.schema'
const router = Router()
// 创建办理(含草稿)
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { type, title, employeeId, formData, status = 'DRAFT', remark } = req.body
if (!type || !PROCESS_TYPES[type]) {
return res.status(400).json({ success: false, error: { code: 'INVALID_TYPE', message: '无效的流程类型' } })
}
const data = createWorkProcessSchema.parse(req.body)
const { type, title, employeeId, formData, status, remark } = data
const process = await (prisma as any).workProcess.create({
data: {
orgId: req.user!.orgId,
@@ -33,7 +33,8 @@ router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: N
// 列表查询
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { type, status, page = '1', pageSize = '20' } = req.query
const { type, status } = req.query
const { page, pageSize } = parsePagination(req.query)
const where: any = { orgId: req.user!.orgId }
if (type) where.type = type
if (status) where.status = status
@@ -42,10 +43,10 @@ router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: Ne
where,
include: { employee: { select: { id: true, name: true, department: true } } },
orderBy: { createdAt: 'desc' },
skip: (Number(page) - 1) * Number(pageSize),
take: Number(pageSize),
skip: (page - 1) * pageSize,
take: pageSize,
})
res.json({ success: true, data: { items, total, page: Number(page), pageSize: Number(pageSize) } })
res.json({ success: true, data: { items, total, page, pageSize } })
} catch (err) {
next(err)
}
@@ -79,7 +80,7 @@ router.patch('/:id', authMiddleware, async (req: AuthRequest, res: Response, nex
if (existing.status !== 'DRAFT') {
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可编辑' } })
}
const { title, employeeId, formData, remark } = req.body
const { title, employeeId, formData, remark } = updateWorkProcessSchema.parse(req.body)
const updated = await (prisma as any).workProcess.update({
where: { id: req.params.id },
data: {
@@ -116,7 +117,7 @@ router.post('/:id/submit', authMiddleware, async (req: AuthRequest, res: Respons
}
// 生成文书
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const doc = generateDocument(process.type, process.formData, org?.name || '')
const doc = await generateDocument(process.type, process.formData, org?.name || '')
const documents = doc.content ? [doc] : []
const updated = await (prisma as any).workProcess.update({
where: { id: process.id },
@@ -151,7 +152,7 @@ router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Respon
return res.status(400).json({ success: false, error: { code: 'EXEC_FAILED', message: `执行失败:${execErr?.message || '未知错误'}` } })
}
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const doc = generateDocument(process.type, process.formData, org?.name || '')
const doc = await generateDocument(process.type, process.formData, org?.name || '')
const documents = doc.content ? [doc] : []
const updated = await (prisma as any).workProcess.update({
where: { id: process.id },
@@ -247,7 +248,7 @@ router.get('/:id/preview', authMiddleware, async (req: AuthRequest, res: Respons
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
}
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
const doc = generateDocument(process.type, process.formData, org?.name || '')
const doc = await generateDocument(process.type, process.formData, org?.name || '')
res.json({ success: true, data: doc })
} catch (err) {
next(err)