feat: 区分待解聘/待离职+撤回功能

1. 花名册列表区分「待解聘」(amber)和「待离职」(blue)
2. 未到日期的解聘/离职记录可撤回,撤回后删除记录并恢复在职状态
3. 后端新增 DELETE /termination/:id/revoke 接口
4. 花名册列表返回 latestTerminationType 和 latestTerminationId
This commit is contained in:
freedakgmail
2026-07-23 18:07:37 +08:00
parent 29ba3a98bc
commit 2896ba77aa
4 changed files with 69 additions and 2 deletions
+2
View File
@@ -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,
+17 -1
View File
@@ -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
@@ -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
+24 -1
View File
@@ -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() {
</button>
)}
{e.hasTermination && e.status === 'ACTIVE' && (
<span className="text-xs text-gray-400"></span>
<div className="flex items-center justify-center gap-2">
<span className={`text-xs ${e.latestTerminationType === 'RESIGNATION' ? 'text-blue-600' : 'text-amber-600'}`}>
{e.latestTerminationType === 'RESIGNATION' ? '待离职' : '待解聘'}
</span>
<button
className="text-xs text-gray-400 hover:text-danger"
onClick={(ev) => {
ev.stopPropagation()
if (e.latestTerminationId && confirm(`确认撤回${e.latestTerminationType === 'RESIGNATION' ? '离职' : '解聘'}记录?`)) {
revokeMutation.mutate(e.latestTerminationId)
}
}}
>
</button>
</div>
)}
</td>
</tr>