feat: 20260815 系统优化 - 全部31项问题修复(P0×6+P1×14+P2×9+P3×2)
P0紧急修复(6项): - 草稿保存完整恢复所有字段(含socialAvgWage) - 补偿金批次从compensationBreakdown读取 - 违法解除风险确认UI - 合同结束日期前后校验(前后端双保险) P1高优先级(14项): - 离职日期联动社保/公积金截止月(15号规则) - 合规检查+工作交接改为软阻断(生成待办) - 补偿月数(N/N+1/2N/自定义)+计算基数(近12月/合同/自定义) - 解聘并入花名册操作栏(类型选择跳转向导) - 合同续签开始日期自动推导(原合同结束日+1天) - 年龄合规筛查(童工阻断/未成年工/退休警告) - 编辑入职日期后状态联动(待入职↔在职) - 转正移植到花名册操作栏+薪资回写 - 男职工无法选择三期 P2体验优化(9项): - "劳动合同"调整为"用工关系" - 费用结算新增剩余年假折算(300%日工资) - 身份证号全域改为"证件号码"(前后端18个文件) - 手机号查重 - 开具证明+合同续签移植到花名册操作栏 - 批量转正+批量开具证明 - 去掉用工办理模块 P3规划(2项): - 组织架构+审批流(Department/Position/ApprovalFlow/ApprovalInstance) - 客服工作台(Ticket/ChatSession+SUPPORT角色) 新增模型: Department/Position/ApprovalFlow/ApprovalInstance/Ticket/TicketMessage/ChatSession/ChatMessage 新增字段: Employee.departmentId/supervisorId 新增角色: SUPPORT Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
+134
-17
@@ -5,8 +5,8 @@ import { toast } from 'sonner'
|
||||
import { toastError } from '../lib/errorToast'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
import { Users, Plus, Check, UserX, UserPlus, DollarSign, Building2, RotateCcw, Upload, Wallet, Download, Phone, MapPin, Search, Settings2 } from 'lucide-react'
|
||||
import { rosterApi, employeeApi, terminationApi } from '../lib/api-services'
|
||||
import { Users, Plus, Check, UserX, UserPlus, DollarSign, Building2, RotateCcw, Upload, Wallet, Download, Phone, MapPin, Search, Settings2, CheckCircle, FileText } from 'lucide-react'
|
||||
import { rosterApi, employeeApi, terminationApi, workProcessApi } from '../lib/api-services'
|
||||
import { copyToClipboard } from '../lib/clipboard'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { useDebouncedValue } from '../hooks/useDebouncedValue'
|
||||
@@ -19,7 +19,7 @@ import { fmt, terminateReasonMap } from './roster/shared'
|
||||
import PageGuide from '../components/ui/PageGuide'
|
||||
import EmployeeProfile from './roster/EmployeeProfile'
|
||||
import { InlineAlert } from '../components/ui/InlineAlert'
|
||||
import { AddEmployeeModal, ResignModal, RehireModal, SalaryChangeModal, DeptChangeModal } from './roster/modals'
|
||||
import { AddEmployeeModal, ResignModal, RehireModal, SalaryChangeModal, DeptChangeModal, ConfirmModal } from './roster/modals'
|
||||
import { ImportSettings } from './Settings'
|
||||
import QueryError from '../components/ui/QueryError'
|
||||
|
||||
@@ -77,6 +77,8 @@ export default function Roster() {
|
||||
const [salaryEmployee, setSalaryEmployee] = useState<any>(null)
|
||||
const [showDeptModal, setShowDeptModal] = useState(false)
|
||||
const [deptEmployee, setDeptEmployee] = useState<any>(null)
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
const [confirmEmployee, setConfirmEmployee] = useState<any>(null)
|
||||
const pageSize = usePageSize()
|
||||
const [page, setPage] = useState(1)
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
||||
@@ -210,6 +212,21 @@ export default function Roster() {
|
||||
},
|
||||
})
|
||||
|
||||
// 转正 mutation
|
||||
const confirmMutation = useMutation({
|
||||
mutationFn: (data: { employeeId: string; confirmDate: string; regularSalary?: number }) =>
|
||||
workProcessApi.create({ type: 'CONFIRM', title: '转正', employeeId: data.employeeId, formData: { employeeId: data.employeeId, confirmDate: data.confirmDate, regularSalary: data.regularSalary }, status: 'DRAFT' }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||||
setShowConfirmModal(false)
|
||||
setConfirmEmployee(null)
|
||||
toast.success('转正已提交')
|
||||
},
|
||||
onError: (err: any) => toast.error(err?.response?.data?.error?.message || '转正失败'),
|
||||
})
|
||||
|
||||
const batchRenewMutation = useMutation({
|
||||
mutationFn: (data: { contractIds: string[]; years: number }) => employeeApi.batchRenew(data),
|
||||
onSuccess: () => {
|
||||
@@ -264,6 +281,49 @@ export default function Roster() {
|
||||
},
|
||||
})
|
||||
|
||||
/** 批量转正:为选中的试用期员工创建转正草稿 */
|
||||
const handleBatchConfirm = async () => {
|
||||
const probationEmployees = employees.filter((e: any) => selectedIds.has(e.id) && e.probationInfo?.isProbation)
|
||||
if (probationEmployees.length === 0) {
|
||||
toast.error('选中的员工中没有试用期员工')
|
||||
return
|
||||
}
|
||||
const confirmDate = new Date().toISOString().slice(0, 10)
|
||||
let success = 0
|
||||
let failed = 0
|
||||
await Promise.all(
|
||||
probationEmployees.map(async (emp: any) => {
|
||||
try {
|
||||
await workProcessApi.create({
|
||||
type: 'CONFIRM',
|
||||
title: '批量转正',
|
||||
employeeId: emp.id,
|
||||
formData: { employeeId: emp.id, confirmDate },
|
||||
status: 'DRAFT',
|
||||
})
|
||||
success++
|
||||
} catch {
|
||||
failed++
|
||||
}
|
||||
})
|
||||
)
|
||||
queryClient.invalidateQueries({ queryKey: ['roster'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
||||
if (success > 0) toast.success(`已为 ${success} 名员工创建转正草稿`)
|
||||
if (failed > 0) toast.error(`${failed} 名员工转正失败`)
|
||||
setSelectedIds(new Set())
|
||||
}
|
||||
|
||||
/** 批量开具证明:跳转到用工办理批量开具收入证明 */
|
||||
const handleBatchCert = () => {
|
||||
if (selectedIds.size === 0) {
|
||||
toast.error('请至少选择一名员工')
|
||||
return
|
||||
}
|
||||
const ids = Array.from(selectedIds).join(',')
|
||||
window.location.href = `/work-process?type=INCOME_CERT&employeeIds=${ids}`
|
||||
}
|
||||
|
||||
const toggleSelect = (id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev)
|
||||
@@ -450,6 +510,12 @@ export default function Roster() {
|
||||
<Button variant="secondary" onClick={() => setShowBatchRenewModal(true)} className="shrink-0">
|
||||
<Check className="mr-1 h-4 w-4" />批量续签
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleBatchConfirm} className="shrink-0">
|
||||
<CheckCircle className="mr-1 h-4 w-4" />批量转正
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={handleBatchCert} className="shrink-0">
|
||||
<FileText className="mr-1 h-4 w-4" />批量开具证明
|
||||
</Button>
|
||||
<Button variant="danger" onClick={() => setShowBatchTerminateModal(true)} className="shrink-0">
|
||||
<UserX className="mr-1 h-4 w-4" />批量解聘
|
||||
</Button>
|
||||
@@ -540,10 +606,10 @@ export default function Roster() {
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium">
|
||||
<div>{e.name}</div>
|
||||
<div className="text-gray-400 text-xs font-mono cursor-pointer hover:text-primary transition-colors" title="点击复制完整身份证号" onClick={(ev) => {
|
||||
<div className="text-gray-400 text-xs font-mono cursor-pointer hover:text-primary transition-colors" title="点击复制完整证件号码" onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
if (e.idCardNumber) {
|
||||
copyToClipboard(e.idCardNumber, '已复制身份证号')
|
||||
copyToClipboard(e.idCardNumber, '已复制证件号码')
|
||||
}
|
||||
}}>{e.idCardMasked || '—'}</div>
|
||||
</td>
|
||||
@@ -713,18 +779,35 @@ export default function Roster() {
|
||||
>
|
||||
<DollarSign className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="发薪"
|
||||
aria-label={`为${e.name}发薪`}
|
||||
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
navigate('/money')
|
||||
}}
|
||||
>
|
||||
<Wallet className="h-4 w-4" />
|
||||
</button>
|
||||
{/* 试用期员工显示转正按钮,其他员工显示发薪按钮 */}
|
||||
{e.probationInfo?.isProbation ? (
|
||||
<button
|
||||
type="button"
|
||||
title="转正"
|
||||
aria-label={`为${e.name}办理转正`}
|
||||
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setConfirmEmployee(e)
|
||||
setShowConfirmModal(true)
|
||||
}}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
title="发薪"
|
||||
aria-label={`为${e.name}发薪`}
|
||||
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
navigate('/money')
|
||||
}}
|
||||
>
|
||||
<Wallet className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
title="调部门"
|
||||
@@ -738,6 +821,30 @@ export default function Roster() {
|
||||
>
|
||||
<Building2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="开具证明"
|
||||
aria-label={`为${e.name}开具证明`}
|
||||
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
window.location.href = `/work-process?type=INCOME_CERT&employeeId=${e.id}`
|
||||
}}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="续签合同"
|
||||
aria-label={`为${e.name}续签合同`}
|
||||
className="rounded-md p-1.5 text-gray-500 transition hover:bg-primary/10 hover:text-primary"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
window.location.href = `/work-process?type=RENEW&employeeId=${e.id}`
|
||||
}}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="离职"
|
||||
@@ -865,6 +972,16 @@ export default function Roster() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showConfirmModal && confirmEmployee && (
|
||||
<ConfirmModal
|
||||
employee={confirmEmployee}
|
||||
onClose={() => { setShowConfirmModal(false); setConfirmEmployee(null) }}
|
||||
onSubmit={(data) => confirmMutation.mutate(data)}
|
||||
loading={confirmMutation.isPending}
|
||||
error={confirmMutation.error as any}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showBatchRenewModal && (
|
||||
<Modal open={showBatchRenewModal} onClose={() => { setShowBatchRenewModal(false); setPreviewData(null); }} title="批量续签">
|
||||
<div className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user