feat: 20260805 系统优化 - 身份证复制fallback/薪税日期筛选/社保版本修复/证据链导出/违纪证明/医疗期政策/绩效类型评级/合同作废/帮助更新

This commit is contained in:
freedakgmail
2026-08-05 20:26:16 +08:00
parent a5901d648e
commit c355a7d208
24 changed files with 1185 additions and 257 deletions
+187 -2
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle, ClipboardList, LayoutGrid, PenTool } from 'lucide-react'
import { Building2, Users, CreditCard, Plus, Bell, Download, Upload, FileSpreadsheet, Clock, CheckCircle, AlertCircle, ClipboardList, LayoutGrid, PenTool, HeartPulse, Trash2 } from 'lucide-react'
import { settingsApi, notificationsApi } from '../lib/api-services'
import { useAuthStore } from '../store/authStore'
import { getPageSize, setPageSize as setGlobalPageSize } from '../lib/pageSize'
@@ -14,7 +14,7 @@ import { useConfirm } from '../hooks/useConfirm'
export default function Settings() {
const queryClient = useQueryClient()
const [activeSection, setActiveSection] = useState<'org' | 'users' | 'plan' | 'notifications' | 'retirement' | 'import' | 'export'>('org')
const [activeSection, setActiveSection] = useState<'org' | 'users' | 'plan' | 'notifications' | 'retirement' | 'medical' | 'import' | 'export'>('org')
const { data: orgData } = useQuery<any>({
queryKey: ['org-settings'],
@@ -41,6 +41,7 @@ export default function Settings() {
{ key: 'plan' as const, label: '套餐', icon: CreditCard },
{ key: 'notifications' as const, label: '通知设置', icon: Bell },
{ key: 'retirement' as const, label: '退休提醒', icon: Clock },
{ key: 'medical' as const, label: '医疗期政策', icon: HeartPulse },
{ key: 'import' as const, label: '数据导入', icon: FileSpreadsheet },
{ key: 'export' as const, label: '数据导出', icon: Download },
]
@@ -82,6 +83,7 @@ export default function Settings() {
{activeSection === 'retirement' && (
<RetirementSettings orgData={orgData} onSave={(data) => updateOrgMutation.mutate(data)} />
)}
{activeSection === 'medical' && <MedicalPeriodSettings />}
{activeSection === 'import' && <ImportSettings />}
{activeSection === 'export' && <ExportSettings />}
</div>
@@ -1387,3 +1389,186 @@ function MonthlyImport() {
)
}
function MedicalPeriodSettings() {
const queryClient = useQueryClient()
const [showForm, setShowForm] = useState(false)
const [editPolicy, setEditPolicy] = useState<any>(null)
const { data: policies = [], isLoading } = useQuery<any[]>({
queryKey: ['medical-period-policies'],
queryFn: () => settingsApi.medicalPeriodPolicies(),
})
const saveMut = useMutation({
mutationFn: (data: any) => settingsApi.saveMedicalPeriodPolicy(data),
onSuccess: () => {
toast.success('政策已保存')
queryClient.invalidateQueries({ queryKey: ['medical-period-policies'] })
setShowForm(false)
setEditPolicy(null)
},
onError: () => toast.error('保存失败'),
})
const deleteMut = useMutation({
mutationFn: (id: string) => settingsApi.deleteMedicalPeriodPolicy(id),
onSuccess: () => {
toast.success('已删除')
queryClient.invalidateQueries({ queryKey: ['medical-period-policies'] })
},
onError: () => toast.error('删除失败'),
})
return (
<div className="space-y-3">
<Card>
<div className="flex items-center justify-between mb-3">
<div>
<h2 className="text-sm font-medium"></h2>
<p className="text-xs text-gray-500 mt-0.5"></p>
</div>
<Button size="sm" onClick={() => { setEditPolicy(null); setShowForm(true) }}>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
{isLoading ? (
<div className="py-8 text-center text-gray-400 text-sm">...</div>
) : policies.length === 0 ? (
<div className="py-8 text-center text-gray-400 text-sm"></div>
) : (
<div className="space-y-2">
{policies.map((p: any) => (
<div key={p.id} className="border rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{p.region}</span>
{p.isDefault && <span className="text-xs px-1.5 py-0.5 rounded bg-primary/10 text-primary"></span>}
</div>
<div className="flex gap-1">
<button onClick={() => { setEditPolicy(p); setShowForm(true) }} className="text-xs text-primary hover:underline"></button>
{!p.isDefault && (
<button onClick={() => { if (confirm(`确认删除「${p.region}」政策?`)) deleteMut.mutate(p.id) }} className="p-1 hover:bg-gray-100 rounded">
<Trash2 className="w-3.5 h-3.5 text-red-400" />
</button>
)}
</div>
</div>
<div className="text-xs text-gray-500 mb-2">{p.legalBasis}</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b text-left text-gray-400">
<th className="py-1 pr-3 font-medium"></th>
<th className="py-1 pr-3 font-medium"></th>
<th className="py-1 pr-3 font-medium"></th>
</tr>
</thead>
<tbody className="divide-y">
{p.rules.map((rule: any, idx: number) => {
const prevMax = idx > 0 ? p.rules[idx - 1].maxYears : 0
const isLast = idx === p.rules.length - 1
return (
<tr key={idx}>
<td className="py-1 pr-3">{isLast ? `${prevMax} 年以上` : `${prevMax}-${rule.maxYears}`}</td>
<td className="py-1 pr-3">{rule.months} </td>
<td className="py-1 pr-3">{rule.cycleMonths} </td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
))}
</div>
)}
</Card>
{showForm && (
<MedicalPolicyForm
policy={editPolicy}
onSave={(data) => saveMut.mutate(data)}
onClose={() => { setShowForm(false); setEditPolicy(null) }}
/>
)}
</div>
)
}
function MedicalPolicyForm({ policy, onSave, onClose }: { policy: any; onSave: (data: any) => void; onClose: () => void }) {
const [region, setRegion] = useState(policy?.region || '')
const [legalBasis, setLegalBasis] = useState(policy?.legalBasis || '')
const [isDefault, setIsDefault] = useState(policy?.isDefault || false)
const [rules, setRules] = useState<any[]>(
policy?.rules?.length ? policy.rules : [{ maxYears: 5, months: 3, cycleMonths: 6 }]
)
const addRule = () => setRules([...rules, { maxYears: 10, months: 6, cycleMonths: 12 }])
const updateRule = (idx: number, field: string, value: number) => {
setRules(rules.map((r, i) => i === idx ? { ...r, [field]: value } : r))
}
const removeRule = (idx: number) => {
if (rules.length <= 1) return
setRules(rules.filter((_, i) => i !== idx))
}
const handleSave = () => {
if (!region.trim()) { toast.error('请输入地区名称'); return }
if (!legalBasis.trim()) { toast.error('请输入法律依据'); return }
const sortedRules = [...rules].sort((a, b) => a.maxYears - b.maxYears)
onSave({ region: region.trim(), legalBasis: legalBasis.trim(), rules: sortedRules, isDefault })
}
return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50" onClick={onClose}>
<div className="bg-white rounded-lg p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium">{policy ? '编辑政策' : '新增政策'}</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"></button>
</div>
<div className="space-y-3">
<div>
<Label></Label>
<Input value={region} onChange={(e) => setRegion(e.target.value)} placeholder="如:广东" disabled={!!policy?.isDefault} />
</div>
<div>
<Label></Label>
<Input value={legalBasis} onChange={(e) => setLegalBasis(e.target.value)} placeholder="如:《广东省...》" />
</div>
<div>
<Label></Label>
<div className="space-y-2">
{rules.map((rule, idx) => (
<div key={idx} className="flex items-center gap-2">
<span className="text-xs text-gray-500 whitespace-nowrap"> &lt;</span>
<input type="number" value={rule.maxYears} onChange={(e) => updateRule(idx, 'maxYears', parseInt(e.target.value) || 0)} className="w-16 px-2 py-1 text-xs border rounded" />
<span className="text-xs text-gray-500"> </span>
<input type="number" value={rule.months} onChange={(e) => updateRule(idx, 'months', parseInt(e.target.value) || 0)} className="w-16 px-2 py-1 text-xs border rounded" />
<span className="text-xs text-gray-500"></span>
<input type="number" value={rule.cycleMonths} onChange={(e) => updateRule(idx, 'cycleMonths', parseInt(e.target.value) || 0)} className="w-16 px-2 py-1 text-xs border rounded" />
<span className="text-xs text-gray-500"></span>
{rules.length > 1 && (
<button onClick={() => removeRule(idx)} className="p-1 hover:bg-gray-100 rounded">
<Trash2 className="w-3 h-3 text-red-400" />
</button>
)}
</div>
))}
<button onClick={addRule} className="text-xs text-primary hover:underline">+ </button>
</div>
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={isDefault} onChange={(e) => setIsDefault(e.target.checked)} disabled={!!policy?.isDefault} />
<span></span>
</label>
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" onClick={onClose}></Button>
<Button onClick={handleSave}></Button>
</div>
</div>
</div>
</div>
)
}