优化: 大文件拆分+代码分割+按需加载+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:
@@ -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
|
||||
@@ -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,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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' })
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user