feat: 全部列表页统一分页 + WorkProcess集成现有服务

- WorkProcess/Termination/Evidence/Contracts/Policies/Templates 添加 Pagination 组件
- 后端 getDrafts/getPolicies/enterprise-template 路由添加分页支持(可选参数,向后兼容)
- work-process.service CONFIRM case 修正:移除不存在的 probationEndDate 字段
- 新增 migration_add_three_tables.sql 增量迁移文件
This commit is contained in:
freedakgmail
2026-07-30 10:39:21 +08:00
parent cea7eb07c4
commit 5a5ce7186b
13 changed files with 221 additions and 63 deletions
@@ -0,0 +1,84 @@
-- 增量迁移:添加 WorkProcess、EnterpriseTemplate、AttendancePublish 三张表
-- 不影响现有数据,仅 CREATE TABLE IF NOT EXISTS
-- 执行方式: PGPASSWORD=turbohr2026 psql -U turbohr -d turbohr -f migration_add_three_tables.sql
-- 1. WorkProcess 用工办理工作流
CREATE TABLE IF NOT EXISTS "WorkProcess" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"title" TEXT NOT NULL,
"employeeId" TEXT,
"status" TEXT NOT NULL DEFAULT 'DRAFT',
"formData" JSONB NOT NULL,
"documents" JSONB,
"approverId" TEXT,
"approvedAt" TIMESTAMP(3),
"remark" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WorkProcess_pkey" PRIMARY KEY ("id")
);
-- 外键:orgId -> Organization
ALTER TABLE "WorkProcess"
ADD CONSTRAINT "WorkProcess_orgId_fkey"
FOREIGN KEY ("orgId") REFERENCES "Organization"("id") ON DELETE CASCADE;
-- 外键:employeeId -> Employee (ON DELETE SET NULL)
ALTER TABLE "WorkProcess"
ADD CONSTRAINT "WorkProcess_employeeId_fkey"
FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE SET NULL;
-- 索引
CREATE INDEX IF NOT EXISTS "WorkProcess_orgId_status_idx" ON "WorkProcess"("orgId", "status");
CREATE INDEX IF NOT EXISTS "WorkProcess_orgId_type_idx" ON "WorkProcess"("orgId", "type");
CREATE INDEX IF NOT EXISTS "WorkProcess_orgId_createdBy_idx" ON "WorkProcess"("orgId", "createdBy");
-- 2. EnterpriseTemplate 企业自建文本模板
CREATE TABLE IF NOT EXISTS "EnterpriseTemplate" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"category" TEXT NOT NULL,
"description" TEXT,
"content" TEXT NOT NULL,
"variables" TEXT[] NOT NULL DEFAULT '{}',
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EnterpriseTemplate_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "EnterpriseTemplate"
ADD CONSTRAINT "EnterpriseTemplate_orgId_fkey"
FOREIGN KEY ("orgId") REFERENCES "Organization"("id") ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS "EnterpriseTemplate_orgId_category_idx" ON "EnterpriseTemplate"("orgId", "category");
-- 3. AttendancePublish 考勤发布记录
CREATE TABLE IF NOT EXISTS "AttendancePublish" (
"id" TEXT NOT NULL,
"orgId" TEXT NOT NULL,
"month" TEXT NOT NULL,
"title" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PUBLISHED',
"publishDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AttendancePublish_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "AttendancePublish"
ADD CONSTRAINT "AttendancePublish_orgId_fkey"
FOREIGN KEY ("orgId") REFERENCES "Organization"("id") ON DELETE CASCADE;
-- 唯一约束:同一组织同月只能有一条发布记录
CREATE UNIQUE INDEX IF NOT EXISTS "AttendancePublish_orgId_month_key" ON "AttendancePublish"("orgId", "month");
CREATE INDEX IF NOT EXISTS "AttendancePublish_orgId_month_idx" ON "AttendancePublish"("orgId", "month");
@@ -15,14 +15,21 @@ function extractVariables(content: string): string[] {
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { category, search } = req.query
const page = parseInt(req.query.page as string) || 1
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
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 })
const [items, total] = await Promise.all([
(prisma as any).enterpriseTemplate.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
(prisma as any).enterpriseTemplate.count({ where }),
])
res.json({ success: true, data: { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize) } })
} catch (err) {
next(err)
}
+4 -2
View File
@@ -13,8 +13,10 @@ const router = Router()
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const status = req.query.status as string | undefined
const policies = await getPolicies(req.user!.orgId, status)
res.json({ success: true, data: policies })
const page = parseInt(req.query.page as string) || 1
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
const result = await getPolicies(req.user!.orgId, status, page, pageSize)
res.json({ success: true, data: result })
} catch (err) {
next(err)
}
+3 -1
View File
@@ -162,7 +162,9 @@ router.get('/drafts', authMiddleware, async (req: AuthRequest, res, next) => {
const status = req.query.status as string | undefined
const search = req.query.search as string | undefined
const department = req.query.department as string | undefined
const result = await getDrafts(req.user!.orgId, status, search, department)
const page = parseInt(req.query.page as string) || 1
const pageSize = Math.min(parseInt(req.query.pageSize as string) || 20, 200)
const result = await getDrafts(req.user!.orgId, status, search, department, page, pageSize)
res.json({ success: true, data: result })
} catch (err) {
next(err)
+10 -3
View File
@@ -73,24 +73,31 @@ export async function createPolicy(orgId: string, userId: string, data: { title:
/**
* 获取制度列表(含阅读签收统计)
*/
export async function getPolicies(orgId: string, status?: string) {
export async function getPolicies(orgId: string, status?: string, page?: number, pageSize?: number) {
const where: any = { orgId }
if (status) where.status = status
const [policies, totalEmployees] = await Promise.all([
const hasPagination = page && pageSize
const [policies, totalEmployees, total] = await Promise.all([
prisma.policyDocument.findMany({
where,
orderBy: { updatedAt: 'desc' },
include: {
_count: { select: { readRecords: true } },
},
...(hasPagination ? { skip: (page! - 1) * pageSize!, take: pageSize! } : {}),
}),
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
hasPagination ? prisma.policyDocument.count({ where }) : Promise.resolve(0),
])
return policies.map(p => ({
const items = policies.map(p => ({
...p,
readCount: p._count?.readRecords || 0,
totalEmployees,
}))
if (hasPagination) {
return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize!) }
}
return items
}
/**
+17 -7
View File
@@ -738,7 +738,7 @@ export async function cancelTermination(orgId: string, recordId: string, userId:
}
/** 获取草稿列表 */
export async function getDrafts(orgId: string, status?: string, search?: string, department?: string) {
export async function getDrafts(orgId: string, status?: string, search?: string, department?: string, page?: number, pageSize?: number) {
const where: any = { orgId }
if (status) {
where.status = status
@@ -758,13 +758,18 @@ export async function getDrafts(orgId: string, status?: string, search?: string,
}
}
const records = await prisma.terminationRecord.findMany({
where,
include: { employee: true },
orderBy: { updatedAt: 'desc' },
})
const hasPagination = page && pageSize
const [records, total] = await Promise.all([
prisma.terminationRecord.findMany({
where,
include: { employee: true },
orderBy: { updatedAt: 'desc' },
...(hasPagination ? { skip: (page! - 1) * pageSize!, take: pageSize! } : {}),
}),
hasPagination ? prisma.terminationRecord.count({ where }) : Promise.resolve(0),
])
return records.map((r) => ({
const items = records.map((r) => ({
id: r.id,
employeeId: r.employeeId,
employeeName: r.employee.name,
@@ -780,6 +785,11 @@ export async function getDrafts(orgId: string, status?: string, search?: string,
createdAt: r.createdAt.toISOString().slice(0, 10),
updatedAt: r.updatedAt.toISOString().slice(0, 10),
}))
if (hasPagination) {
return { items, total, page, pageSize, totalPages: Math.ceil(total / pageSize!) }
}
return items
}
/** 获取单条记录详情(含所有流程字段) */
+1 -8
View File
@@ -69,17 +69,10 @@ export async function executeWorkProcess(processId: string, type: string, formDa
case 'CONFIRM': {
const { employeeId, confirmDate, regularSalary } = formData
if (employeeId) {
const updateData: any = {}
if (regularSalary) {
updateData.monthlySalary = encrypt(String(regularSalary))
}
if (confirmDate) {
updateData.probationEndDate = new Date(confirmDate)
}
if (Object.keys(updateData).length > 0) {
await prisma.employee.update({
where: { id: employeeId },
data: updateData,
data: { monthlySalary: encrypt(String(regularSalary)) },
})
await runRiskDetection(orgId)
}