feat: 区分待解聘/待离职+撤回功能
1. 花名册列表区分「待解聘」(amber)和「待离职」(blue) 2. 未到日期的解聘/离职记录可撤回,撤回后删除记录并恢复在职状态 3. 后端新增 DELETE /termination/:id/revoke 接口 4. 花名册列表返回 latestTerminationType 和 latestTerminationId
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user