feat: 系统优化Phase2 - 面包屑导航/侧边栏间距/制度公示阅读签收/模板变量中文化/通知类型补全

- 面包屑导航组件,集成至TopNav header
- 侧边栏菜单分组间距增大,分组间分隔线
- 制度公示员工阅读签收:PolicyReadRecord模型、portal路由、管理端阅读统计
- 修复Policies.tsx民主程序推进bug(字段名/API路径/参数)
- 用工文本模板变量名英文转中文显示
- 通知类型TYPE_LABELS补全(RISK_ALERT/SOCIAL_INS/OVERTIME_ALERT/PAYSLIP_READY)
- 通知示例数据补充
- h2标题统一为text-sm font-medium
- 新增run.md
This commit is contained in:
selfrelease
2026-07-26 20:32:38 +08:00
parent 9cb0d1f63b
commit d79e3baa34
71 changed files with 18561 additions and 3230 deletions
@@ -0,0 +1,282 @@
import { useState, useRef } from "react"
import { toast } from "sonner"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import api from "../../lib/api"
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 { Users, FileText, AlertTriangle, Calendar, GraduationCap, TrendingUp, Scale, X, Plus, Paperclip, Trash2, Printer, Calculator, Shield, Info, Check, Eye, Download, UserX, UserPlus, Briefcase, FileSignature, DollarSign, Building2, RotateCcw, History } from "lucide-react"
import { fmt } from "./shared"
/** 薪酬社保合并组件(工资条 / 缴纳记录) */
export default function PayslipSocialInfo({ payslips, monthlyProcessRecords }: { payslips: any[]; socialInsRecords: any[]; housingFundRecords: any[]; monthlyProcessRecords: any[] }) {
const [subTab, setSubTab] = useState<'payslip' | 'monthly'>('payslip')
const changeTypeMap: Record<string, string> = { ONBOARDING: '入职', REHIRE: '重新入职', ADJUST: '调基', TERMINATION: '离职/解聘', CITY_CHANGE: '城市变更' }
const changeTypeColor: Record<string, string> = { ONBOARDING: 'bg-green-50 text-safe', REHIRE: 'bg-blue-50 text-blue-600', ADJUST: 'bg-amber-50 text-amber-600', TERMINATION: 'bg-red-50 text-danger', CITY_CHANGE: 'bg-cyan-50 text-cyan-600' }
return (
<div className="space-y-3">
<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>
{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 url = cat === '社保'
? `/social/records/social/${editing.id}/correct`
: `/social/records/housing/${editing.id}/correct`
const res = await api.put(url, data) as any
return res.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>
)}
</>
)
}