fix: 修复已撤销解聘记录(CANCELLED)在各模块中未正确过滤的问题

- 花名册API新增latestTerminationStatus字段,前端列表/操作按钮/离职日期列过滤CANCELLED/COMPLETED
- 证据链后端过滤CANCELLED状态的解聘记录,不再产生无效证据和风险提醒
- 重新入职判断增加status=COMPLETED条件,避免已撤销记录被误判为已离职
- 薪酬警告过滤CANCELLED状态,不再对已撤销记录触发结算提醒
- 风险扫描查询条件增加status=COMPLETED,避免已撤销记录导致员工被跳过
- 解聘向导canProceed和警告提示过滤CANCELLED/COMPLETED状态
- 档案解聘记录列表过滤CANCELLED状态,新增流程状态标签
- 重新生成Prisma Client以同步femaleWorkerType字段
This commit is contained in:
selfrelease
2026-07-25 20:10:42 +08:00
parent 41a2c665ed
commit 7ca2ada0d4
6 changed files with 20 additions and 12 deletions
+3 -2
View File
@@ -118,6 +118,7 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
latestTerminationDate: e.terminations[0]?.terminationDate || null, latestTerminationDate: e.terminations[0]?.terminationDate || null,
latestTerminationType: e.terminations[0]?.type || null, latestTerminationType: e.terminations[0]?.type || null,
latestTerminationId: e.terminations[0]?.id || null, latestTerminationId: e.terminations[0]?.id || null,
latestTerminationStatus: e.terminations[0]?.status || null,
hireDate: e.hireDate, hireDate: e.hireDate,
gender: e.gender, gender: e.gender,
phone: e.phone, phone: e.phone,
@@ -389,8 +390,8 @@ router.get('/:id/evidence-chain', authMiddleware, async (req: AuthRequest, res,
}) })
}) })
// 7. 解聘证据 // 7. 解聘证据(排除已撤销的记录)
employee.terminations.forEach((t) => { employee.terminations.filter((t) => t.status !== 'CANCELLED').forEach((t) => {
const reasonMap: Record<string, string> = { NEGOTIATED: '协商一致', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', RESIGNATION: '员工主动离职' } const reasonMap: Record<string, string> = { NEGOTIATED: '协商一致', FAULT: '员工过错', NONFAULT: '非过错解除', LAYOFF: '经济性裁员', EXPIRED: '合同到期', RESIGNATION: '员工主动离职' }
const typeLabel = t.type === 'RESIGNATION' && t.reason === 'NEGOTIATED' const typeLabel = t.type === 'RESIGNATION' && t.reason === 'NEGOTIATED'
? '协商一致离职' ? '协商一致离职'
+1 -1
View File
@@ -315,7 +315,7 @@ export async function rehireEmployee(orgId: string, userId: string, id: string,
const today = new Date() const today = new Date()
today.setHours(0, 0, 0, 0) today.setHours(0, 0, 0, 0)
const isResigned = employee.terminations.some((t) => t.terminationDate <= today) const isResigned = employee.terminations.some((t) => t.status === 'COMPLETED' && t.terminationDate <= today)
if (!isResigned) { if (!isResigned) {
throw { code: 'CONFLICT', message: '该员工当前在职,无需重新入职' } throw { code: 'CONFLICT', message: '该员工当前在职,无需重新入职' }
} }
+1 -1
View File
@@ -236,7 +236,7 @@ export async function getPayrollRiskWarnings(orgId: string, employeeId: string):
if (!employee.housingFundBase) { if (!employee.housingFundBase) {
warnings.push('未设置公积金缴费基数') warnings.push('未设置公积金缴费基数')
} }
if (employee.terminations.length) { if (employee.terminations.some((t) => t.status !== 'CANCELLED')) {
warnings.push('已有解聘记录,请注意结算') warnings.push('已有解聘记录,请注意结算')
} }
+1 -1
View File
@@ -107,7 +107,7 @@ export async function detectOnboardingRisks(orgId: string) {
where: { orgId, status: 'ACTIVE', hireDate: { lte: today } }, where: { orgId, status: 'ACTIVE', hireDate: { lte: today } },
include: { include: {
contracts: { orderBy: { createdAt: 'desc' }, take: 1 }, contracts: { orderBy: { createdAt: 'desc' }, take: 1 },
terminations: { where: { terminationDate: { lte: today } }, take: 1 }, terminations: { where: { terminationDate: { lte: today }, status: 'COMPLETED' }, take: 1 },
}, },
}) })
+11 -5
View File
@@ -346,7 +346,7 @@ export default function Roster() {
</td> </td>
<td className="hidden px-4 py-3 text-gray-500">{e.hireDate?.toString().slice(0, 10)}</td> <td className="hidden px-4 py-3 text-gray-500">{e.hireDate?.toString().slice(0, 10)}</td>
<td className="hidden px-4 py-3 text-gray-500"> <td className="hidden px-4 py-3 text-gray-500">
{e.hasTermination && e.latestTerminationDate ? ( {e.hasTermination && e.latestTerminationDate && e.latestTerminationStatus !== 'CANCELLED' && e.latestTerminationStatus !== 'COMPLETED' ? (
<span className={e.status === 'RESIGNED' ? 'text-gray-500' : 'text-amber-600'}> <span className={e.status === 'RESIGNED' ? 'text-gray-500' : 'text-amber-600'}>
{e.latestTerminationDate.toString().slice(0, 10)} {e.latestTerminationDate.toString().slice(0, 10)}
{e.status === 'ACTIVE' && ' (预计)'} {e.status === 'ACTIVE' && ' (预计)'}
@@ -425,7 +425,7 @@ export default function Roster() {
<td className="px-4 py-3 text-center text-gray-500">{e.counts?.performanceRecords || 0}</td> <td className="px-4 py-3 text-center text-gray-500">{e.counts?.performanceRecords || 0}</td>
<td className="px-4 py-3 text-center text-gray-500">{e.counts?.payslips || 0}</td> <td className="px-4 py-3 text-center text-gray-500">{e.counts?.payslips || 0}</td>
<td className="px-4 py-3 text-center"> <td className="px-4 py-3 text-center">
{e.status === 'ACTIVE' && !e.hasTermination && ( {e.status === 'ACTIVE' && (!e.hasTermination || e.latestTerminationStatus === 'CANCELLED' || e.latestTerminationStatus === 'COMPLETED') && (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<button <button
type="button" type="button"
@@ -468,7 +468,7 @@ export default function Roster() {
</button> </button>
</div> </div>
)} )}
{e.hasTermination && e.status === 'ACTIVE' && ( {e.hasTermination && e.status === 'ACTIVE' && e.latestTerminationStatus !== 'CANCELLED' && e.latestTerminationStatus !== 'COMPLETED' && (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<span className={`text-xs ${e.latestTerminationType === 'RESIGNATION' ? 'text-blue-600' : 'text-amber-600'}`}> <span className={`text-xs ${e.latestTerminationType === 'RESIGNATION' ? 'text-blue-600' : 'text-amber-600'}`}>
{e.latestTerminationType === 'RESIGNATION' ? '待离职' : '待解聘'} {e.latestTerminationType === 'RESIGNATION' ? '待离职' : '待解聘'}
@@ -2294,7 +2294,8 @@ function TerminationInfo({ employeeId, profile, records }: { employeeId: string;
enabled: !!printRecord, enabled: !!printRecord,
}) })
if (!records?.length) return <Card><div className="text-center py-8 text-gray-400">/</div></Card> const validRecords = (records || []).filter((t: any) => t.status !== 'CANCELLED')
if (!validRecords.length) return <Card><div className="text-center py-8 text-gray-400">/</div></Card>
if (printRecord) { if (printRecord) {
return ( return (
@@ -2412,7 +2413,7 @@ function TerminationInfo({ employeeId, profile, records }: { employeeId: string;
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{records.map((t) => ( {validRecords.map((t) => (
<Card key={t.id} className="p-4"> <Card key={t.id} className="p-4">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
@@ -2424,6 +2425,11 @@ function TerminationInfo({ employeeId, profile, records }: { employeeId: string;
{t.riskLevel === 'SAFE' ? '风险低' : t.riskLevel === 'WARNING' ? '注意' : '高风险'} {t.riskLevel === 'SAFE' ? '风险低' : t.riskLevel === 'WARNING' ? '注意' : '高风险'}
</span> </span>
)} )}
{t.status && (
<span className={`px-2 py-0.5 rounded text-xs ${t.status === 'COMPLETED' ? 'bg-green-50 text-safe' : t.status === 'PENDING_APPROVAL' ? 'bg-amber-50 text-amber-700' : t.status === 'APPROVED' ? 'bg-blue-50 text-blue-700' : 'bg-gray-100 text-gray-600'}`}>
{t.status === 'DRAFT' ? '草稿' : t.status === 'PENDING_APPROVAL' ? '待审批' : t.status === 'APPROVED' ? '已审批' : t.status === 'COMPLETED' ? '已完成' : t.status === 'REJECTED' ? '已驳回' : t.status === 'EXECUTING' ? '执行中' : t.status}
</span>
)}
</div> </div>
<div className="grid md:grid-cols-2 gap-3 text-xs"> <div className="grid md:grid-cols-2 gap-3 text-xs">
{t.type === 'RESIGNATION' ? ( {t.type === 'RESIGNATION' ? (
+3 -2
View File
@@ -49,6 +49,7 @@ interface RosterEmployee {
department: string department: string
status: string status: string
hasTermination?: boolean hasTermination?: boolean
latestTerminationStatus?: string
hireDate: string hireDate: string
monthlySalary: number monthlySalary: number
latestContract: any latestContract: any
@@ -417,7 +418,7 @@ export default function Termination() {
}, [selectedEmployee, terminationDate, socialAvgWage, reason]) }, [selectedEmployee, terminationDate, socialAvgWage, reason])
const canProceed = () => { const canProceed = () => {
if (step === 0) return !!employeeId && (!!draftId || !selectedEmployee?.hasTermination) if (step === 0) return !!employeeId && (!!draftId || !selectedEmployee?.hasTermination || selectedEmployee?.latestTerminationStatus === 'CANCELLED' || selectedEmployee?.latestTerminationStatus === 'COMPLETED')
if (step === 1) return !!reason && !!terminationDate && (!riskAssessment?.warnings.length || acknowledgeRisk) if (step === 1) return !!reason && !!terminationDate && (!riskAssessment?.warnings.length || acknowledgeRisk)
if (step === 2) return true if (step === 2) return true
if (step === 3) return true if (step === 3) return true
@@ -921,7 +922,7 @@ export default function Termination() {
<div className="font-medium">{selectedEmployee.name}{selectedEmployee.department}</div> <div className="font-medium">{selectedEmployee.name}{selectedEmployee.department}</div>
<div>{selectedEmployee.hireDate?.toString().slice(0, 10)}</div> <div>{selectedEmployee.hireDate?.toString().slice(0, 10)}</div>
<div>¥{fmt(selectedEmployee.monthlySalary)}</div> <div>¥{fmt(selectedEmployee.monthlySalary)}</div>
{selectedEmployee.hasTermination && ( {selectedEmployee.hasTermination && selectedEmployee.latestTerminationStatus !== 'CANCELLED' && selectedEmployee.latestTerminationStatus !== 'COMPLETED' && (
<div className="text-danger font-medium mt-1"> /</div> <div className="text-danger font-medium mt-1"> /</div>
)} )}
{selectedEmployee.latestContract ? ( {selectedEmployee.latestContract ? (