From 2896ba77aaa94a830d909bbc0b6da555b1d54d53 Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Thu, 23 Jul 2026 18:07:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8C=BA=E5=88=86=E5=BE=85=E8=A7=A3?= =?UTF-8?q?=E8=81=98/=E5=BE=85=E7=A6=BB=E8=81=8C+=E6=92=A4=E5=9B=9E?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 花名册列表区分「待解聘」(amber)和「待离职」(blue) 2. 未到日期的解聘/离职记录可撤回,撤回后删除记录并恢复在职状态 3. 后端新增 DELETE /termination/:id/revoke 接口 4. 花名册列表返回 latestTerminationType 和 latestTerminationId --- backend/src/routes/roster.routes.ts | 2 ++ backend/src/routes/termination.routes.ts | 18 +++++++++++++- backend/src/services/termination.service.ts | 26 +++++++++++++++++++++ frontend/src/pages/Roster.tsx | 25 +++++++++++++++++++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/roster.routes.ts b/backend/src/routes/roster.routes.ts index 499fce0..9050255 100644 --- a/backend/src/routes/roster.routes.ts +++ b/backend/src/routes/roster.routes.ts @@ -65,6 +65,8 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => { status: e.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE', hasTermination: e.terminations.length > 0, latestTerminationDate: e.terminations[0]?.terminationDate || null, + latestTerminationType: e.terminations[0]?.type || null, + latestTerminationId: e.terminations[0]?.id || null, hireDate: e.hireDate, gender: e.gender, phone: e.phone, diff --git a/backend/src/routes/termination.routes.ts b/backend/src/routes/termination.routes.ts index c74d8b6..06982ba 100644 --- a/backend/src/routes/termination.routes.ts +++ b/backend/src/routes/termination.routes.ts @@ -2,7 +2,7 @@ import { Router } from 'express' import { authMiddleware, AuthRequest } from '../middleware/auth' import { auditLog } from '../middleware/auditLog' import { terminationChecklistSchema } from '../schemas/termination.schema' -import { createTermination, createResignation, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service' +import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service' import prisma from '../lib/prisma' import { decrypt } from '../lib/crypto' @@ -87,4 +87,20 @@ router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) } }) +router.delete('/:id/revoke', authMiddleware, async (req: AuthRequest, res, next) => { + try { + const result = await revokeTermination(req.user!.orgId, req.params.id) + await auditLog(req, 'REVOKE_TERMINATION', 'TERMINATION_RECORD', req.params.id, {}) + res.json({ success: true, data: result }) + } catch (err: any) { + if (err?.code === 'CONFLICT') { + return res.status(409).json({ success: false, error: { code: err.code, message: err.message } }) + } + if (err?.code === 'NOT_FOUND') { + return res.status(404).json({ success: false, error: { code: err.code, message: err.message } }) + } + next(err) + } +}) + export default router diff --git a/backend/src/services/termination.service.ts b/backend/src/services/termination.service.ts index 61578ba..70a00b8 100644 --- a/backend/src/services/termination.service.ts +++ b/backend/src/services/termination.service.ts @@ -237,6 +237,32 @@ export async function createResignation(orgId: string, userId: string, data: any return { id: record.id } } +// 撤回离职/解聘(仅未到日期可撤回) +export async function revokeTermination(orgId: string, recordId: string) { + const record = await prisma.terminationRecord.findFirst({ + where: { id: recordId, orgId }, + }) + if (!record) { + throw { code: 'NOT_FOUND', message: '离职/解聘记录不存在' } + } + + const today = new Date() + today.setHours(0, 0, 0, 0) + if (record.terminationDate <= today) { + throw { code: 'CONFLICT', message: '离职/解聘日期已到或已过,无法撤回' } + } + + await prisma.terminationRecord.delete({ where: { id: recordId } }) + + // 恢复员工状态为 ACTIVE + await prisma.employee.update({ + where: { id: record.employeeId }, + data: { status: 'ACTIVE' }, + }) + + return { id: recordId } +} + export async function getTerminations(orgId: string, page: number, pageSize: number) { const skip = (page - 1) * pageSize diff --git a/frontend/src/pages/Roster.tsx b/frontend/src/pages/Roster.tsx index 7b70377..af88da9 100644 --- a/frontend/src/pages/Roster.tsx +++ b/frontend/src/pages/Roster.tsx @@ -51,6 +51,14 @@ export default function Roster() { }, }) + const revokeMutation = useMutation({ + mutationFn: (recordId: string) => api.delete(`/termination/${recordId}/revoke`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['roster'] }) + queryClient.invalidateQueries({ queryKey: ['dashboard'] }) + }, + }) + const filtered = employees?.filter((e: any) => !search || e.name.includes(search) || e.department.includes(search) ) || [] @@ -156,7 +164,22 @@ export default function Roster() { )} {e.hasTermination && e.status === 'ACTIVE' && ( - 待离职 +
+ + {e.latestTerminationType === 'RESIGNATION' ? '待离职' : '待解聘'} + + +
)}