feat: 实现20260730优化方案全部功能
- AI文件审查:.docx上传提取文本,支持多种文档类型 - 用工办理工作流:WorkProcess页面+后端API,支持入职/续签/终止等流程 - 企业自建文本库:Templates页面Tab切换,企业模板CRUD+渲染+下载Word - 考勤发布:Attendance发布/取消发布按钮,员工端MyAttendance页面 - 工资条发布:Money发布/定时发送按钮+弹窗,portal端publishStatus过滤 - 合同到期弹窗:Dashboard合同到期预警可点击打开弹窗,支持续签/终止操作 - Prisma schema新增WorkProcess/EnterpriseTemplate/AttendancePublish模型 - 前后端编译验证全部通过
This commit is contained in:
Generated
+96
@@ -19,6 +19,7 @@
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mammoth": "^1.12.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^2.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
@@ -944,6 +945,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.13",
|
||||
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -1097,6 +1107,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -1638,6 +1657,21 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dingbat-to-unicode": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/duck": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/duck/-/duck-0.1.12.tgz",
|
||||
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
|
||||
"license": "BSD",
|
||||
"dependencies": {
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -2576,6 +2610,17 @@
|
||||
"integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lop": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/lop/-/lop-0.4.2.tgz",
|
||||
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"duck": "^0.1.12",
|
||||
"option": "~0.2.1",
|
||||
"underscore": "^1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -2583,6 +2628,30 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mammoth": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmmirror.com/mammoth/-/mammoth-1.12.0.tgz",
|
||||
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.6",
|
||||
"argparse": "~1.0.3",
|
||||
"base64-js": "^1.5.1",
|
||||
"bluebird": "~3.4.0",
|
||||
"dingbat-to-unicode": "^1.0.1",
|
||||
"jszip": "^3.7.1",
|
||||
"lop": "^0.4.2",
|
||||
"path-is-absolute": "^1.0.0",
|
||||
"underscore": "^1.13.1",
|
||||
"xmlbuilder": "^10.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"mammoth": "bin/mammoth"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -2860,6 +2929,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/option": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/option/-/option-0.2.4.tgz",
|
||||
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
|
||||
@@ -3297,6 +3372,12 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
|
||||
@@ -3578,6 +3659,12 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.8",
|
||||
"resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.13.8.tgz",
|
||||
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||
@@ -3730,6 +3817,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mammoth": "^1.12.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^2.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
|
||||
@@ -172,6 +172,9 @@ model Organization {
|
||||
leaveRecords LeaveRecord[]
|
||||
calendarEvents CalendarEvent[]
|
||||
consultations Consultation[]
|
||||
workProcesses WorkProcess[]
|
||||
enterpriseTemplates EnterpriseTemplate[]
|
||||
attendancePublishes AttendancePublish[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -252,6 +255,7 @@ model Employee {
|
||||
shiftAssignments ShiftAssignment[]
|
||||
leaveRecords LeaveRecord[]
|
||||
calendarEvents CalendarEvent[]
|
||||
workProcesses WorkProcess[]
|
||||
|
||||
@@unique([orgId, idCardHash])
|
||||
}
|
||||
@@ -628,6 +632,8 @@ model Payslip {
|
||||
confirmedAt DateTime?
|
||||
confirmedIp String?
|
||||
publishedAt DateTime? // 工资条发布到员工端的时间
|
||||
publishStatus String? // UNPUBLISHED/PUBLISHED/SCHEDULED
|
||||
scheduledAt DateTime? // 定时发送时间
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1171,3 +1177,61 @@ model Consultation {
|
||||
@@index([orgId, status])
|
||||
@@index([orgId, type])
|
||||
}
|
||||
|
||||
// ========== 用工办理工作流 ==========
|
||||
model WorkProcess {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
type String // HIRE/ONBOARD/CUSTOM_CONTRACT/INFO_SUBMIT/CONFIRM/CHANGE/RENEW/SUSPEND/INCOME_CERT/TERMINATE/RESCIND/LEAVING_CERT/FLEXIBLE
|
||||
title String
|
||||
employeeId String?
|
||||
employee Employee? @relation(fields: [employeeId], references: [id], onDelete: SetNull)
|
||||
status String @default("DRAFT") // DRAFT/PENDING_APPROVAL/APPROVED/REJECTED/EXECUTING/COMPLETED/CANCELLED
|
||||
formData Json // 表单数据 JSON
|
||||
documents Json? // 生成的文书列表 [{name, content, type}]
|
||||
approverId String?
|
||||
approvedAt DateTime?
|
||||
remark String?
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, status])
|
||||
@@index([orgId, type])
|
||||
@@index([orgId, createdBy])
|
||||
}
|
||||
|
||||
// ========== 企业自建文本模板 ==========
|
||||
model EnterpriseTemplate {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
category String // CONTRACT/RULES/NOTICE/AGREEMENT/OTHER
|
||||
description String?
|
||||
content String @db.Text
|
||||
variables String[] // 变量名列表
|
||||
status String @default("ACTIVE") // ACTIVE/ARCHIVED
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, category])
|
||||
}
|
||||
|
||||
// ========== 考勤发布记录 ==========
|
||||
model AttendancePublish {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
month String // YYYY-MM
|
||||
title String // 如"2026年7月考勤表"
|
||||
status String @default("PUBLISHED") // PUBLISHED/CANCELLED
|
||||
publishDate DateTime @default(now())
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([orgId, month])
|
||||
@@index([orgId, month])
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ import templateRoutes from './routes/template.routes'
|
||||
import auditRoutes from './routes/audit.routes'
|
||||
import calendarRoutes from './routes/calendar.routes'
|
||||
import platformRoutes from './routes/platform.routes'
|
||||
import workProcessRoutes from './routes/work-process.routes'
|
||||
import enterpriseTemplateRoutes from './routes/enterprise-template.routes'
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
app.use('/api/v1/dashboard', dashboardRoutes)
|
||||
app.use('/api/v1/employees', employeeRoutes)
|
||||
@@ -80,6 +82,8 @@ app.use('/api/v1/templates', templateRoutes)
|
||||
app.use('/api/v1/audit', auditRoutes)
|
||||
app.use('/api/v1/calendar', calendarRoutes)
|
||||
app.use('/api/v1/platform', platformRoutes)
|
||||
app.use('/api/v1/work-processes', workProcessRoutes)
|
||||
app.use('/api/v1/enterprise-templates', enterpriseTemplateRoutes)
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import { seedKnowledgeBase, addKnowledge, searchKnowledge, ensureRAGTable, searc
|
||||
import prisma from '../lib/prisma'
|
||||
import { z } from 'zod'
|
||||
import { decrypt } from '../lib/crypto'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import mammoth from 'mammoth'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -947,6 +950,42 @@ ${expiringContracts.length > 0 ? expiringContracts.join('\n') : '无'}`
|
||||
}
|
||||
})
|
||||
|
||||
// ========== AI 文件审查上传 ==========
|
||||
|
||||
const reviewUpload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 100 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase()
|
||||
if (ext !== '.docx' && ext !== '.doc') {
|
||||
return cb(null, false)
|
||||
}
|
||||
cb(null, true)
|
||||
},
|
||||
})
|
||||
|
||||
router.post('/review/upload', authMiddleware, reviewUpload.single('file'), async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ success: false, error: { code: 'NO_FILE', message: '请上传 .docx 文件' } })
|
||||
}
|
||||
const ext = path.extname(req.file.originalname).toLowerCase()
|
||||
let text = ''
|
||||
if (ext === '.docx') {
|
||||
const result = await mammoth.extractRawText({ buffer: req.file.buffer })
|
||||
text = result.value
|
||||
} else {
|
||||
return res.status(400).json({ success: false, error: { code: 'UNSUPPORTED', message: '暂不支持 .doc 格式,请将文件另存为 .docx 后上传' } })
|
||||
}
|
||||
if (text.length > 50000) {
|
||||
text = text.slice(0, 50000) + '\n\n[文本过长,已截断]'
|
||||
}
|
||||
res.json({ success: true, data: { text, fileName: req.file.originalname } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 人工咨询服务 ==========
|
||||
|
||||
router.post('/consultation', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
deleteLeaveRecord,
|
||||
} from '../services/attendance.service'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
import prisma from '../lib/prisma'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -238,4 +239,76 @@ router.delete('/leaves/:id', authMiddleware, async (req: AuthRequest, res: Respo
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 考勤发布 ==========
|
||||
|
||||
// 发布考勤表
|
||||
router.post('/publish', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { month, title } = req.body
|
||||
if (!month) {
|
||||
return res.status(400).json({ success: false, error: { code: 'MISSING_MONTH', message: '请选择月份' } })
|
||||
}
|
||||
// 检查是否已发布且未取消
|
||||
const existing = await (prisma as any).attendancePublish.findFirst({
|
||||
where: { orgId: req.user!.orgId, month, status: 'PUBLISHED' },
|
||||
})
|
||||
if (existing) {
|
||||
return res.status(400).json({ success: false, error: { code: 'ALREADY_PUBLISHED', message: `${month}月考勤表已发布` } })
|
||||
}
|
||||
// 如果有已取消的记录,删除后重新创建
|
||||
const cancelled = await (prisma as any).attendancePublish.findFirst({
|
||||
where: { orgId: req.user!.orgId, month, status: 'CANCELLED' },
|
||||
})
|
||||
if (cancelled) {
|
||||
await (prisma as any).attendancePublish.delete({ where: { id: cancelled.id } })
|
||||
}
|
||||
const record = await (prisma as any).attendancePublish.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
month,
|
||||
title: title || `${month}月考勤表`,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 发布记录列表
|
||||
router.get('/publish-records', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const records = await (prisma as any).attendancePublish.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 取消发布
|
||||
router.post('/publish/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await (prisma as any).attendancePublish.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '发布记录不存在' } })
|
||||
}
|
||||
if (record.status !== 'PUBLISHED') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_PUBLISHED', message: '仅已发布状态可取消' } })
|
||||
}
|
||||
const updated = await (prisma as any).attendancePublish.update({
|
||||
where: { id: record.id },
|
||||
data: { status: 'CANCELLED' },
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
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()
|
||||
|
||||
// 提取变量名
|
||||
function extractVariables(content: string): string[] {
|
||||
const matches = content.match(/\{\{(\w+)\}\}/g) || []
|
||||
return [...new Set(matches.map(m => m.replace(/\{\{|\}\}/g, '')))]
|
||||
}
|
||||
|
||||
// 列表
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { category, search } = req.query
|
||||
const where: any = { orgId: req.user!.orgId, status: 'ACTIVE' }
|
||||
if (category) where.category = category
|
||||
if (search) where.name = { contains: String(search) }
|
||||
const items = await (prisma as any).enterpriseTemplate.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: items })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 新建
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, category, description, content } = req.body
|
||||
if (!name || !category || !content) {
|
||||
return res.status(400).json({ success: false, error: { code: 'MISSING_FIELDS', message: '名称、分类、内容为必填' } })
|
||||
}
|
||||
const variables = extractVariables(content)
|
||||
const template = await (prisma as any).enterpriseTemplate.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
name,
|
||||
category,
|
||||
description: description || null,
|
||||
content,
|
||||
variables,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: template })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 详情
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const template = await (prisma as any).enterpriseTemplate.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!template) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
res.json({ success: true, data: template })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新
|
||||
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const existing = await (prisma as any).enterpriseTemplate.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
const { name, category, description, content, status } = req.body
|
||||
const variables = content ? extractVariables(content) : existing.variables
|
||||
const updated = await (prisma as any).enterpriseTemplate.update({
|
||||
where: { id: req.params.id },
|
||||
data: {
|
||||
...(name !== undefined && { name }),
|
||||
...(category !== undefined && { category }),
|
||||
...(description !== undefined && { description }),
|
||||
...(content !== undefined && { content, variables }),
|
||||
...(status !== undefined && { status }),
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除
|
||||
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const existing = await (prisma as any).enterpriseTemplate.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
await (prisma as any).enterpriseTemplate.delete({ where: { id: req.params.id } })
|
||||
res.json({ success: true, data: { message: '已删除' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 渲染
|
||||
router.post('/:id/render', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const template = await (prisma as any).enterpriseTemplate.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!template) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
const { variables } = req.body as { variables: Record<string, string> }
|
||||
let content = template.content
|
||||
for (const [key, value] of Object.entries(variables || {})) {
|
||||
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value)
|
||||
}
|
||||
res.json({ success: true, data: { content } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 下载 Word
|
||||
router.get('/:id/download', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const template = await (prisma as any).enterpriseTemplate.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!template) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '模板不存在' } })
|
||||
}
|
||||
const encoded = encodeURIComponent(template.name + '.doc')
|
||||
res.setHeader('Content-Type', 'application/msword')
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encoded}"; filename*=UTF-8''${encoded}`)
|
||||
res.send(template.content)
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -870,4 +870,99 @@ router.get('/batches/:batchId/pre-check', async (req: AuthRequest, res: Response
|
||||
}
|
||||
})
|
||||
|
||||
// ========== 工资条发布 ==========
|
||||
|
||||
// 发布工资条(将批次内所有工资条标记为 PUBLISHED)
|
||||
router.post('/batches/:batchId/publish', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { batchId } = 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: '批次不存在' } })
|
||||
}
|
||||
// 查找该批次关联的所有工资条(通过 BatchEntry 关联的 employeeId + month)
|
||||
const entries = await prisma.batchEntry.findMany({
|
||||
where: { batchId, orgId },
|
||||
select: { employeeId: true },
|
||||
})
|
||||
const employeeIds = entries.map(e => e.employeeId)
|
||||
if (employeeIds.length === 0) {
|
||||
return res.status(400).json({ success: false, error: { code: 'EMPTY', message: '批次内无员工' } })
|
||||
}
|
||||
// 更新对应月份的工资条
|
||||
const result = await prisma.payslip.updateMany({
|
||||
where: { orgId, employeeId: { in: employeeIds }, month: batch.month },
|
||||
data: { publishStatus: 'PUBLISHED', publishedAt: new Date() },
|
||||
})
|
||||
res.json({ success: true, data: { published: result.count, month: batch.month } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 定时发送工资条
|
||||
router.post('/batches/:batchId/schedule', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { batchId } = req.params
|
||||
const { scheduledAt } = req.body
|
||||
if (!scheduledAt) {
|
||||
return res.status(400).json({ success: false, error: { code: 'MISSING_DATE', message: '请选择发送时间' } })
|
||||
}
|
||||
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 entries = await prisma.batchEntry.findMany({
|
||||
where: { batchId, orgId },
|
||||
select: { employeeId: true },
|
||||
})
|
||||
const employeeIds = entries.map(e => e.employeeId)
|
||||
if (employeeIds.length === 0) {
|
||||
return res.status(400).json({ success: false, error: { code: 'EMPTY', message: '批次内无员工' } })
|
||||
}
|
||||
const result = await prisma.payslip.updateMany({
|
||||
where: { orgId, employeeId: { in: employeeIds }, month: batch.month },
|
||||
data: { publishStatus: 'SCHEDULED', scheduledAt: new Date(scheduledAt) },
|
||||
})
|
||||
res.json({ success: true, data: { scheduled: result.count, scheduledAt } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 定时发送记录
|
||||
router.get('/schedule-records', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const records = await prisma.payslip.findMany({
|
||||
where: { orgId: req.user!.orgId, publishStatus: 'SCHEDULED' },
|
||||
include: { employee: { select: { name: true, department: true } } },
|
||||
orderBy: { scheduledAt: 'asc' },
|
||||
})
|
||||
res.json({ success: true, data: records })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 取消定时发送
|
||||
router.post('/schedule/:id/cancel', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const payslip = await prisma.payslip.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId, publishStatus: 'SCHEDULED' },
|
||||
})
|
||||
if (!payslip) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '定时发送记录不存在' } })
|
||||
}
|
||||
const updated = await prisma.payslip.update({
|
||||
where: { id: payslip.id },
|
||||
data: { publishStatus: 'UNPUBLISHED', scheduledAt: null },
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -110,7 +110,7 @@ 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 },
|
||||
where: { employeeId: req.employee.id, orgId: req.employee.orgId, month, publishStatus: 'PUBLISHED' },
|
||||
})
|
||||
if (!payslip) {
|
||||
return res.json({ success: true, data: null })
|
||||
@@ -125,7 +125,7 @@ router.get('/payslip', portalAuth, async (req: any, res, next) => {
|
||||
router.get('/payslip/history', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const payslips = await prisma.payslip.findMany({
|
||||
where: { employeeId: req.employee.id, orgId: req.employee.orgId },
|
||||
where: { employeeId: req.employee.id, orgId: req.employee.orgId, publishStatus: 'PUBLISHED' },
|
||||
orderBy: { month: 'desc' },
|
||||
take: 6,
|
||||
})
|
||||
@@ -611,4 +611,33 @@ router.get('/auto-login', async (req, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 员工端查看自己的月度考勤
|
||||
router.get('/attendance', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const month = req.query.month as string || new Date().toISOString().slice(0, 7)
|
||||
// 检查该月份是否已发布
|
||||
const publish = await (prisma as any).attendancePublish.findFirst({
|
||||
where: { orgId: req.employee.orgId, month, status: 'PUBLISHED' },
|
||||
})
|
||||
if (!publish) {
|
||||
return res.json({ success: true, data: { published: false, records: [] } })
|
||||
}
|
||||
// 查询该月考勤记录
|
||||
const startDate = new Date(`${month}-01`)
|
||||
const endDate = new Date(startDate)
|
||||
endDate.setMonth(endDate.getMonth() + 1)
|
||||
const records = await prisma.attendanceRecord.findMany({
|
||||
where: {
|
||||
employeeId: req.employee.id,
|
||||
orgId: req.employee.orgId,
|
||||
date: { gte: startDate, lt: endDate },
|
||||
},
|
||||
orderBy: { date: 'asc' },
|
||||
})
|
||||
res.json({ success: true, data: { published: true, records, title: publish.title } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { executeWorkProcess, generateDocument, PROCESS_TYPES, PROCESS_STATUS } from '../services/work-process.service'
|
||||
|
||||
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 process = await (prisma as any).workProcess.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
type,
|
||||
title: title || PROCESS_TYPES[type].label,
|
||||
employeeId: employeeId || null,
|
||||
formData: formData || {},
|
||||
status,
|
||||
remark: remark || null,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: process })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 列表查询
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { type, status, page = '1', pageSize = '20' } = req.query
|
||||
const where: any = { orgId: req.user!.orgId }
|
||||
if (type) where.type = type
|
||||
if (status) where.status = status
|
||||
const total = await (prisma as any).workProcess.count({ where })
|
||||
const items = await (prisma as any).workProcess.findMany({
|
||||
where,
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (Number(page) - 1) * Number(pageSize),
|
||||
take: Number(pageSize),
|
||||
})
|
||||
res.json({ success: true, data: { items, total, page: Number(page), pageSize: Number(pageSize) } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 详情
|
||||
router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
include: { employee: { select: { id: true, name: true, department: true, status: true } } },
|
||||
})
|
||||
if (!process) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
res.json({ success: true, data: process })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 更新草稿
|
||||
router.patch('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const existing = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
if (existing.status !== 'DRAFT') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可编辑' } })
|
||||
}
|
||||
const { title, employeeId, formData, remark } = req.body
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: req.params.id },
|
||||
data: {
|
||||
...(title !== undefined && { title }),
|
||||
...(employeeId !== undefined && { employeeId: employeeId || null }),
|
||||
...(formData !== undefined && { formData }),
|
||||
...(remark !== undefined && { remark }),
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 提交办理
|
||||
router.post('/:id/submit', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!process) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
if (process.status !== 'DRAFT') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可提交' } })
|
||||
}
|
||||
// 执行业务联动
|
||||
let execResult: any = {}
|
||||
try {
|
||||
execResult = await executeWorkProcess(process.id, process.type, process.formData, req.user!.orgId, req.user!.id)
|
||||
} catch (execErr: any) {
|
||||
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 documents = doc.content ? [doc] : []
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: process.id },
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
documents: documents.length > 0 ? documents : null,
|
||||
...(execResult.employeeId && !process.employeeId && { employeeId: execResult.employeeId }),
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 审批通过
|
||||
router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!process) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
if (process.status !== 'PENDING_APPROVAL') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_PENDING', message: '仅待审批状态可审批' } })
|
||||
}
|
||||
let execResult: any = {}
|
||||
try {
|
||||
execResult = await executeWorkProcess(process.id, process.type, process.formData, req.user!.orgId, req.user!.id)
|
||||
} catch (execErr: any) {
|
||||
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 documents = doc.content ? [doc] : []
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: process.id },
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
approverId: req.user!.id,
|
||||
approvedAt: new Date(),
|
||||
documents: documents.length > 0 ? documents : null,
|
||||
...(execResult.employeeId && !process.employeeId && { employeeId: execResult.employeeId }),
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 驳回
|
||||
router.post('/:id/reject', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!process) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
if (process.status !== 'PENDING_APPROVAL') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_PENDING', message: '仅待审批状态可驳回' } })
|
||||
}
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: process.id },
|
||||
data: {
|
||||
status: 'REJECTED',
|
||||
approverId: req.user!.id,
|
||||
approvedAt: new Date(),
|
||||
remark: req.body.reason || '驳回',
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 撤销
|
||||
router.post('/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!process) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
if (['COMPLETED', 'CANCELLED'].includes(process.status)) {
|
||||
return res.status(400).json({ success: false, error: { code: 'INVALID_STATUS', message: '已完成或已撤销的记录不可撤销' } })
|
||||
}
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: process.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 process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!process) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
if (process.status !== 'DRAFT') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可删除' } })
|
||||
}
|
||||
await (prisma as any).workProcess.delete({ where: { id: process.id } })
|
||||
res.json({ success: true, data: { message: '已删除' } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 预览文书
|
||||
router.get('/:id/preview', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const process = await (prisma as any).workProcess.findFirst({
|
||||
where: { id: req.params.id, orgId: req.user!.orgId },
|
||||
})
|
||||
if (!process) {
|
||||
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 || '')
|
||||
res.json({ success: true, data: doc })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取流程类型列表
|
||||
router.get('/meta/types', authMiddleware, (_req: AuthRequest, res: Response) => {
|
||||
res.json({ success: true, data: PROCESS_TYPES })
|
||||
})
|
||||
|
||||
// 获取状态列表
|
||||
router.get('/meta/statuses', authMiddleware, (_req: AuthRequest, res: Response) => {
|
||||
res.json({ success: true, data: PROCESS_STATUS })
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,286 @@
|
||||
import prisma from '../lib/prisma'
|
||||
import { encrypt } from '../lib/crypto'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// 13类流程定义
|
||||
export const PROCESS_TYPES: Record<string, { label: string; description: string; icon: string }> = {
|
||||
HIRE: { label: '员工录用', description: '录用新员工并起草劳动合同', icon: 'user-plus' },
|
||||
ONBOARD: { label: '员工入职', description: '办理员工入职手续', icon: 'log-in' },
|
||||
CUSTOM_CONTRACT: { label: '自定义合同签署', description: '自定义合同内容并签署', icon: 'file-signature' },
|
||||
INFO_SUBMIT: { label: '员工信息提交', description: '提交员工信息变更', icon: 'edit' },
|
||||
CONFIRM: { label: '员工转正', description: '试用期员工转正', icon: 'check-circle' },
|
||||
CHANGE: { label: '合同变更', description: '变更合同内容', icon: 'refresh-cw' },
|
||||
RENEW: { label: '合同续签', description: '到期合同续签', icon: 'repeat' },
|
||||
SUSPEND: { label: '合同中止', description: '中止履行合同', icon: 'pause' },
|
||||
INCOME_CERT: { label: '开具收入证明', description: '为员工开具收入证明', icon: 'file-text' },
|
||||
TERMINATE: { label: '合同终止', description: '合同到期终止', icon: 'x-circle' },
|
||||
RESCIND: { label: '合同解除', description: '协商或单方解除合同', icon: 'user-x' },
|
||||
LEAVING_CERT: { label: '开具离职证明', description: '为离职员工开具证明', icon: 'file-minus' },
|
||||
FLEXIBLE: { label: '灵活用工', description: '灵活用工协议签署', icon: 'briefcase' },
|
||||
}
|
||||
|
||||
export const PROCESS_STATUS: Record<string, { label: string; color: string }> = {
|
||||
DRAFT: { label: '草稿', color: 'bg-gray-100 text-gray-600' },
|
||||
PENDING_APPROVAL: { label: '待审批', color: 'bg-amber-50 text-amber-700' },
|
||||
APPROVED: { label: '已审批', color: 'bg-blue-50 text-blue-700' },
|
||||
REJECTED: { label: '已驳回', color: 'bg-red-50 text-red-700' },
|
||||
EXECUTING: { label: '执行中', color: 'bg-purple-50 text-purple-700' },
|
||||
COMPLETED: { label: '已完成', color: 'bg-green-50 text-safe' },
|
||||
CANCELLED: { label: '已撤销', color: 'bg-gray-100 text-gray-400' },
|
||||
}
|
||||
|
||||
// 提交后业务联动
|
||||
export async function executeWorkProcess(processId: string, type: string, formData: any, orgId: string, userId: string) {
|
||||
switch (type) {
|
||||
case 'HIRE': {
|
||||
// 创建员工 + 合同
|
||||
const { name, department, hireDate, monthlySalary, phone, idCardNumber, gender, contractStartDate, contractEndDate, contractType = 'FIXED' } = formData
|
||||
const idCardHash = idCardNumber ? crypto.createHash('sha256').update(idCardNumber).digest('hex') : null
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
orgId,
|
||||
name,
|
||||
department: department || '未分配',
|
||||
hireDate: new Date(hireDate),
|
||||
monthlySalary: encrypt(String(monthlySalary || 0)),
|
||||
phone: phone || null,
|
||||
idCardNumber: idCardNumber ? encrypt(idCardNumber) : null,
|
||||
idCardHash,
|
||||
gender: gender || null,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
if (contractStartDate) {
|
||||
await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
startDate: new Date(contractStartDate),
|
||||
endDate: contractEndDate ? new Date(contractEndDate) : null,
|
||||
contractType: contractType as any,
|
||||
signMethod: 'PAPER',
|
||||
contractYears: 3,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
return { employeeId: employee.id }
|
||||
}
|
||||
case 'ONBOARD': {
|
||||
const { employeeId, hireDate } = formData
|
||||
if (employeeId) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { hireDate: new Date(hireDate), status: 'ACTIVE' },
|
||||
})
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'CONFIRM': {
|
||||
const { employeeId, confirmDate, regularSalary } = formData
|
||||
if (employeeId) {
|
||||
if (regularSalary) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { monthlySalary: encrypt(String(regularSalary)) },
|
||||
})
|
||||
}
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'RENEW': {
|
||||
const { employeeId, oldContractId, newStartDate, newEndDate, newSalary, contractType = 'FIXED', contractYears = 3 } = formData
|
||||
if (oldContractId) {
|
||||
const oldEndDate = new Date(newStartDate)
|
||||
oldEndDate.setDate(oldEndDate.getDate() - 1)
|
||||
await prisma.laborContract.update({
|
||||
where: { id: oldContractId },
|
||||
data: { endDate: oldEndDate },
|
||||
})
|
||||
}
|
||||
if (employeeId) {
|
||||
const oldContract = oldContractId ? await prisma.laborContract.findUnique({ where: { id: oldContractId } }) : null
|
||||
const contract = await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
signDate: new Date(),
|
||||
startDate: new Date(newStartDate),
|
||||
endDate: newEndDate ? new Date(newEndDate) : null,
|
||||
contractType: contractType as any,
|
||||
signMethod: 'PAPER',
|
||||
contractYears: Number(contractYears) || 3,
|
||||
renewalCount: (oldContract?.renewalCount || 0) + 1,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
if (newSalary) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { monthlySalary: encrypt(String(newSalary)) },
|
||||
})
|
||||
}
|
||||
return { employeeId, newContractId: contract.id }
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'TERMINATE': {
|
||||
const { employeeId, contractId, terminateDate } = formData
|
||||
if (contractId) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(terminateDate) },
|
||||
})
|
||||
}
|
||||
if (employeeId) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { status: 'RESIGNED' },
|
||||
})
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'RESCIND': {
|
||||
const { employeeId, contractId, rescindDate } = formData
|
||||
if (contractId) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(rescindDate) },
|
||||
})
|
||||
}
|
||||
if (employeeId) {
|
||||
await prisma.employee.update({
|
||||
where: { id: employeeId },
|
||||
data: { status: 'RESIGNED' },
|
||||
})
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'CHANGE': {
|
||||
const { contractId, newEndDate } = formData
|
||||
if (contractId && newEndDate) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(newEndDate) },
|
||||
})
|
||||
}
|
||||
return { contractId }
|
||||
}
|
||||
case 'SUSPEND': {
|
||||
const { contractId, suspendDate } = formData
|
||||
if (contractId && suspendDate) {
|
||||
await prisma.laborContract.update({
|
||||
where: { id: contractId },
|
||||
data: { endDate: new Date(suspendDate) },
|
||||
})
|
||||
}
|
||||
return { contractId }
|
||||
}
|
||||
case 'CUSTOM_CONTRACT': {
|
||||
const { employeeId, contractStartDate, contractEndDate, contractType = 'FIXED', signMethod = 'PAPER', contractYears = 3 } = formData
|
||||
if (employeeId) {
|
||||
const contract = await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
signDate: new Date(),
|
||||
startDate: new Date(contractStartDate),
|
||||
endDate: contractEndDate ? new Date(contractEndDate) : null,
|
||||
contractType: contractType as any,
|
||||
signMethod: signMethod as any,
|
||||
contractYears: Number(contractYears) || 3,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
return { employeeId, newContractId: contract.id }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
case 'FLEXIBLE': {
|
||||
const { name, phone, idCardNumber, department, agreementStartDate, agreementEndDate, payMethod } = formData
|
||||
const idCardHash = idCardNumber ? crypto.createHash('sha256').update(idCardNumber).digest('hex') : null
|
||||
const employee = await prisma.employee.create({
|
||||
data: {
|
||||
orgId,
|
||||
name,
|
||||
department: department || '灵活用工',
|
||||
hireDate: new Date(agreementStartDate),
|
||||
monthlySalary: encrypt('0'),
|
||||
phone: phone || null,
|
||||
idCardNumber: idCardNumber ? encrypt(idCardNumber) : null,
|
||||
idCardHash,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
const contract = await prisma.laborContract.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: employee.id,
|
||||
signDate: new Date(),
|
||||
startDate: new Date(agreementStartDate),
|
||||
endDate: agreementEndDate ? new Date(agreementEndDate) : null,
|
||||
contractType: 'LABOR',
|
||||
signMethod: 'PAPER',
|
||||
contractYears: 1,
|
||||
createdBy: userId,
|
||||
},
|
||||
})
|
||||
return { employeeId: employee.id, newContractId: contract.id }
|
||||
}
|
||||
case 'INFO_SUBMIT': {
|
||||
const { employeeId, ...updateFields } = formData
|
||||
if (employeeId) {
|
||||
const allowedFields: Record<string, any> = {}
|
||||
if (updateFields.department) allowedFields.department = updateFields.department
|
||||
if (updateFields.phone) allowedFields.phone = updateFields.phone
|
||||
if (updateFields.address) allowedFields.address = updateFields.address
|
||||
if (updateFields.emergencyContact) allowedFields.emergencyContact = updateFields.emergencyContact
|
||||
if (updateFields.emergencyPhone) allowedFields.emergencyPhone = updateFields.emergencyPhone
|
||||
if (updateFields.bankAccount) allowedFields.bankAccount = encrypt(updateFields.bankAccount)
|
||||
if (updateFields.bankName) allowedFields.bankName = updateFields.bankName
|
||||
if (Object.keys(allowedFields).length > 0) {
|
||||
await prisma.employee.update({ where: { id: employeeId }, data: allowedFields })
|
||||
}
|
||||
}
|
||||
return { employeeId }
|
||||
}
|
||||
case 'INCOME_CERT':
|
||||
case 'LEAVING_CERT': {
|
||||
// 这两类只生成文书,不改变业务数据
|
||||
return {}
|
||||
}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成文书预览
|
||||
export function generateDocument(type: string, formData: any, orgName: string): { name: string; content: string } {
|
||||
const templates: Record<string, (data: any, org: string) => string> = {
|
||||
INCOME_CERT: (data, org) => `收入证明
|
||||
|
||||
兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})系我单位员工,自 ${data.hireDate || '___'} 起在我单位工作,现任 ${data.position || '___'} 职务。
|
||||
|
||||
该员工近一年平均月收入为人民币 ${data.monthlyIncome || '___'} 元(税前)。
|
||||
|
||||
本证明仅用于 ${data.purpose || '___'},不作其他用途。
|
||||
|
||||
特此证明。
|
||||
|
||||
${org}
|
||||
${new Date().toLocaleDateString('zh-CN')}`,
|
||||
LEAVING_CERT: (data, org) => `离职证明
|
||||
|
||||
兹证明 ${data.employeeName || '___'}(身份证号:${data.idCardNumber || '___'})自 ${data.hireDate || '___'} 至 ${data.leaveDate || '___'} 在我单位工作,最后职务为 ${data.position || '___'}。
|
||||
|
||||
该员工已于 ${data.leaveDate || '___'} 与我单位解除劳动关系,双方已办妥交接手续。
|
||||
|
||||
特此证明。
|
||||
|
||||
${org}
|
||||
${new Date().toLocaleDateString('zh-CN')}`,
|
||||
}
|
||||
const generator = templates[type]
|
||||
if (!generator) return { name: '', content: '' }
|
||||
return { name: `${PROCESS_TYPES[type]?.label || '文书'}.doc`, content: generator(formData, orgName) }
|
||||
}
|
||||
Reference in New Issue
Block a user