diff --git a/backend/prisma/migration_add_three_tables.sql b/backend/prisma/migration_add_three_tables.sql new file mode 100644 index 0000000..35e86e2 --- /dev/null +++ b/backend/prisma/migration_add_three_tables.sql @@ -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"); diff --git a/backend/src/routes/enterprise-template.routes.ts b/backend/src/routes/enterprise-template.routes.ts index 4b4441a..74b0dd2 100644 --- a/backend/src/routes/enterprise-template.routes.ts +++ b/backend/src/routes/enterprise-template.routes.ts @@ -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) } diff --git a/backend/src/routes/policy.routes.ts b/backend/src/routes/policy.routes.ts index c066b27..59b83c3 100644 --- a/backend/src/routes/policy.routes.ts +++ b/backend/src/routes/policy.routes.ts @@ -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) } diff --git a/backend/src/routes/termination.routes.ts b/backend/src/routes/termination.routes.ts index 8c736ee..ae029f0 100644 --- a/backend/src/routes/termination.routes.ts +++ b/backend/src/routes/termination.routes.ts @@ -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) diff --git a/backend/src/services/policy.service.ts b/backend/src/services/policy.service.ts index 4a4fffb..644d1a4 100644 --- a/backend/src/services/policy.service.ts +++ b/backend/src/services/policy.service.ts @@ -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 } /** diff --git a/backend/src/services/termination.service.ts b/backend/src/services/termination.service.ts index b3f946d..a58c58b 100644 --- a/backend/src/services/termination.service.ts +++ b/backend/src/services/termination.service.ts @@ -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 } /** 获取单条记录详情(含所有流程字段) */ diff --git a/backend/src/services/work-process.service.ts b/backend/src/services/work-process.service.ts index 72bcdf5..db7133d 100644 --- a/backend/src/services/work-process.service.ts +++ b/backend/src/services/work-process.service.ts @@ -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) } diff --git a/frontend/src/pages/Contracts.tsx b/frontend/src/pages/Contracts.tsx index 9f4e5f9..6d67264 100644 --- a/frontend/src/pages/Contracts.tsx +++ b/frontend/src/pages/Contracts.tsx @@ -8,6 +8,7 @@ import Button from '../components/ui/Button' import { Input, Label, Select } from '../components/ui/Input' import Modal from '../components/ui/Modal' import EmptyState from '../components/ui/EmptyState' +import Pagination from '../components/ui/Pagination' interface EmployeeItem { id: string @@ -37,13 +38,14 @@ export default function Contracts() { const [filterDepartment, setFilterDepartment] = useState('') const [filterContractStatus, setFilterContractStatus] = useState('') const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(20) const [showAddModal, setShowAddModal] = useState(false) const [selectedEmpId, setSelectedEmpId] = useState(null) const { data, isLoading } = useQuery({ - queryKey: ['employees', search, filterDepartment, filterContractStatus, page], + queryKey: ['employees', search, filterDepartment, filterContractStatus, page, pageSize], queryFn: async () => { - const params: any = { search, page, pageSize: 20 } + const params: any = { search, page, pageSize } if (filterDepartment) params.department = filterDepartment if (filterContractStatus) params.contractStatus = filterContractStatus const res = await api.get('/roster', { params }) as any @@ -181,23 +183,13 @@ export default function Contracts() { {/* 分页 */} - {data.totalPages > 1 && ( -
- - {page} / {data.totalPages} - -
- )} + { setPageSize(s); setPage(1) }} + /> )} diff --git a/frontend/src/pages/Evidence.tsx b/frontend/src/pages/Evidence.tsx index d2a43dd..033ee07 100644 --- a/frontend/src/pages/Evidence.tsx +++ b/frontend/src/pages/Evidence.tsx @@ -4,21 +4,27 @@ import { ShieldCheck, FileText, AlertCircle, CheckCircle, XCircle } from 'lucide import api from '../lib/api' import Card from '../components/ui/Card' import EmptyState from '../components/ui/EmptyState' +import Pagination from '../components/ui/Pagination' /** * 证据链管理页面 */ export default function Evidence() { const [refType, setRefType] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(20) - const { data: list, isLoading } = useQuery({ - queryKey: ['evidence', refType], + const { data: listData, isLoading } = useQuery({ + queryKey: ['evidence', refType, page, pageSize], queryFn: async () => { - const params = refType ? `?category=${refType}` : '?category=ALL' - const res = await api.get(`/evidence${params}`) as any - return res.data?.records || [] + const params: any = { page, pageSize } + params.category = refType || 'ALL' + const res = await api.get('/evidence', { params }) as any + return res.data }, }) + const list = listData?.records || [] + const total = listData?.total || 0 const { data: verifyResult, refetch: verifyAll } = useQuery({ queryKey: ['evidence-verify-all'], @@ -68,7 +74,7 @@ export default function Evidence() { {['', 'ONBOARD', 'CONTRACT_SIGN', 'PAYSLIP_CONFIRM', 'DISCIPLINARY', 'ATTENDANCE', 'TERMINATION'].map((t) => (