feat: 20260805 系统优化 - 身份证复制fallback/薪税日期筛选/社保版本修复/证据链导出/违纪证明/医疗期政策/绩效类型评级/合同作废/帮助更新
This commit is contained in:
@@ -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">工龄 <</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user