Files
TurboHR/frontend/src/pages/roster/PayslipSocialInfo.tsx
T
freedakgmail a2e9ba55c2 feat: 20260809 系统优化 - 全部28项问题修复(P0×6+P1×16+P2×6)
P0: 福利批量参保/离职证明下载防乱码/考勤模板合并Sheet/补卡修改/附件在线查看删除
P1: 分页pageSize修复/离职导出筛选/撤回删除草稿/加班费自动计算/考勤加班汇总/证据链异常详情/制度催办/模板导入Word/社保封顶保底/校验字段提示/职务字段/社保费用明细/弹窗防误关/身份证查重/证明员工下拉/培训批量
P2: 离职流程去重/社保基数覆盖输入/薪税入口改名/添加员工引导/绩效模板清理
2026-08-09 11:59:02 +08:00

287 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)}
</>
)
}