feat: 20260809 系统优化 - 全部28项问题修复(P0×6+P1×16+P2×6)
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除 P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量 P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
This commit is contained in:
@@ -205,6 +205,8 @@ export default function Termination() {
|
||||
const [filterStatus, setFilterStatus] = useState('')
|
||||
const [filterDepartment, setFilterDepartment] = useState('')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterDateFrom, setFilterDateFrom] = useState('')
|
||||
const [filterDateTo, setFilterDateTo] = useState('')
|
||||
const draftPageSize = usePageSize()
|
||||
const [draftPage, setDraftPage] = useState(1)
|
||||
|
||||
@@ -433,6 +435,16 @@ export default function Termination() {
|
||||
onError: () => toast.error('撤销失败'),
|
||||
})
|
||||
|
||||
// 删除草稿
|
||||
const deleteDraftMutation = useMutation({
|
||||
mutationFn: (id: string) => terminationApi.deleteDraft(id),
|
||||
onSuccess: () => {
|
||||
toast.success('草稿已删除')
|
||||
queryClient.invalidateQueries({ queryKey: ['termination-drafts'] })
|
||||
},
|
||||
onError: () => toast.error('删除失败'),
|
||||
})
|
||||
|
||||
const { data: evidenceChain } = useQuery({
|
||||
queryKey: ['evidence-chain', employeeId],
|
||||
queryFn: async () => {
|
||||
@@ -728,8 +740,23 @@ export default function Termination() {
|
||||
<option value="">全部部门</option>
|
||||
{departmentList?.map((d: string) => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
{(searchTerm || filterStatus || filterDepartment) && (
|
||||
<button onClick={() => { setSearchTerm(''); setFilterStatus(''); setFilterDepartment('') }} className="text-xs text-gray-500 hover:text-primary">清除筛选</button>
|
||||
<input
|
||||
type="date"
|
||||
value={filterDateFrom}
|
||||
onChange={(e) => setFilterDateFrom(e.target.value)}
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
placeholder="开始日期"
|
||||
/>
|
||||
<span className="text-xs text-gray-400">至</span>
|
||||
<input
|
||||
type="date"
|
||||
value={filterDateTo}
|
||||
onChange={(e) => setFilterDateTo(e.target.value)}
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
placeholder="结束日期"
|
||||
/>
|
||||
{(searchTerm || filterStatus || filterDepartment || filterDateFrom || filterDateTo) && (
|
||||
<button onClick={() => { setSearchTerm(''); setFilterStatus(''); setFilterDepartment(''); setFilterDateFrom(''); setFilterDateTo('') }} className="text-xs text-gray-500 hover:text-primary">清除筛选</button>
|
||||
)}
|
||||
<Button variant="secondary" size="sm" onClick={async () => {
|
||||
try {
|
||||
@@ -737,6 +764,8 @@ export default function Termination() {
|
||||
if (searchTerm) params.set('search', searchTerm)
|
||||
if (filterStatus) params.set('status', filterStatus)
|
||||
if (filterDepartment) params.set('department', filterDepartment)
|
||||
if (filterDateFrom) params.set('dateFrom', filterDateFrom)
|
||||
if (filterDateTo) params.set('dateTo', filterDateTo)
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1'
|
||||
const res = await fetch(`${baseURL}/export/terminations?${params}`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
@@ -866,6 +895,20 @@ export default function Termination() {
|
||||
<Ban className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{(item.status === 'DRAFT' || item.status === 'CANCELLED') && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (await confirm({ title: '删除草稿', message: '确定删除此草稿记录?删除后不可恢复。' })) {
|
||||
deleteDraftMutation.mutate(item.id)
|
||||
}
|
||||
}}
|
||||
className="p-1 text-gray-400 hover:text-danger"
|
||||
aria-label="删除"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleViewDetail(item.id)}
|
||||
className="p-1 text-gray-500 hover:text-primary"
|
||||
@@ -1025,21 +1068,29 @@ export default function Termination() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const doc = new jsPDF()
|
||||
doc.setFontSize(18)
|
||||
doc.text('解除/终止劳动合同证明书', 105, 25, { align: 'center' })
|
||||
doc.setFontSize(11)
|
||||
let y = 45
|
||||
doc.text(`兹证明 ${draftDetail.employeeName}(身份证号:${draftDetail.idCardNumber || '—'}),`, 14, y); y += 8
|
||||
doc.text(`原系我公司 ${draftDetail.department} 部门员工,`, 14, y); y += 8
|
||||
doc.text(`于 ${draftDetail.terminationDate} 因 ${REASONS.find(r => r.value === draftDetail.reason)?.label || draftDetail.reason} 原因,`, 14, y); y += 8
|
||||
doc.text(`正式解除/终止劳动合同。`, 14, y); y += 8
|
||||
doc.text(`经济补偿金已结清:¥${fmt(draftDetail.compensation)}。`, 14, y); y += 8
|
||||
doc.text(`社保截止月份:${draftDetail.socialInsEndMonth || '—'},公积金截止月份:${draftDetail.housingFundEndMonth || '—'}。`, 14, y); y += 16
|
||||
doc.text('特此证明。', 14, y); y += 24
|
||||
doc.text('公司(盖章)', 140, y)
|
||||
doc.text(new Date().toISOString().slice(0, 10), 140, y + 8)
|
||||
doc.save(`离职证明-${draftDetail.employeeName}-${draftDetail.terminationDate}.pdf`)
|
||||
const reasonLabel = REASONS.find(r => r.value === draftDetail.reason)?.label || draftDetail.reason
|
||||
const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"><title>离职证明</title>
|
||||
<style>
|
||||
body { font-family: SimSun, serif; font-size: 14pt; line-height: 2; text-align: center; }
|
||||
.title { font-size: 22pt; font-weight: bold; margin-bottom: 30pt; }
|
||||
.body { text-align: justify; text-indent: 2em; margin: 0 20pt; }
|
||||
.sign { text-align: right; margin-top: 30pt; margin-right: 20pt; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="title">解除/终止劳动合同证明书</div>
|
||||
<div class="body">兹证明 ${draftDetail.employeeName}(身份证号:${draftDetail.idCardNumber || '—'}),原系我公司 ${draftDetail.department || '—'} 部门员工,于 ${draftDetail.terminationDate} 因 ${reasonLabel} 原因,正式解除/终止劳动合同。</div>
|
||||
<div class="body">经济补偿金已结清:¥${fmt(draftDetail.compensation)}。社保截止月份:${draftDetail.socialInsEndMonth || '—'},公积金截止月份:${draftDetail.housingFundEndMonth || '—'}。</div>
|
||||
<div class="body">特此证明。</div>
|
||||
<div class="sign">公司(盖章)<br/>${new Date().toISOString().slice(0, 10)}</div>
|
||||
</body></html>`
|
||||
const blob = new Blob(['\ufeff' + html], { type: 'application/msword;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `离职证明-${draftDetail.employeeName}-${draftDetail.terminationDate}.doc`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />下载离职证明
|
||||
@@ -1048,25 +1099,28 @@ export default function Termination() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
const doc = new jsPDF()
|
||||
doc.setFontSize(16)
|
||||
doc.text('工作交接清单', 105, 25, { align: 'center' })
|
||||
doc.setFontSize(11)
|
||||
let y = 40
|
||||
doc.text(`员工姓名:${draftDetail.employeeName}`, 14, y); y += 8
|
||||
doc.text(`部门:${draftDetail.department || '—'}`, 14, y); y += 8
|
||||
doc.text(`离职日期:${draftDetail.terminationDate}`, 14, y); y += 12
|
||||
doc.setFontSize(10)
|
||||
draftDetail.handoverItems.forEach((item: any, i: number) => {
|
||||
if (y > 270) { doc.addPage(); y = 20 }
|
||||
doc.text(`${item.done ? '[√]' : '[ ]'} ${item.label}${item.remark ? '(' + item.remark + ')' : ''}`, 14, y); y += 7
|
||||
})
|
||||
y += 16
|
||||
doc.text('交接人签字:____________', 14, y)
|
||||
doc.text('接收人签字:____________', 100, y)
|
||||
y += 12
|
||||
doc.text('日期:____________', 14, y)
|
||||
doc.save(`交接清单-${draftDetail.employeeName}-${draftDetail.terminationDate}.pdf`)
|
||||
const items = draftDetail.handoverItems.map((item: any) => `<div class="body">${item.done ? '[√]' : '[ ]'} ${item.label}${item.remark ? '(' + item.remark + ')' : ''}</div>`).join('')
|
||||
const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head><meta charset="utf-8"><title>工作交接清单</title>
|
||||
<style>
|
||||
body { font-family: SimSun, serif; font-size: 14pt; line-height: 2; text-align: center; }
|
||||
.title { font-size: 18pt; font-weight: bold; margin-bottom: 20pt; }
|
||||
.body { text-align: justify; text-indent: 2em; margin: 0 20pt; }
|
||||
.sign { text-align: right; margin-top: 30pt; margin-right: 20pt; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="title">工作交接清单</div>
|
||||
<div class="body">员工姓名:${draftDetail.employeeName} 部门:${draftDetail.department || '—'} 离职日期:${draftDetail.terminationDate}</div>
|
||||
${items}
|
||||
<div class="sign">交接人签字:____________<br/>接收人签字:____________<br/>日期:____________</div>
|
||||
</body></html>`
|
||||
const blob = new Blob(['\ufeff' + html], { type: 'application/msword;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `交接清单-${draftDetail.employeeName}-${draftDetail.terminationDate}.doc`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />下载交接清单
|
||||
|
||||
Reference in New Issue
Block a user