feat: 离职/解聘完整功能
1. 已有离职/解聘记录的员工不能再次解聘(后端校验+前端提示) 2. 员工主动离职功能:花名册列表增加「离职」操作按钮和弹窗 3. 离职也按离职日期动态判断在职/离职状态,支持提前办理 4. 员工档案「解聘记录」tab改为「离职/解聘记录」,区分类型显示 5. Schema: TerminationRecord 增加 type、resignationReason 字段 6. 后端新增 POST /termination/resignation 接口 7. 花名册列表返回 hasTermination 标记
This commit is contained in:
@@ -83,6 +83,7 @@ enum TerminationReason {
|
|||||||
NONFAULT
|
NONFAULT
|
||||||
LAYOFF
|
LAYOFF
|
||||||
EXPIRED
|
EXPIRED
|
||||||
|
RESIGNATION
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RiskAssessment {
|
enum RiskAssessment {
|
||||||
@@ -249,8 +250,10 @@ model TerminationRecord {
|
|||||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||||
employeeId String
|
employeeId String
|
||||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||||
|
type String @default("TERMINATION") // TERMINATION=公司解聘, RESIGNATION=员工主动离职
|
||||||
reason TerminationReason
|
reason TerminationReason
|
||||||
terminationDate DateTime
|
terminationDate DateTime
|
||||||
|
resignationReason String? // 主动离职原因(type=RESIGNATION时使用)
|
||||||
compensation Float @default(0)
|
compensation Float @default(0)
|
||||||
riskLevel RiskAssessment @default(SAFE)
|
riskLevel RiskAssessment @default(SAFE)
|
||||||
checklist Json
|
checklist Json
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ router.get('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
|||||||
name: e.name,
|
name: e.name,
|
||||||
department: e.department,
|
department: e.department,
|
||||||
status: e.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE',
|
status: e.terminations.some((t) => t.terminationDate <= today) ? 'RESIGNED' : 'ACTIVE',
|
||||||
|
hasTermination: e.terminations.length > 0,
|
||||||
|
latestTerminationDate: e.terminations[0]?.terminationDate || null,
|
||||||
hireDate: e.hireDate,
|
hireDate: e.hireDate,
|
||||||
gender: e.gender,
|
gender: e.gender,
|
||||||
phone: e.phone,
|
phone: e.phone,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Router } from 'express'
|
|||||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||||
import { auditLog } from '../middleware/auditLog'
|
import { auditLog } from '../middleware/auditLog'
|
||||||
import { terminationChecklistSchema } from '../schemas/termination.schema'
|
import { terminationChecklistSchema } from '../schemas/termination.schema'
|
||||||
import { createTermination, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service'
|
import { createTermination, createResignation, getTerminations, getChecklistForReason, assessRisk, calculateCompensation } from '../services/termination.service'
|
||||||
import prisma from '../lib/prisma'
|
import prisma from '../lib/prisma'
|
||||||
import { decrypt } from '../lib/crypto'
|
import { decrypt } from '../lib/crypto'
|
||||||
|
|
||||||
@@ -70,4 +70,21 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||||
|
try {
|
||||||
|
const { employeeId, terminationDate, resignationReason, remark } = req.body
|
||||||
|
if (!employeeId || !terminationDate) {
|
||||||
|
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '缺少必填字段' } })
|
||||||
|
}
|
||||||
|
const result = await createResignation(req.user!.orgId, req.user!.id, { employeeId, terminationDate, resignationReason, remark })
|
||||||
|
await auditLog(req, 'RESIGN', 'EMPLOYEE', employeeId, { resignationReason })
|
||||||
|
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 } })
|
||||||
|
}
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -141,12 +141,22 @@ export async function createTermination(orgId: string, userId: string, data: any
|
|||||||
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 校验:已有离职/解聘记录且未重新雇佣则不允许再次解聘
|
||||||
|
const latestTerm = await prisma.terminationRecord.findFirst({
|
||||||
|
where: { employeeId: data.employeeId },
|
||||||
|
orderBy: { terminationDate: 'desc' },
|
||||||
|
})
|
||||||
|
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
|
||||||
|
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次解聘请先办理重新雇佣' }
|
||||||
|
}
|
||||||
|
|
||||||
const { level } = assessRisk(employee, data.reason)
|
const { level } = assessRisk(employee, data.reason)
|
||||||
|
|
||||||
const record = await prisma.terminationRecord.create({
|
const record = await prisma.terminationRecord.create({
|
||||||
data: {
|
data: {
|
||||||
orgId,
|
orgId,
|
||||||
employeeId: data.employeeId,
|
employeeId: data.employeeId,
|
||||||
|
type: 'TERMINATION',
|
||||||
reason: data.reason,
|
reason: data.reason,
|
||||||
terminationDate: new Date(data.terminationDate),
|
terminationDate: new Date(data.terminationDate),
|
||||||
compensation: data.compensation || 0,
|
compensation: data.compensation || 0,
|
||||||
@@ -176,6 +186,57 @@ export async function createTermination(orgId: string, userId: string, data: any
|
|||||||
return { id: record.id }
|
return { id: record.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 员工主动离职
|
||||||
|
export async function createResignation(orgId: string, userId: string, data: any) {
|
||||||
|
const employee = await prisma.employee.findFirst({ where: { id: data.employeeId, orgId } })
|
||||||
|
if (!employee) {
|
||||||
|
throw { code: 'NOT_FOUND', message: '员工不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验:已有离职/解聘记录且未重新雇佣则不允许再次离职
|
||||||
|
const latestTerm = await prisma.terminationRecord.findFirst({
|
||||||
|
where: { employeeId: data.employeeId },
|
||||||
|
orderBy: { terminationDate: 'desc' },
|
||||||
|
})
|
||||||
|
if (latestTerm && latestTerm.terminationDate >= employee.hireDate) {
|
||||||
|
throw { code: 'CONFLICT', message: '该员工已有离职/解聘记录,如需再次办理请先重新雇佣' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = await prisma.terminationRecord.create({
|
||||||
|
data: {
|
||||||
|
orgId,
|
||||||
|
employeeId: data.employeeId,
|
||||||
|
type: 'RESIGNATION',
|
||||||
|
reason: 'RESIGNATION',
|
||||||
|
terminationDate: new Date(data.terminationDate),
|
||||||
|
resignationReason: data.resignationReason || null,
|
||||||
|
compensation: 0,
|
||||||
|
riskLevel: 'SAFE',
|
||||||
|
checklist: {},
|
||||||
|
remark: data.remark || null,
|
||||||
|
createdBy: userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据离职日期判断在职/离职状态
|
||||||
|
const termDate = new Date(data.terminationDate)
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
const isResigned = termDate <= today
|
||||||
|
|
||||||
|
await prisma.employee.update({
|
||||||
|
where: { id: data.employeeId },
|
||||||
|
data: { status: isResigned ? 'RESIGNED' : 'ACTIVE' },
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.riskItem.updateMany({
|
||||||
|
where: { employeeId: data.employeeId, status: 'PENDING' },
|
||||||
|
data: { status: 'RESOLVED', resolvedAt: new Date() },
|
||||||
|
})
|
||||||
|
|
||||||
|
return { id: record.id }
|
||||||
|
}
|
||||||
|
|
||||||
export async function getTerminations(orgId: string, page: number, pageSize: number) {
|
export async function getTerminations(orgId: string, page: number, pageSize: number) {
|
||||||
const skip = (page - 1) * pageSize
|
const skip = (page - 1) * pageSize
|
||||||
|
|
||||||
@@ -195,10 +256,13 @@ export async function getTerminations(orgId: string, page: number, pageSize: num
|
|||||||
id: r.id,
|
id: r.id,
|
||||||
employeeName: r.employee.name,
|
employeeName: r.employee.name,
|
||||||
department: r.employee.department,
|
department: r.employee.department,
|
||||||
|
type: r.type,
|
||||||
reason: r.reason,
|
reason: r.reason,
|
||||||
|
resignationReason: r.resignationReason,
|
||||||
terminationDate: r.terminationDate.toISOString().slice(0, 10),
|
terminationDate: r.terminationDate.toISOString().slice(0, 10),
|
||||||
compensation: r.compensation,
|
compensation: r.compensation,
|
||||||
riskLevel: r.riskLevel,
|
riskLevel: r.riskLevel,
|
||||||
|
remark: r.remark,
|
||||||
createdAt: r.createdAt.toISOString().slice(0, 10),
|
createdAt: r.createdAt.toISOString().slice(0, 10),
|
||||||
})),
|
})),
|
||||||
total,
|
total,
|
||||||
|
|||||||
+142
-17
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download } from 'lucide-react'
|
import { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX } from 'lucide-react'
|
||||||
import api from '../lib/api'
|
import api from '../lib/api'
|
||||||
import Card from '../components/ui/Card'
|
import Card from '../components/ui/Card'
|
||||||
import Button from '../components/ui/Button'
|
import Button from '../components/ui/Button'
|
||||||
@@ -19,6 +19,8 @@ export default function Roster() {
|
|||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
|
const [showResignModal, setShowResignModal] = useState(false)
|
||||||
|
const [resignEmployee, setResignEmployee] = useState<any>(null)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(10)
|
const [pageSize, setPageSize] = useState(10)
|
||||||
|
|
||||||
@@ -39,6 +41,16 @@ export default function Roster() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resignMutation = useMutation({
|
||||||
|
mutationFn: (data: any) => api.post('/termination/resignation', data),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||||
|
setShowResignModal(false)
|
||||||
|
setResignEmployee(null)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const filtered = employees?.filter((e: any) =>
|
const filtered = employees?.filter((e: any) =>
|
||||||
!search || e.name.includes(search) || e.department.includes(search)
|
!search || e.name.includes(search) || e.department.includes(search)
|
||||||
) || []
|
) || []
|
||||||
@@ -87,6 +99,7 @@ export default function Roster() {
|
|||||||
<th className="py-2 px-3 text-center">培训</th>
|
<th className="py-2 px-3 text-center">培训</th>
|
||||||
<th className="py-2 px-3 text-center">绩效</th>
|
<th className="py-2 px-3 text-center">绩效</th>
|
||||||
<th className="py-2 px-3 text-center">工资条</th>
|
<th className="py-2 px-3 text-center">工资条</th>
|
||||||
|
<th className="py-2 px-3 text-center">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -129,6 +142,23 @@ export default function Roster() {
|
|||||||
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.trainingRecords || 0}</td>
|
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.trainingRecords || 0}</td>
|
||||||
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.performanceRecords || 0}</td>
|
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.performanceRecords || 0}</td>
|
||||||
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.payslips || 0}</td>
|
<td className="py-2 px-3 text-center text-gray-500">{e.counts?.payslips || 0}</td>
|
||||||
|
<td className="py-2 px-3 text-center">
|
||||||
|
{e.status === 'ACTIVE' && !e.hasTermination && (
|
||||||
|
<button
|
||||||
|
className="text-xs text-gray-500 hover:text-danger flex items-center gap-0.5"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation()
|
||||||
|
setResignEmployee(e)
|
||||||
|
setShowResignModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserX className="w-3.5 h-3.5" />离职
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{e.hasTermination && e.status === 'ACTIVE' && (
|
||||||
|
<span className="text-xs text-gray-400">待离职</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -145,6 +175,16 @@ export default function Roster() {
|
|||||||
error={addMutation.error as any}
|
error={addMutation.error as any}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showResignModal && resignEmployee && (
|
||||||
|
<ResignModal
|
||||||
|
employee={resignEmployee}
|
||||||
|
onClose={() => { setShowResignModal(false); setResignEmployee(null) }}
|
||||||
|
onSubmit={(data) => resignMutation.mutate(data)}
|
||||||
|
loading={resignMutation.isPending}
|
||||||
|
error={resignMutation.error as any}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -170,7 +210,7 @@ function EmployeeProfile({ employeeId, onBack }: { employeeId: string; onBack: (
|
|||||||
{ key: 'attendance', label: '考勤记录', icon: Calendar },
|
{ key: 'attendance', label: '考勤记录', icon: Calendar },
|
||||||
{ key: 'training', label: '培训签收', icon: GraduationCap },
|
{ key: 'training', label: '培训签收', icon: GraduationCap },
|
||||||
{ key: 'performance', label: '绩效考核', icon: TrendingUp },
|
{ key: 'performance', label: '绩效考核', icon: TrendingUp },
|
||||||
{ key: 'termination', label: '解聘记录', icon: FileText },
|
{ key: 'termination', label: '离职/解聘记录', icon: FileText },
|
||||||
{ key: 'attachment', label: '附件管理', icon: Paperclip },
|
{ key: 'attachment', label: '附件管理', icon: Paperclip },
|
||||||
{ key: 'evidence', label: '仲裁证据链', icon: Scale },
|
{ key: 'evidence', label: '仲裁证据链', icon: Scale },
|
||||||
]
|
]
|
||||||
@@ -617,6 +657,74 @@ function ContractInfo({ employeeId, contracts, hireDate }: { employeeId: string;
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResignModal({ employee, onClose, onSubmit, loading, error }: {
|
||||||
|
employee: any
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (data: any) => void
|
||||||
|
loading: boolean
|
||||||
|
error: any
|
||||||
|
}) {
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
terminationDate: new Date().toISOString().slice(0, 10),
|
||||||
|
resignationReason: '个人原因',
|
||||||
|
remark: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const reasons = ['个人原因', '职业发展', '薪资不满意', '家庭原因', '身体原因', '其他']
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit({
|
||||||
|
employeeId: employee.id,
|
||||||
|
terminationDate: new Date(form.terminationDate).toISOString(),
|
||||||
|
resignationReason: form.resignationReason,
|
||||||
|
remark: form.remark || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open onClose={onClose} title={`办理离职 - ${employee.name}`}>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-blue-50 text-blue-700 text-xs px-3 py-2 rounded-md">
|
||||||
|
员工主动离职,不涉及经济补偿金。离职日期可在未来(提前办理),到日期后状态自动变为离职。
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>员工</Label>
|
||||||
|
<div className="text-xs text-gray-600">{employee.name} - {employee.department}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>离职日期</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={form.terminationDate}
|
||||||
|
onChange={(e) => setForm({ ...form, terminationDate: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>离职原因</Label>
|
||||||
|
<Select value={form.resignationReason} onChange={(e) => setForm({ ...form, resignationReason: e.target.value })}>
|
||||||
|
{reasons.map((r) => <option key={r} value={r}>{r}</option>)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>备注(选填)</Label>
|
||||||
|
<Input value={form.remark} onChange={(e) => setForm({ ...form, remark: e.target.value })} placeholder="补充说明" />
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="text-xs text-danger">
|
||||||
|
{(error as any)?.response?.data?.error?.message || '操作失败,请重试'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button variant="secondary" onClick={onClose}>取消</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={loading}>
|
||||||
|
{loading ? '提交中...' : '确认离职'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSubmit: (data: any) => void
|
onSubmit: (data: any) => void
|
||||||
@@ -892,6 +1000,7 @@ function TerminationInfo({ employeeId, profile, records }: { employeeId: string;
|
|||||||
const reasonMap: Record<string, string> = {
|
const reasonMap: Record<string, string> = {
|
||||||
NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除',
|
NEGOTIATED: '协商解除', FAULT: '员工过错', NONFAULT: '非过错解除',
|
||||||
LAYOFF: '经济性裁员', EXPIRED: '合同到期', ILLEGAL: '违法解除',
|
LAYOFF: '经济性裁员', EXPIRED: '合同到期', ILLEGAL: '违法解除',
|
||||||
|
RESIGNATION: '员工主动离职',
|
||||||
}
|
}
|
||||||
const legalBasisMap: Record<string, string> = {
|
const legalBasisMap: Record<string, string> = {
|
||||||
NEGOTIATED: '《劳动合同法》第36条', FAULT: '《劳动合同法》第39条',
|
NEGOTIATED: '《劳动合同法》第36条', FAULT: '《劳动合同法》第39条',
|
||||||
@@ -908,7 +1017,7 @@ 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>
|
if (!records?.length) return <Card><div className="text-center py-8 text-gray-400">暂无离职/解聘记录</div></Card>
|
||||||
|
|
||||||
if (printRecord) {
|
if (printRecord) {
|
||||||
return (
|
return (
|
||||||
@@ -1031,26 +1140,42 @@ function TerminationInfo({ employeeId, profile, records }: { employeeId: string;
|
|||||||
<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">
|
||||||
<span className="text-xs font-medium text-gray-700">{t.terminationDate?.toString().slice(0, 10)}</span>
|
<span className="text-xs font-medium text-gray-700">{t.terminationDate?.toString().slice(0, 10)}</span>
|
||||||
|
<span className={`px-2 py-0.5 rounded text-xs ${t.type === 'RESIGNATION' ? 'bg-blue-50 text-blue-700' : 'bg-gray-100 text-gray-600'}`}>{t.type === 'RESIGNATION' ? '主动离职' : '公司解聘'}</span>
|
||||||
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 text-xs">{reasonMap[t.reason] || t.reason}</span>
|
<span className="px-2 py-0.5 rounded bg-gray-100 text-gray-600 text-xs">{reasonMap[t.reason] || t.reason}</span>
|
||||||
<span className={`px-2 py-0.5 rounded text-xs ${t.riskLevel === 'SAFE' ? 'bg-green-50 text-safe' : t.riskLevel === 'WARNING' ? 'bg-amber-50 text-amber-600' : 'bg-red-50 text-danger'}`}>
|
{t.type !== 'RESIGNATION' && (
|
||||||
{t.riskLevel === 'SAFE' ? '风险低' : t.riskLevel === 'WARNING' ? '注意' : '高风险'}
|
<span className={`px-2 py-0.5 rounded text-xs ${t.riskLevel === 'SAFE' ? 'bg-green-50 text-safe' : t.riskLevel === 'WARNING' ? 'bg-amber-50 text-amber-600' : 'bg-red-50 text-danger'}`}>
|
||||||
</span>
|
{t.riskLevel === 'SAFE' ? '风险低' : t.riskLevel === 'WARNING' ? '注意' : '高风险'}
|
||||||
|
</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">
|
||||||
<div className="flex justify-between border-b pb-1.5">
|
{t.type === 'RESIGNATION' ? (
|
||||||
<span className="text-gray-400">经济补偿金</span>
|
<>
|
||||||
<span className="font-medium text-gray-700">¥{fmt(t.compensation)}</span>
|
<div className="flex justify-between border-b pb-1.5">
|
||||||
</div>
|
<span className="text-gray-400">离职原因</span>
|
||||||
<div className="flex justify-between border-b pb-1.5">
|
<span className="font-medium text-gray-700">{t.resignationReason || '-'}</span>
|
||||||
<span className="text-gray-400">法律依据</span>
|
</div>
|
||||||
<span className="font-medium text-gray-700">{legalBasisMap[t.reason] || '-'}</span>
|
</>
|
||||||
</div>
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between border-b pb-1.5">
|
||||||
|
<span className="text-gray-400">经济补偿金</span>
|
||||||
|
<span className="font-medium text-gray-700">¥{fmt(t.compensation)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between border-b pb-1.5">
|
||||||
|
<span className="text-gray-400">法律依据</span>
|
||||||
|
<span className="font-medium text-gray-700">{legalBasisMap[t.reason] || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{t.remark && <div className="text-xs text-gray-500 bg-gray-50 px-2 py-1.5 rounded">{t.remark}</div>}
|
{t.remark && <div className="text-xs text-gray-500 bg-gray-50 px-2 py-1.5 rounded">{t.remark}</div>}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button variant="secondary" size="sm" onClick={() => setPrintRecord(t)}>
|
{t.type !== 'RESIGNATION' && (
|
||||||
<Printer className="w-4 h-4 mr-1" />打印解聘材料
|
<Button variant="secondary" size="sm" onClick={() => setPrintRecord(t)}>
|
||||||
</Button>
|
<Printer className="w-4 h-4 mr-1" />打印解聘材料
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface RosterEmployee {
|
|||||||
name: string
|
name: string
|
||||||
department: string
|
department: string
|
||||||
status: string
|
status: string
|
||||||
|
hasTermination?: boolean
|
||||||
hireDate: string
|
hireDate: string
|
||||||
monthlySalary: number
|
monthlySalary: number
|
||||||
latestContract: any
|
latestContract: any
|
||||||
@@ -276,7 +277,7 @@ export default function Termination() {
|
|||||||
}, [selectedEmployee, terminationDate, socialAvgWage, reason])
|
}, [selectedEmployee, terminationDate, socialAvgWage, reason])
|
||||||
|
|
||||||
const canProceed = () => {
|
const canProceed = () => {
|
||||||
if (step === 0) return !!employeeId
|
if (step === 0) return !!employeeId && !selectedEmployee?.hasTermination
|
||||||
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
|
||||||
@@ -440,6 +441,9 @@ 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 && (
|
||||||
|
<div className="text-danger font-medium mt-1">⚠️ 该员工已有离职/解聘记录,如需再次解聘请先办理重新雇佣</div>
|
||||||
|
)}
|
||||||
{selectedEmployee.latestContract ? (
|
{selectedEmployee.latestContract ? (
|
||||||
<div>合同状态:{selectedEmployee.latestContract.contractType === 'UNSIGNED' ? '未签订' : `签订于 ${selectedEmployee.latestContract.signDate?.slice(0, 10) || '未知'}`}</div>
|
<div>合同状态:{selectedEmployee.latestContract.contractType === 'UNSIGNED' ? '未签订' : `签订于 ${selectedEmployee.latestContract.signDate?.slice(0, 10) || '未知'}`}</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user