feat: 北京解聘合规增强 — 政策法规库+地区过滤+工会回执证据链
1. 政策法规库(RAG知识库): - 新增6条北京地区单方解除劳动合同工作指引种子数据 - 涵盖通知工会程序、函件内容要求、回执要求、监督提示函、仲裁审查等 - 企业用户通过AI问答可检索到北京地区工会通知规定 2. 地区差异化合规检查: - getChecklistForReason 增加 orgCity 参数 - 工会通知检查项仅北京地区显示(FAULT/NONFAULT/LAYOFF) - 前端解聘方式说明中北京工会提示仅北京地区动态显示 - 非北京地区不显示工会通知检查项,避免误导 3. 工会回执上传+证据链留存: - 后端新增3个接口:上传回执文件、保存回执信息、获取回执信息 - 回执信息保存到草稿 checklistOverrides - 自动追加到证据链(appendEvidence),作为劳动仲裁举证材料 - 前端合规检查步骤增加工会回执上传区域 - 确认提交步骤展示回执文件链接 - 新增 uploads 静态文件服务 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -697,6 +697,18 @@ export const terminationApi = {
|
||||
/** 批量预览 */
|
||||
batchPreview: (items: Record<string, unknown>[]) =>
|
||||
post('/termination/batch/preview', { items }),
|
||||
/** 上传工会回执文件 */
|
||||
uploadUnionReceipt: (draftId: string, file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return post(`/termination/draft/${draftId}/union-receipt/upload`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(unwrap<any>())
|
||||
},
|
||||
/** 保存工会回执信息 */
|
||||
saveUnionReceipt: (draftId: string, data: Record<string, unknown>) =>
|
||||
post(`/termination/draft/${draftId}/union-receipt`, data).then(unwrap<any>()),
|
||||
/** 获取工会回执信息 */
|
||||
getUnionReceipt: (draftId: string) =>
|
||||
get(`/termination/draft/${draftId}/union-receipt`).then(unwrap<any>()),
|
||||
}
|
||||
|
||||
// ========== 制度相关 ==========
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Stepper } from '../components/ui/Stepper'
|
||||
import { InlineAlert } from '../components/ui/InlineAlert'
|
||||
import PageGuide from '../components/ui/PageGuide'
|
||||
import jsPDF from 'jspdf'
|
||||
import { rosterApi, terminationApi, esignApi } from '../lib/api-services'
|
||||
import { rosterApi, terminationApi, esignApi, settingsApi } from '../lib/api-services'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
@@ -42,10 +42,9 @@ const REASONS = [
|
||||
steps: [
|
||||
'第一步:收集并固化证据(违纪事实、制度依据、证人证言等)',
|
||||
'第二步:确认规章制度经过民主程序制定并已向员工公示(签收记录)',
|
||||
'第三步:提前5个工作日将解除理由书面通知工会(北京地区必经程序),工会提出意见后书面回复工会',
|
||||
'第三步:将解除理由通知工会,工会提出意见后书面回复工会',
|
||||
'第四步:向员工送达《解除劳动合同通知书》,注明解除依据和事实',
|
||||
'第五步:办理工作交接、社保减员、档案转移',
|
||||
'⚠ 北京地区:单方解除须提前5个工作日书面通知工会,未建立工会的通知上一级工会(经营地乡镇/街道/园区/开发区总工会)。可在「文本模板库」使用《拟解除劳动合同通知工会函》模板',
|
||||
'⚠ 注意:证据不足或制度未公示可能导致违法解除,建议咨询律师',
|
||||
],
|
||||
},
|
||||
@@ -59,11 +58,9 @@ const REASONS = [
|
||||
'第一步(不胜任):进行绩效考核,确认不胜任事实',
|
||||
'第二步(医疗期满):另行安排合适岗位,员工仍不能胜任',
|
||||
'第二步(不胜任):进行培训或调岗,再次考核仍不胜任',
|
||||
'第三步:提前5个工作日书面通知工会(北京地区必经程序),收到工会回执后再解除',
|
||||
'第四步:提前30天书面通知员工,或额外支付1个月代通知金',
|
||||
'第五步:支付经济补偿金(N)',
|
||||
'第六步:出具解除证明,办理交接、社保减员',
|
||||
'⚠ 北京地区:单方解除须提前5个工作日书面通知工会,可在「文本模板库」使用《拟解除劳动合同通知工会函》模板',
|
||||
'第三步:提前30天书面通知员工,或额外支付1个月代通知金',
|
||||
'第四步:支付经济补偿金(N)',
|
||||
'第五步:出具解除证明,办理交接、社保减员',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -74,13 +71,11 @@ const REASONS = [
|
||||
steps: [
|
||||
'第一步:确认符合法定裁员情形,准备相关证明材料',
|
||||
'第二步:提前30天向工会或全体职工说明情况(书面会议记录)',
|
||||
'第三步:听取工会或职工意见,形成意见处理方案,收到工会书面回执',
|
||||
'第三步:听取工会或职工意见,形成意见处理方案',
|
||||
'第四步:向当地劳动行政部门报告裁员方案',
|
||||
'第五步:确定裁员名单(优先留用法定保护人员)',
|
||||
'第六步:提前5个工作日书面通知工会拟解除名单(北京地区必经程序)',
|
||||
'第七步:向员工送达解除通知,支付经济补偿金(N)',
|
||||
'第八步:办理交接、社保减员、档案转移',
|
||||
'⚠ 北京地区:裁员属单方解除,须提前5个工作日书面通知工会,可在「文本模板库」使用《拟解除劳动合同通知工会函》模板',
|
||||
'第六步:向员工送达解除通知,支付经济补偿金(N)',
|
||||
'第七步:办理交接、社保减员、档案转移',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -176,6 +171,14 @@ export default function Termination() {
|
||||
const [employeeId, setEmployeeId] = useState('')
|
||||
const [terminationDate, setTerminationDate] = useState('')
|
||||
|
||||
// 获取组织所在城市,用于地区差异化合规提示(如北京通知工会程序)
|
||||
const { data: orgData } = useQuery<any>({
|
||||
queryKey: ['org-settings'],
|
||||
queryFn: () => settingsApi.org(),
|
||||
staleTime: 300000,
|
||||
})
|
||||
const isBeijing = !orgData?.city || orgData?.city === '北京' || orgData?.city === '北京市' || (orgData?.city as string)?.includes('北京')
|
||||
|
||||
// 从 URL 参数预填员工和解聘类型(从花名册操作栏跳转)
|
||||
useEffect(() => {
|
||||
const urlEmployeeId = searchParams.get('employeeId')
|
||||
@@ -197,6 +200,16 @@ export default function Termination() {
|
||||
const [compAdjustments, setCompAdjustments] = useState<Array<{ field: string; from: number; to: number; reason: string }>>([])
|
||||
const [handoverItems, setHandoverItems] = useState(DEFAULT_HANDOVER_ITEMS)
|
||||
const [checklistOverrides, setChecklistOverrides] = useState<Record<string, { checked: boolean; overrideReason: string }>>({})
|
||||
/** 工会回执上传(北京地区单方解除证据链) */
|
||||
const [unionReceipt, setUnionReceipt] = useState<{
|
||||
receiptNo: string
|
||||
unionName: string
|
||||
receiptDate: string
|
||||
fileUrl: string
|
||||
fileName: string
|
||||
unionOpinion: string
|
||||
} | null>(null)
|
||||
const [uploadingReceipt, setUploadingReceipt] = useState(false)
|
||||
const [editingCompField, setEditingCompField] = useState<string | null>(null)
|
||||
const [editCompValue, setEditCompValue] = useState<number>(0)
|
||||
const [editCompReason, setEditCompReason] = useState('')
|
||||
@@ -1377,6 +1390,17 @@ ${items}
|
||||
{r.steps.map((s, i) => (
|
||||
<li key={i} className={`text-xs leading-relaxed ${s.includes('⚠') ? 'text-amber-600 font-medium' : 'text-gray-600'}`}>{s}</li>
|
||||
))}
|
||||
{/* 北京地区单方解除须通知工会 */}
|
||||
{isBeijing && ['FAULT', 'NONFAULT', 'LAYOFF'].includes(r.value) && (
|
||||
<>
|
||||
<li className="text-xs leading-relaxed text-amber-600 font-medium">
|
||||
⚠ 北京地区:单方解除须提前5个工作日书面通知工会,未建立工会的通知上一级工会(经营地乡镇/街道/园区/开发区总工会)。可在「文本模板库」使用《拟解除劳动合同通知工会函》模板
|
||||
</li>
|
||||
<li className="text-xs leading-relaxed text-amber-600 font-medium">
|
||||
⚠ 收到工会书面回执后,请上传回执扫描件作为证据链留存
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1534,6 +1558,101 @@ ${items}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* 工会回执上传区域(北京地区单方解除,checklist 包含 union_receipt 项时显示) */}
|
||||
{checklistItems?.some(item => item.key === 'union_receipt') && checklist['notify_union'] && (
|
||||
<div className="border rounded-md p-3 space-y-3 bg-blue-50/30">
|
||||
<div className="text-xs font-medium text-gray-700 flex items-center gap-1.5">
|
||||
<Shield className="w-3.5 h-3.5 text-primary" />
|
||||
工会回执上传(证据链留存)
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
收到工会书面回执后,请上传回执扫描件。回执信息将自动保存到解聘草稿并追加到证据链,作为劳动仲裁举证材料。
|
||||
</div>
|
||||
|
||||
{unionReceipt && unionReceipt.fileUrl ? (
|
||||
/* 已上传回执展示 */
|
||||
<div className="space-y-2 bg-white rounded-md p-2.5 border">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<CheckCircle className="w-4 h-4 text-safe" />
|
||||
<span className="font-medium">回执已上传</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-gray-600">
|
||||
<div>回执编号:{unionReceipt.receiptNo || '—'}</div>
|
||||
<div>工会名称:{unionReceipt.unionName || '—'}</div>
|
||||
<div>回执日期:{unionReceipt.receiptDate || '—'}</div>
|
||||
<div>文件:<a href={unionReceipt.fileUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">{unionReceipt.fileName}</a></div>
|
||||
</div>
|
||||
{unionReceipt.unionOpinion && (
|
||||
<div className="text-xs text-gray-600">工会意见:{unionReceipt.unionOpinion}</div>
|
||||
)}
|
||||
<Button size="sm" variant="secondary" onClick={() => setUnionReceipt(null)}>重新上传</Button>
|
||||
</div>
|
||||
) : (
|
||||
/* 上传表单 */
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>回执编号</Label>
|
||||
<Input value={unionReceipt?.receiptNo || ''} onChange={(e) => setUnionReceipt(prev => ({ ...(prev || { receiptNo: '', unionName: '', receiptDate: '', fileUrl: '', fileName: '', unionOpinion: '' }), receiptNo: e.target.value }))} placeholder="如:2026第001号" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>工会名称</Label>
|
||||
<Input value={unionReceipt?.unionName || ''} onChange={(e) => setUnionReceipt(prev => ({ ...(prev || { receiptNo: '', unionName: '', receiptDate: '', fileUrl: '', fileName: '', unionOpinion: '' }), unionName: e.target.value }))} placeholder="如:XX街道总工会" />
|
||||
</div>
|
||||
<div>
|
||||
<Label>回执日期</Label>
|
||||
<Input type="date" value={unionReceipt?.receiptDate || ''} onChange={(e) => setUnionReceipt(prev => ({ ...(prev || { receiptNo: '', unionName: '', receiptDate: '', fileUrl: '', fileName: '', unionOpinion: '' }), receiptDate: e.target.value }))} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>工会意见(选填)</Label>
|
||||
<Input value={unionReceipt?.unionOpinion || ''} onChange={(e) => setUnionReceipt(prev => ({ ...(prev || { receiptNo: '', unionName: '', receiptDate: '', fileUrl: '', fileName: '', unionOpinion: '' }), unionOpinion: e.target.value }))} placeholder="如有意见建议请填写" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>回执扫描件</Label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf,.bmp"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file || !draftId) return
|
||||
setUploadingReceipt(true)
|
||||
try {
|
||||
const res = await terminationApi.uploadUnionReceipt(draftId, file)
|
||||
setUnionReceipt(prev => ({ ...(prev || { receiptNo: '', unionName: '', receiptDate: '', fileUrl: '', fileName: '', unionOpinion: '' }), fileUrl: res.fileUrl, fileName: res.fileName }))
|
||||
toast.success('文件上传成功')
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.error?.message || '上传失败')
|
||||
} finally {
|
||||
setUploadingReceipt(false)
|
||||
}
|
||||
}}
|
||||
className="block w-full text-xs text-gray-500 file:mr-3 file:py-1.5 file:px-3 file:rounded-md file:border-0 file:text-xs file:font-medium file:bg-primary file:text-white hover:file:bg-primary/90"
|
||||
/>
|
||||
{uploadingReceipt && <div className="text-xs text-gray-400 mt-1">上传中...</div>}
|
||||
</div>
|
||||
{draftId && unionReceipt?.fileUrl && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await terminationApi.saveUnionReceipt(draftId, unionReceipt)
|
||||
toast.success('工会回执已保存到证据链')
|
||||
setChecklist({ ...checklist, union_receipt: true })
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.error?.message || '保存失败')
|
||||
}
|
||||
}}
|
||||
>保存回执到证据链</Button>
|
||||
)}
|
||||
{!draftId && (
|
||||
<div className="text-xs text-amber-600">请先保存草稿后再上传工会回执</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1896,6 +2015,7 @@ ${items}
|
||||
<div className={checklist['notify_union'] ? 'text-safe' : 'text-danger'}>
|
||||
工会通知:{checklist['notify_union'] ? '已通知' : '⚠ 未通知工会(北京地区单方解除必经程序)'}
|
||||
{checklist['notify_union'] && checklist['union_receipt'] ? ' · 已收到回执' : checklist['notify_union'] ? ' · 未收到回执' : ''}
|
||||
{unionReceipt?.fileUrl && <a href={unionReceipt.fileUrl} target="_blank" rel="noopener noreferrer" className="ml-1 text-primary hover:underline">查看回执</a>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user