a2e9ba55c2
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除 P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量 P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
287 lines
15 KiB
TypeScript
287 lines
15 KiB
TypeScript
import { useState } from "react"
|
||
import { useNavigate } from "react-router-dom"
|
||
import { toast } from "sonner"
|
||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||
import { socialInsuranceApi } from '../../lib/api-services'
|
||
import Card from "../../components/ui/Card"
|
||
import Button from "../../components/ui/Button"
|
||
import { Input, Label, Select } from "../../components/ui/Input"
|
||
import Modal from "../../components/ui/Modal"
|
||
import { fmt } from "./shared"
|
||
import { ExternalLink } from "lucide-react"
|
||
|
||
/** 薪酬社保合并组件(工资条 / 缴纳记录) */
|
||
export default function PayslipSocialInfo({ payslips, monthlyProcessRecords, employeeId }: { payslips: any[]; socialInsRecords: any[]; housingFundRecords: any[]; monthlyProcessRecords: any[]; employeeId?: string }) {
|
||
const [subTab, setSubTab] = useState<'payslip' | 'monthly'>('payslip')
|
||
const navigate = useNavigate()
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex gap-1">
|
||
<button
|
||
onClick={() => setSubTab('payslip')}
|
||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${subTab === 'payslip' ? 'bg-primary text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||
>
|
||
工资条({payslips?.length || 0}条)
|
||
</button>
|
||
<button
|
||
onClick={() => setSubTab('monthly')}
|
||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${subTab === 'monthly' ? 'bg-primary text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||
>
|
||
缴纳记录({monthlyProcessRecords?.length || 0}条)
|
||
</button>
|
||
</div>
|
||
{employeeId && (
|
||
<Button variant="secondary" size="sm" onClick={() => navigate(`/money?employeeId=${employeeId}&tab=payslip`)}>
|
||
<ExternalLink className="w-3.5 h-3.5 mr-1" />
|
||
查看薪资历史
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{subTab === 'payslip' && (
|
||
<>
|
||
{!payslips?.length ? (
|
||
<Card><div className="text-center py-8 text-gray-400">暂无工资条记录</div></Card>
|
||
) : (
|
||
<Card>
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b text-xs text-gray-500">
|
||
<th className="py-2 text-left">月份</th>
|
||
<th className="py-2 text-right">基本工资</th>
|
||
<th className="py-2 text-right">加班费</th>
|
||
<th className="py-2 text-right">津贴</th>
|
||
<th className="py-2 text-right">扣款</th>
|
||
<th className="py-2 text-right">应发合计</th>
|
||
<th className="py-2 text-center">确认状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{payslips.map((p) => (
|
||
<tr key={p.id} className="border-b last:border-0 hover:bg-gray-50">
|
||
<td className="py-2">{p.month}</td>
|
||
<td className="py-2 text-right text-gray-600">¥{fmt(p.baseSalary)}</td>
|
||
<td className="py-2 text-right text-gray-600">{p.overtimePay > 0 ? `¥${fmt(p.overtimePay)}` : '-'}</td>
|
||
<td className="py-2 text-right text-gray-600">{p.allowance > 0 ? `¥${fmt(p.allowance)}` : '-'}</td>
|
||
<td className="py-2 text-right text-gray-600">{p.deduction > 0 ? `-¥${fmt(p.deduction)}` : '-'}</td>
|
||
<td className="py-2 text-right font-medium text-gray-700">¥{fmt(p.totalPay)}</td>
|
||
<td className="py-2 text-center">
|
||
{p.confirmedAt ? (
|
||
<span className="px-2 py-0.5 rounded bg-green-50 text-safe text-xs">已确认</span>
|
||
) : (
|
||
<span className="px-2 py-0.5 rounded bg-amber-50 text-amber-600 text-xs">未确认</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
<tr className="border-t-2 bg-gray-50">
|
||
<td className="py-2 font-medium">合计</td>
|
||
<td className="py-2 text-right font-medium text-gray-600">¥{fmt(payslips.reduce((s, p) => s + (p.baseSalary || 0), 0))}</td>
|
||
<td className="py-2 text-right font-medium text-gray-600">{payslips.reduce((s, p) => s + (p.overtimePay || 0), 0) > 0 ? `¥${fmt(payslips.reduce((s, p) => s + (p.overtimePay || 0), 0))}` : '-'}</td>
|
||
<td className="py-2 text-right font-medium text-gray-600">{payslips.reduce((s, p) => s + (p.allowance || 0), 0) > 0 ? `¥${fmt(payslips.reduce((s, p) => s + (p.allowance || 0), 0))}` : '-'}</td>
|
||
<td className="py-2 text-right font-medium text-gray-600">{payslips.reduce((s, p) => s + (p.deduction || 0), 0) > 0 ? `-¥${fmt(payslips.reduce((s, p) => s + (p.deduction || 0), 0))}` : '-'}</td>
|
||
<td className="py-2 text-right font-bold text-gray-700">¥{fmt(payslips.reduce((s, p) => s + (p.totalPay || 0), 0))}</td>
|
||
<td></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</Card>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{subTab === 'monthly' && (
|
||
<>
|
||
{!monthlyProcessRecords?.length ? (
|
||
<Card><div className="text-center py-8 text-gray-400">暂无缴纳记录</div></Card>
|
||
) : (
|
||
<Card>
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b text-xs text-gray-500">
|
||
<th className="py-2 text-left">办理月份</th>
|
||
<th className="py-2 text-center">类型</th>
|
||
<th className="py-2 text-center">城市</th>
|
||
<th className="py-2 text-center">状态</th>
|
||
<th className="py-2 text-right">缴费基数</th>
|
||
<th className="py-2 text-right">企业部分</th>
|
||
<th className="py-2 text-right">个人部分</th>
|
||
<th className="py-2 text-right">合计</th>
|
||
<th className="py-2 text-center">变动</th>
|
||
<th className="py-2 text-left">办理时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{monthlyProcessRecords.map((r, idx) => {
|
||
const d = r.detail
|
||
const isSocial = r.type === 'SOCIAL'
|
||
const orgAmt = isSocial ? d?.totalOrg : d?.orgAmount
|
||
const empAmt = isSocial ? d?.totalEmp : d?.empAmount
|
||
const total = isSocial ? d?.total : d?.total
|
||
return (
|
||
<tr key={idx} className="border-b last:border-0 hover:bg-gray-50">
|
||
<td className="py-2 font-medium">{r.month}</td>
|
||
<td className="py-2 text-center"><span className={`px-2 py-0.5 rounded text-xs ${isSocial ? 'bg-blue-50 text-blue-600' : 'bg-purple-50 text-purple-600'}`}>{isSocial ? '社保' : '公积金'}</span></td>
|
||
<td className="py-2 text-center text-xs text-gray-600">{r.city || '-'}</td>
|
||
<td className="py-2 text-center"><span className="px-2 py-0.5 rounded bg-green-50 text-safe text-xs">已缴纳</span></td>
|
||
<td className="py-2 text-right text-gray-600">¥{fmt(r.base)}</td>
|
||
<td className="py-2 text-right text-danger">{orgAmt != null ? `¥${fmt(orgAmt)}` : '-'}</td>
|
||
<td className="py-2 text-right text-warning">{empAmt != null ? `¥${fmt(empAmt)}` : '-'}</td>
|
||
<td className="py-2 text-right font-medium text-primary">{total != null ? `¥${fmt(total)}` : '-'}</td>
|
||
<td className="py-2 text-center text-xs text-gray-500">{r.changeType}</td>
|
||
<td className="py-2 text-gray-400 text-xs">{new Date(r.processedAt).toLocaleString('zh-CN')}</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</Card>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** 参保城市变更历史组件(含修正功能) */
|
||
export function CityHistoryTab({ socialInsRecords, housingFundRecords, changeTypeMap, changeTypeColor }: { socialInsRecords: any[]; housingFundRecords: any[]; changeTypeMap: Record<string, string>; changeTypeColor: Record<string, string> }) {
|
||
const queryClient = useQueryClient()
|
||
const [editing, setEditing] = useState<any>(null)
|
||
const [correctForm, setCorrectForm] = useState({ city: '', base: '', startMonth: '', endMonth: '', changeType: '', remark: '', reason: '' })
|
||
|
||
const correctMutation = useMutation({
|
||
mutationFn: async (data: any) => {
|
||
const cat = editing.cat
|
||
const type = cat === '社保' ? 'social' : 'housing'
|
||
return await socialInsuranceApi.correctRecord(type, editing.id, data)
|
||
},
|
||
onSuccess: () => {
|
||
toast.success('记录已修正')
|
||
queryClient.invalidateQueries({ queryKey: ['roster-profile'] })
|
||
setEditing(null)
|
||
},
|
||
onError: () => toast.error('修正失败'),
|
||
})
|
||
|
||
const handleCorrect = () => {
|
||
correctMutation.mutate({
|
||
city: correctForm.city || undefined,
|
||
base: correctForm.base ? Number(correctForm.base) : undefined,
|
||
startMonth: correctForm.startMonth || undefined,
|
||
endMonth: correctForm.endMonth || undefined,
|
||
changeType: correctForm.changeType || undefined,
|
||
remark: correctForm.remark || undefined,
|
||
reason: correctForm.reason || '数据修正',
|
||
})
|
||
}
|
||
|
||
const openCorrect = (r: any) => {
|
||
setEditing(r)
|
||
setCorrectForm({
|
||
city: r.city || '',
|
||
base: String(r.base || ''),
|
||
startMonth: r.startMonth || '',
|
||
endMonth: r.endMonth || '',
|
||
changeType: r.changeType || '',
|
||
remark: r.remark || '',
|
||
reason: '',
|
||
})
|
||
}
|
||
|
||
const allRecords = [
|
||
...(socialInsRecords || []).map((r: any) => ({ ...r, cat: '社保' })),
|
||
...(housingFundRecords || []).map((r: any) => ({ ...r, cat: '公积金' })),
|
||
].sort((a, b) => b.startMonth.localeCompare(a.startMonth))
|
||
|
||
if (!allRecords.length) return <Card><div className="text-center py-8 text-gray-400">暂无参保记录</div></Card>
|
||
|
||
return (
|
||
<>
|
||
<Card>
|
||
<div className="text-xs text-gray-500 mb-3">参保城市变更历史(社保 + 公积金记录按时间倒序,点击「修正」可直接修改错误数据并记录审计日志)</div>
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b text-xs text-gray-500">
|
||
<th className="py-2 text-left">开始年月</th>
|
||
<th className="py-2 text-left">截止年月</th>
|
||
<th className="py-2 text-center">类型</th>
|
||
<th className="py-2 text-left">参保城市</th>
|
||
<th className="py-2 text-right">缴费基数</th>
|
||
<th className="py-2 text-center">变动类型</th>
|
||
<th className="py-2 text-left">备注</th>
|
||
<th className="py-2 text-center">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{allRecords.map((r, idx) => (
|
||
<tr key={idx} className="border-b last:border-0 hover:bg-gray-50">
|
||
<td className="py-2 font-medium">{r.startMonth}</td>
|
||
<td className="py-2 text-gray-400">{r.endMonth || '至今'}</td>
|
||
<td className="py-2 text-center"><span className={`px-2 py-0.5 rounded text-xs ${r.cat === '社保' ? 'bg-blue-50 text-blue-600' : 'bg-purple-50 text-purple-600'}`}>{r.cat}</span></td>
|
||
<td className="py-2"><span className="px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 text-xs">{r.city || '-'}</span></td>
|
||
<td className="py-2 text-right text-gray-600">¥{fmt(r.base)}</td>
|
||
<td className="py-2 text-center"><span className={`px-2 py-0.5 rounded text-xs ${changeTypeColor[r.changeType] || 'bg-gray-100 text-gray-500'}`}>{changeTypeMap[r.changeType] || r.changeType}</span></td>
|
||
<td className="py-2 text-gray-400 text-xs">{r.remark || '-'}</td>
|
||
<td className="py-2 text-center"><button onClick={() => openCorrect(r)} className="text-xs text-primary hover:underline">修正</button></td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</Card>
|
||
|
||
{editing && (
|
||
<Modal open={true} onClose={() => setEditing(null)} title={`修正${editing.cat}记录`}>
|
||
<div className="space-y-3">
|
||
<div className="bg-amber-50 text-amber-700 text-xs px-3 py-2 rounded-md">
|
||
此操作将直接修改记录并写入审计日志(记录修改前后的值和修正原因),不会创建新记录。
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label>参保城市</Label>
|
||
<Input value={correctForm.city} onChange={(e) => setCorrectForm({ ...correctForm, city: e.target.value })} placeholder="如 北京" />
|
||
</div>
|
||
<div>
|
||
<Label>缴费基数</Label>
|
||
<Input type="number" value={correctForm.base} onChange={(e) => setCorrectForm({ ...correctForm, base: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label>开始年月</Label>
|
||
<Input type="month" value={correctForm.startMonth} onChange={(e) => setCorrectForm({ ...correctForm, startMonth: e.target.value })} />
|
||
</div>
|
||
<div>
|
||
<Label>截止年月</Label>
|
||
<Input type="month" value={correctForm.endMonth} onChange={(e) => setCorrectForm({ ...correctForm, endMonth: e.target.value })} placeholder="留空=至今" />
|
||
</div>
|
||
<div>
|
||
<Label>变动类型</Label>
|
||
<Select value={correctForm.changeType} onChange={(e) => setCorrectForm({ ...correctForm, changeType: e.target.value })}>
|
||
<option value="ONBOARDING">入职</option>
|
||
<option value="REHIRE">重新入职</option>
|
||
<option value="ADJUST">调基</option>
|
||
<option value="CITY_CHANGE">城市变更</option>
|
||
<option value="TERMINATION">离职/解聘</option>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label>备注</Label>
|
||
<Input value={correctForm.remark} onChange={(e) => setCorrectForm({ ...correctForm, remark: e.target.value })} />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label>修正原因(必填,写入审计日志)</Label>
|
||
<Input value={correctForm.reason} onChange={(e) => setCorrectForm({ ...correctForm, reason: e.target.value })} placeholder="如:城市录入错误" />
|
||
</div>
|
||
<div className="flex justify-end gap-2 pt-2 border-t">
|
||
<Button variant="secondary" size="sm" onClick={() => setEditing(null)}>取消</Button>
|
||
<Button size="sm" onClick={handleCorrect} disabled={correctMutation.isPending}>{correctMutation.isPending ? '保存中...' : '确认修正'}</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)}
|
||
</>
|
||
)
|
||
}
|