fix: 薪资批次排除预入职员工(hireDate > 批次月末)

原查询只过滤status=ACTIVE,预入职员工status也是ACTIVE会被错误纳入。
增加hireDate <= monthEnd条件,确保只拉入已入职员工。

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-18 14:15:38 +08:00
parent 1feada76d1
commit 9ac07eba0f
11 changed files with 474 additions and 120 deletions
+6
View File
@@ -677,6 +677,12 @@ export const socialInsuranceApi = {
/** 员工参保信息列表 */
employeeEnrollment: (keyword?: string) =>
get('/social/employee-enrollment', { params: keyword ? { keyword } : {} }).then(unwrap<any[]>()),
/** 办理社保增员(批量创建社保记录) */
enrollSocial: (employeeIds: string[], startMonth: string) =>
post('/social/enroll-social', { employeeIds, startMonth }).then(unwrap<any>()),
/** 办理公积金增员(批量创建公积金记录) */
enrollHousing: (employeeIds: string[], startMonth: string) =>
post('/social/enroll-housing', { employeeIds, startMonth }).then(unwrap<any>()),
}
// ========== 商业保险 ==========
+55 -17
View File
@@ -6,7 +6,7 @@ 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, Download, Phone, MapPin, Search, Settings2, CheckCircle, FileText } from 'lucide-react'
import { rosterApi, employeeApi, terminationApi, workProcessApi } from '../lib/api-services'
import { rosterApi, employeeApi, terminationApi, workProcessApi, esignApi } from '../lib/api-services'
import api from '../lib/api'
import { copyToClipboard } from '../lib/clipboard'
import { useAuthStore } from '../store/authStore'
@@ -170,15 +170,38 @@ export default function Roster() {
const addMutation = useMutation({
mutationFn: async (data: any) => {
const res = await employeeApi.create(data)
return res
return { res, data }
},
onSuccess: () => {
onSuccess: async ({ res, data }) => {
queryClient.invalidateQueries({ queryKey: ['roster'] })
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
localStorage.removeItem('add-employee-draft')
setShowAddModal(false)
toast.success('员工已添加,请前往员工档案签订合同')
// 电子签:自动创建电子签署记录
if (data.contract?.signMethod === 'ELECTRONIC' && res?.id) {
try {
// 查询员工档案获取刚创建的合同
const profile = await rosterApi.profile(res.id) as any
const contract = profile?.contracts?.[0]
if (contract?.id) {
await esignApi.create({
contractId: contract.id,
employeeId: res.id,
documentTitle: `${data.name || ''}的劳动合同`,
remark: '新增员工时自动发起',
scene: 'CONTRACT',
})
toast.success('员工已添加,电子签署记录已创建')
} else {
toast.success('员工已添加,请前往员工档案发起电子签署')
}
} catch {
toast.success('员工已添加,电子签署记录创建失败(可稍后手动发起)')
}
} else {
toast.success('员工已添加,请前往员工档案签订合同')
}
},
onError: (err: any) => toastError(err, '创建失败'),
})
@@ -216,23 +239,38 @@ export default function Roster() {
})
const rehireMutation = useMutation({
mutationFn: (data: any) => employeeApi.rehire(rehireEmployee?.id, data),
onSuccess: () => {
mutationFn: async (data: any) => {
const res = await employeeApi.rehire(rehireEmployee?.id, data)
return { res, data }
},
onSuccess: async ({ res, data }) => {
queryClient.invalidateQueries({ queryKey: ['roster'] })
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
setShowRehireModal(false)
// 弹出签署方式选择
setSignChoice({
open: true,
employeeId: rehireEmployee?.id || '',
employeeName: rehireEmployee?.name || '',
employeeIdCardNumber: rehireEmployee?.idCardNumber,
scene: 'CONTRACT',
documentTitle: `${rehireEmployee?.name || ''}的劳动合同`,
remark: '重新入职时发起',
actionName: '重新入职',
})
// 电子签:自动创建电子签署记录
if (data.contract?.signMethod === 'ELECTRONIC' && rehireEmployee?.id) {
try {
const profile = await rosterApi.profile(rehireEmployee.id) as any
const contract = profile?.contracts?.[0]
if (contract?.id) {
await esignApi.create({
contractId: contract.id,
employeeId: rehireEmployee.id,
documentTitle: `${rehireEmployee?.name || ''}的劳动合同`,
remark: '重新入职时自动发起',
scene: 'CONTRACT',
})
toast.success('重新入职成功,电子签署记录已创建')
} else {
toast.success('重新入职成功,请前往员工档案发起电子签署')
}
} catch {
toast.success('重新入职成功,电子签署记录创建失败(可稍后手动发起)')
}
} else {
toast.success('重新入职成功,请前往员工档案签订合同')
}
setRehireEmployee(null)
},
})
+58 -9
View File
@@ -117,6 +117,28 @@ export default function SocialInsurance() {
}
}
// 办理社保增员
const enrollSocialMutation = useMutation({
mutationFn: ({ employeeIds, startMonth }: { employeeIds: string[]; startMonth: string }) =>
socialInsuranceApi.enrollSocial(employeeIds, startMonth),
onSuccess: () => {
toast.success('社保增员已办理')
handleMonthlyProcess()
},
onError: () => toast.error('办理失败'),
})
// 办理公积金增员
const enrollHousingMutation = useMutation({
mutationFn: ({ employeeIds, startMonth }: { employeeIds: string[]; startMonth: string }) =>
socialInsuranceApi.enrollHousing(employeeIds, startMonth),
onSuccess: () => {
toast.success('公积金增员已办理')
handleMonthlyProcess()
},
onError: () => toast.error('办理失败'),
})
const completeProcessMutation = useMutation({
mutationFn: async (type: 'SOCIAL' | 'HOUSING') => {
const snapshot = type === 'SOCIAL' ? monthlyChanges.social : monthlyChanges.housing
@@ -346,7 +368,7 @@ export default function SocialInsurance() {
</div>
)}
<InlineAlert type="info" className="mb-3">
///
///"待办理""在保"
</InlineAlert>
{(() => {
if (!monthlyProcessed) {
@@ -482,6 +504,19 @@ export default function SocialInsurance() {
icon={<Shield className="w-4 h-4 text-blue-500" />}
summary={`${sAddCity.length + sSubCity.length + sNormalCity.length} 人 | 企业 ¥${fmt([...sAddCity, ...sNormalCity].reduce((s: number, i: any) => s + (i.detail?.totalOrg || 0), 0))} + 个人 ¥${fmt([...sAddCity, ...sNormalCity].reduce((s: number, i: any) => s + (i.detail?.totalEmp || 0), 0))} = ¥${fmt(sTotal)}`}
defaultOpen={true}
action={sAddCity.filter((i: any) => i.changeType === 'PENDING').length > 0 ? (
<Button
size="sm"
variant="primary"
onClick={() => {
const pendingIds = sAddCity.filter((i: any) => i.changeType === 'PENDING').map((i: any) => i.employeeId)
enrollSocialMutation.mutate({ employeeIds: pendingIds, startMonth: monthlyMonth })
}}
disabled={enrollSocialMutation.isPending}
>
{enrollSocialMutation.isPending ? '办理中...' : `办理增员(${sAddCity.filter((i: any) => i.changeType === 'PENDING').length}人)`}
</Button>
) : undefined}
>
{sTable}
</CollapsibleSection>
@@ -490,6 +525,19 @@ export default function SocialInsurance() {
icon={<Home className="w-4 h-4 text-green-500" />}
summary={`${hAddCity.length + hSubCity.length + hNormalCity.length} 人 | 企业 ¥${fmt([...hAddCity, ...hNormalCity].reduce((s: number, i: any) => s + (i.detail?.orgAmount || 0), 0))} + 个人 ¥${fmt([...hAddCity, ...hNormalCity].reduce((s: number, i: any) => s + (i.detail?.empAmount || 0), 0))} = ¥${fmt(hTotal)}`}
defaultOpen={true}
action={hAddCity.filter((i: any) => i.changeType === 'PENDING').length > 0 ? (
<Button
size="sm"
variant="primary"
onClick={() => {
const pendingIds = hAddCity.filter((i: any) => i.changeType === 'PENDING').map((i: any) => i.employeeId)
enrollHousingMutation.mutate({ employeeIds: pendingIds, startMonth: monthlyMonth })
}}
disabled={enrollHousingMutation.isPending}
>
{enrollHousingMutation.isPending ? '办理中...' : `办理增员(${hAddCity.filter((i: any) => i.changeType === 'PENDING').length}人)`}
</Button>
) : undefined}
>
{hTable}
</CollapsibleSection>
@@ -518,21 +566,22 @@ export default function SocialInsurance() {
)
}
function CollapsibleSection({ title, icon, summary, defaultOpen = false, children }: { title: string; icon: React.ReactNode; summary?: string; defaultOpen?: boolean; children: React.ReactNode }) {
function CollapsibleSection({ title, icon, summary, defaultOpen = false, action, children }: { title: string; icon: React.ReactNode; summary?: string; defaultOpen?: boolean; action?: React.ReactNode; children: React.ReactNode }) {
const [open, setOpen] = useState(defaultOpen)
return (
<div className="border rounded-lg overflow-hidden">
<button
onClick={() => setOpen(!open)}
className="w-full flex items-center justify-between px-3 py-2 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-2">
<div className="w-full flex items-center justify-between px-3 py-2 hover:bg-gray-50 transition-colors">
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-2 flex-1"
>
{open ? <ChevronDown className="w-4 h-4 text-gray-400" /> : <ChevronRight className="w-4 h-4 text-gray-400" />}
{icon}
<span className="text-sm font-medium">{title}</span>
{summary && <span className="text-xs text-gray-400 ml-2">{summary}</span>}
</div>
</button>
</button>
{action}
</div>
{open && <div className="px-3 pb-3 pt-1">{children}</div>}
</div>
)
+24
View File
@@ -1174,6 +1174,7 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
<thead>
<tr className="border-b text-left text-xs text-gray-500">
<th className="py-2 px-2"></th>
<th className="py-2 px-2"></th>
<th className="py-2 px-2 text-right"></th>
<th className="py-2 px-2 text-right"></th>
<th className="py-2 px-2 text-right"></th>
@@ -1203,6 +1204,29 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
)}
</div>
</td>
<td className="py-2 px-2">
{(() => {
const ct = entry.employee.contracts?.[0]?.contractType
const cfg: Record<string, { label: string; style: string }> = {
FIXED: { label: '固定期限', style: 'bg-blue-50 text-blue-700' },
UNFIXED: { label: '无固定期', style: 'bg-purple-50 text-purple-700' },
LABOR: { label: '劳务协议', style: 'bg-amber-50 text-amber-700' },
INTERNSHIP: { label: '实习协议', style: 'bg-teal-50 text-teal-700' },
PARTTIME: { label: '兼职协议', style: 'bg-cyan-50 text-cyan-700' },
OUTSOURCING: { label: '业务外包', style: 'bg-slate-50 text-slate-700' },
DISPATCH: { label: '劳务派遣', style: 'bg-cyan-50 text-cyan-700' },
UNSIGNED: { label: '未签合同', style: 'bg-red-50 text-danger' },
}
const c = cfg[ct || ''] || { label: '未签合同', style: 'bg-red-50 text-danger' }
const noSocial = ct && ['LABOR', 'INTERNSHIP', 'PARTTIME', 'OUTSOURCING', 'UNSIGNED'].includes(ct)
return (
<div className="flex flex-col gap-0.5">
<span className={`px-1.5 py-0.5 rounded text-[11px] ${c.style}`}>{c.label}</span>
{noSocial && <span className="text-[10px] text-gray-400"></span>}
</div>
)
})()}
</td>
{renderCell(entry, 'baseSalary')}
{renderCell(entry, 'overtimePay')}
{renderCell(entry, 'allowance')}
+7 -1
View File
@@ -89,6 +89,12 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
}
buildDeptOptions(departments, null, 0)
// 拉取岗位字典列表,用于职务/岗位下拉选择
const { data: positions = [] } = useQuery<any[]>({
queryKey: ['positions'],
queryFn: () => api.get('/positions').then(r => r.data),
})
const [form, setForm] = useState({
department: profile.department || '',
position: profile.position || '',
@@ -338,7 +344,7 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
)}
<div><Label></Label><Input value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} placeholder="选填" maxLength={11} /></div>
<div><Label></Label><Select value={form.education} onChange={(e) => setForm({ ...form, education: e.target.value })}><option value=""></option><option value="博士"></option><option value="硕士"></option><option value="本科"></option><option value="大专"></option><option value="高中"></option><option value="其他"></option></Select></div>
<div><Label>/</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
<div><Label>/</Label><Select value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })}><option value=""></option>{positions.map((p: any) => <option key={p.id} value={p.name}>{p.name}</option>)}</Select></div>
<div><Label></Label><Input type="date" value={form.hireDate} onChange={(e) => setForm({ ...form, hireDate: e.target.value })} /></div>
<div>
<Label></Label>
+75 -2
View File
@@ -472,6 +472,20 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
})
}
buildDeptOptions(departments, null, 0)
// 拉取岗位字典列表,用于职务/岗位下拉选择
const { data: positions = [] } = useQuery<any[]>({
queryKey: ['positions'],
queryFn: () => api.get('/positions').then(r => r.data),
})
// 根据选中部门过滤岗位:通用岗位(departmentId 为空)+ 该部门及父部门专属岗位
const getDeptAncestorIds = (deptLabel: string): string[] => {
const dept = departments.find((d: any) => d.name === deptLabel)
if (!dept) return []
const ids: string[] = []
let cur: any = dept
while (cur) { ids.push(cur.id); cur = departments.find((d: any) => d.id === cur.parentId) }
return ids
}
const defaultEndDate = (() => {
const d = new Date()
d.setFullYear(d.getFullYear() + 3)
@@ -481,7 +495,9 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
const [form, setForm] = useState({
hireDate: todayStr,
department: employee.department || '',
position: employee.position || '',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC',
signDate: '',
startDate: todayStr,
endDate: defaultEndDate,
@@ -494,6 +510,10 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
})
// 根据选中部门过滤岗位
const filteredPositions = form.department
? positions.filter((p: any) => !p.departmentId || getDeptAncestorIds(form.department).includes(p.departmentId))
: positions
const hireMonth = form.hireDate ? form.hireDate.slice(0, 7) : ''
// 计算合同月数
@@ -579,6 +599,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
const data: any = {
hireDate: new Date(form.hireDate).toISOString(),
department: form.department,
position: form.position || undefined,
baseSalary: base,
performanceSalary: perf,
monthlySalary: base + perf,
@@ -593,6 +614,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
startDate: new Date(form.startDate).toISOString(),
endDate: form.endDate ? new Date(form.endDate).toISOString() : null,
contractType: form.contractType,
signMethod: form.signMethod,
contractYears: form.contractYears,
probationMonths: form.probationMonths,
probationSalary: form.probationSalary,
@@ -620,7 +642,7 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
</div>
<div>
<Label> *</Label>
<Select value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value })}>
<Select value={form.department} onChange={(e) => setForm({ ...form, department: e.target.value, position: '' })}>
<option value=""></option>
{deptOptions.map(d => (
<option key={d.id} value={d.label}>{' '.repeat(d.level)}{d.label}</option>
@@ -628,6 +650,15 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
</Select>
</div>
</div>
<div>
<Label>/</Label>
<Select value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })}>
<option value=""></option>
{filteredPositions.map((p: any) => (
<option key={p.id} value={p.name}>{p.name}</option>
))}
</Select>
</div>
<div>
<Label> *</Label>
<Input type="date" value={form.hireDate} onChange={(e) => handleHireDateChange(e.target.value)} />
@@ -687,6 +718,18 @@ export function RehireModal({ employee, onClose, onSubmit, loading, error }: {
{contractTypes.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
</Select>
</div>
{form.contractType !== 'UNSIGNED' && (
<div className="col-span-1">
<Label></Label>
<Select value={form.signMethod} onChange={(e) => setForm({ ...form, signMethod: e.target.value as 'PAPER' | 'ELECTRONIC' })}>
<option value="PAPER"></option>
<option value="ELECTRONIC"></option>
</Select>
{form.signMethod === 'ELECTRONIC' && (
<div className="text-xs text-blue-600 mt-1"></div>
)}
</div>
)}
</div>
</div>
{form.contractType !== 'UNSIGNED' && (
@@ -776,6 +819,18 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
})
}
buildDeptOptions(departments, null, 0)
// 拉取岗位字典列表,用于职务/岗位下拉选择
const { data: positions = [] } = useQuery<any[]>({
queryKey: ['positions'],
queryFn: () => api.get('/positions').then(r => r.data),
})
// 根据选中部门过滤岗位:通用岗位(departmentId 为空)+ 该部门及父部门专属岗位
const getDeptAncestorIds = (deptId: string): string[] => {
const ids: string[] = []
let cur: any = departments.find((d: any) => d.id === deptId)
while (cur) { ids.push(cur.id); cur = departments.find((d: any) => d.id === cur.parentId) }
return ids
}
const defaultEndDate = (() => {
const d = new Date()
d.setFullYear(d.getFullYear() + 3)
@@ -793,12 +848,17 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
idCardNumber: '', gender: '男' as '男' | '女', femaleWorkerType: '' as '' | 'CADRE' | 'WORKER', phone: '',
city: '北京', education: '', status: 'ACTIVE',
contractType: 'FIXED' as 'FIXED' | 'UNFIXED' | 'UNSIGNED',
signMethod: 'PAPER' as 'PAPER' | 'ELECTRONIC',
signDate: '', startDate: todayStr, endDate: defaultEndDate,
contractYears: 3, probationMonths: 0, probationSalary: 0,
socialInsBase: '', socialInsStartMonth: '',
housingFundBase: '', housingFundStartMonth: '',
}
})
// 根据选中部门过滤岗位:通用岗位(departmentId 为空)+ 该部门及父部门专属岗位
const filteredPositions = form.departmentId
? positions.filter((p: any) => !p.departmentId || getDeptAncestorIds(form.departmentId).includes(p.departmentId))
: positions
// 持久化草稿到 localStorage,防止录入数据丢失
useEffect(() => {
@@ -1058,6 +1118,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
endDate: form.endDate ? new Date(form.endDate).toISOString() : null,
contractType: form.contractType, contractYears: form.contractYears,
probationMonths: form.probationMonths, probationSalary: form.probationSalary,
signMethod: form.signMethod,
}
}
onSubmit(data)
@@ -1093,7 +1154,7 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
const opt = deptOptions.find(d => d.id === e.target.value)
setForm({ ...form, departmentId: e.target.value, department: opt?.label || '' })
}}><option value=""></option>{deptOptions.map(d => <option key={d.id} value={d.id}>{' '.repeat(d.level)}{d.label}</option>)}</Select></div>
<div><Label>/</Label><Input value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })} placeholder="如:前端工程师" /></div>
<div><Label>/</Label><Select value={form.position} onChange={(e) => setForm({ ...form, position: e.target.value })}><option value=""></option>{filteredPositions.map((p: any) => <option key={p.id} value={p.name}>{p.name}</option>)}</Select></div>
<div><Label> *</Label><Input value={form.idCardNumber} onChange={(e) => handleIdCardChange(e.target.value)} placeholder="18位" maxLength={18} /></div>
{idCardDuplicate?.exists && (
<div className="col-span-4 px-3 py-2 rounded-md bg-amber-50 text-amber-700 text-xs flex items-center gap-2">
@@ -1266,6 +1327,18 @@ export function AddEmployeeModal({ onClose, onSubmit, loading, error }: {
<div className="text-xs text-amber-600 mt-1">//</div>
)}
</div>
{form.contractType !== 'UNSIGNED' && (
<div className="col-span-1">
<Label></Label>
<Select value={form.signMethod} onChange={(e) => setForm({ ...form, signMethod: e.target.value as 'PAPER' | 'ELECTRONIC' })}>
<option value="PAPER"></option>
<option value="ELECTRONIC"></option>
</Select>
{form.signMethod === 'ELECTRONIC' && (
<div className="text-xs text-blue-600 mt-1"></div>
)}
</div>
)}
</div>
</div>
{form.contractType !== 'UNSIGNED' && (
@@ -11,8 +11,8 @@ export function MonthlyRow({ item: i, type, onCorrected }: { item: any; type: 'a
const [expanded, setExpanded] = useState(false)
const [editing, setEditing] = useState(false)
const [editBase, setEditBase] = useState(i.base?.toString() || '')
const typeLabel = type === 'add' ? (i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
const typeClass = type === 'add' ? 'bg-green-50 text-safe' : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
const typeLabel = type === 'add' ? (i.changeType === 'PENDING' ? '待办理' : i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
const typeClass = type === 'add' ? (i.changeType === 'PENDING' ? 'bg-amber-50 text-amber-600' : 'bg-green-50 text-safe') : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
const d = i.detail
const correctMutation = useMutation({
@@ -104,8 +104,8 @@ export function MonthlyRow({ item: i, type, onCorrected }: { item: any; type: 'a
export function MonthlyHousingRow({ item: i, type, onCorrected }: { item: any; type: 'add' | 'sub' | 'normal'; onCorrected?: () => void }) {
const [editing, setEditing] = useState(false)
const [editBase, setEditBase] = useState(i.base?.toString() || '')
const typeLabel = type === 'add' ? (i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
const typeClass = type === 'add' ? 'bg-green-50 text-safe' : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
const typeLabel = type === 'add' ? (i.changeType === 'PENDING' ? '待办理' : i.changeType === 'CITY_CHANGE' ? '新增(城市变更)' : '新增') : type === 'sub' ? (i.changeType === 'CITY_CHANGE' ? '减员(城市变更)' : '减员') : '正常'
const typeClass = type === 'add' ? (i.changeType === 'PENDING' ? 'bg-amber-50 text-amber-600' : 'bg-green-50 text-safe') : type === 'sub' ? 'bg-red-50 text-danger' : 'bg-gray-100 text-gray-500'
const d = i.detail
const correctMutation = useMutation({