feat: 社保AI建议、合同附件多文件上传预览、删除合同、扩展上传格式
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useConfirm } from '../hooks/useConfirm'
|
||||
import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History, Download, AlertCircle, Clock } from 'lucide-react'
|
||||
import { Calculator, Info, Check, Settings as SettingsIcon, Plus, History, Download, AlertCircle, Clock, MapPin, Sparkles } from 'lucide-react'
|
||||
import api from '../lib/api'
|
||||
import Card from '../components/ui/Card'
|
||||
import Button from '../components/ui/Button'
|
||||
@@ -16,6 +16,8 @@ export default function SocialInsurance() {
|
||||
const confirm = useConfirm()
|
||||
const [tab, setTab] = useState<'monthly' | 'social' | 'housing' | 'deduction'>('monthly')
|
||||
const [city, setCity] = useState<string>('北京')
|
||||
const [showAddCity, setShowAddCity] = useState(false)
|
||||
const [newCityName, setNewCityName] = useState('')
|
||||
const [base, setBase] = useState(8000)
|
||||
const [deductionMonth, setDeductionMonth] = useState(new Date().toISOString().slice(0, 7))
|
||||
const [showNewVersion, setShowNewVersion] = useState(false)
|
||||
@@ -161,14 +163,14 @@ export default function SocialInsurance() {
|
||||
|
||||
const { data: result, mutate: calcMutate, isPending } = useMutation<any>({
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/social/calculate', { base }) as any
|
||||
const res = await api.post('/social/calculate', { base, city }) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
|
||||
const { data: housingResult, mutate: calcHousingMutate, isPending: housingCalcPending } = useMutation<any>({
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/social/housing-calculate', { base }) as any
|
||||
const res = await api.post('/social/housing-calculate', { base, city }) as any
|
||||
return res.data
|
||||
},
|
||||
})
|
||||
@@ -193,6 +195,45 @@ export default function SocialInsurance() {
|
||||
},
|
||||
})
|
||||
|
||||
const aiSuggestMut = useMutation<any, any, { city: string; effectiveFrom: string; type: 'social' | 'housing' }>({
|
||||
mutationFn: async (vars: { city: string; effectiveFrom: string; type: 'social' | 'housing' }) => {
|
||||
const res = await api.post('/social/ai-suggest', vars) as any
|
||||
return res.data
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (isHousing) {
|
||||
activeSetNewVersion({
|
||||
...activeNewVersion,
|
||||
baseMin: data.baseMin ?? activeNewVersion.baseMin,
|
||||
baseMax: data.baseMax ?? activeNewVersion.baseMax,
|
||||
housingOrg: data.housingOrg ?? activeNewVersion.housingOrg,
|
||||
housingEmp: data.housingEmp ?? activeNewVersion.housingEmp,
|
||||
})
|
||||
} else {
|
||||
activeSetNewVersion({
|
||||
...activeNewVersion,
|
||||
baseMin: data.baseMin ?? activeNewVersion.baseMin,
|
||||
baseMax: data.baseMax ?? activeNewVersion.baseMax,
|
||||
medicalBaseMin: data.medicalBaseMin ?? 0,
|
||||
medicalBaseMax: data.medicalBaseMax ?? 0,
|
||||
pensionOrg: data.pensionOrg ?? activeNewVersion.pensionOrg,
|
||||
pensionEmp: data.pensionEmp ?? activeNewVersion.pensionEmp,
|
||||
medicalOrg: data.medicalOrg ?? activeNewVersion.medicalOrg,
|
||||
medicalEmp: data.medicalEmp ?? activeNewVersion.medicalEmp,
|
||||
unemploymentOrg: data.unemploymentOrg ?? activeNewVersion.unemploymentOrg,
|
||||
unemploymentEmp: data.unemploymentEmp ?? activeNewVersion.unemploymentEmp,
|
||||
injuryOrg: data.injuryOrg ?? activeNewVersion.injuryOrg,
|
||||
maternityOrg: data.maternityOrg ?? activeNewVersion.maternityOrg,
|
||||
extraInsurances: data.extraInsurances ?? [],
|
||||
})
|
||||
}
|
||||
toast.success('AI建议已填入,请核对后保存')
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('AI建议获取失败,请手动填写')
|
||||
},
|
||||
})
|
||||
|
||||
const previewAdjustMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await api.get(`/social/config/${config?.id}/adjust-preview`) as any
|
||||
@@ -337,16 +378,52 @@ export default function SocialInsurance() {
|
||||
{(tab === 'social' || tab === 'housing') && (
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<label className="text-sm text-gray-500">城市:</label>
|
||||
<input
|
||||
list="social-cities"
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
placeholder="输入或选择城市"
|
||||
/>
|
||||
<datalist id="social-cities">
|
||||
{cities.map((c) => <option key={c} value={c} />)}
|
||||
</datalist>
|
||||
{showAddCity ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
className="h-9 w-24 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
value={newCityName}
|
||||
onChange={(e) => setNewCityName(e.target.value)}
|
||||
placeholder="城市名"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && newCityName.trim()) {
|
||||
setCity(newCityName.trim())
|
||||
setNewCityName('')
|
||||
setShowAddCity(false)
|
||||
queryClient.invalidateQueries({ queryKey: ['social-config-cities'] })
|
||||
}
|
||||
if (e.key === 'Escape') { setShowAddCity(false); setNewCityName('') }
|
||||
}}
|
||||
/>
|
||||
<button className="h-9 px-2 text-xs text-primary" onClick={() => {
|
||||
if (newCityName.trim()) {
|
||||
setCity(newCityName.trim())
|
||||
setNewCityName('')
|
||||
setShowAddCity(false)
|
||||
queryClient.invalidateQueries({ queryKey: ['social-config-cities'] })
|
||||
}
|
||||
}}>确定</button>
|
||||
<button className="h-9 px-2 text-xs text-gray-400" onClick={() => { setShowAddCity(false); setNewCityName('') }}>取消</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<select
|
||||
className="h-9 rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-700 shadow-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
|
||||
value={city}
|
||||
onChange={(e) => setCity(e.target.value)}
|
||||
>
|
||||
{cities.map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
<button
|
||||
className="h-9 w-9 flex items-center justify-center rounded-md border border-gray-200 bg-white text-gray-400 hover:text-primary hover:border-primary transition"
|
||||
title="添加新城市"
|
||||
onClick={() => setShowAddCity(true)}
|
||||
>
|
||||
<MapPin className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -415,6 +492,17 @@ export default function SocialInsurance() {
|
||||
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">失业(企业/个人)</span><span className="font-medium">{activeConfig.unemploymentOrg}% / {activeConfig.unemploymentEmp}%</span></div>
|
||||
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">工伤(企业)</span><span className="font-medium">{activeConfig.injuryOrg}%</span></div>
|
||||
<div className="flex justify-between border-b pb-1.5"><span className="text-gray-500">生育(企业)</span><span className="font-medium">{activeConfig.maternityOrg}%</span></div>
|
||||
{Array.isArray(activeConfig.extraInsurances) && activeConfig.extraInsurances.map((ins: any, idx: number) => (
|
||||
<div key={idx} className="flex justify-between border-b pb-1.5">
|
||||
<span className="text-gray-500">{ins.name}(企业/个人)</span>
|
||||
<span className="font-medium">
|
||||
{ins.baseType === 'fixed'
|
||||
? `¥${ins.fixedAmount} / ¥${ins.empFixedAmount || 0}`
|
||||
: `${ins.orgRate}% / ${ins.empRate}%`
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -573,9 +661,22 @@ export default function SocialInsurance() {
|
||||
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<div>新版本生效后,当前版本将自动归档。发薪批次计算时按批次月份匹配对应版本的配置。通常每年7月调基时新建版本。</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 font-medium"
|
||||
onClick={() => aiSuggestMut.mutate({ city: activeNewVersion.city, effectiveFrom: activeNewVersion.effectiveFrom, type: isHousing ? 'housing' : 'social' })}
|
||||
disabled={aiSuggestMut.isPending}
|
||||
>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
{aiSuggestMut.isPending ? 'AI获取中...' : 'AI建议 — 根据城市和年份自动填充最新政策'}
|
||||
</button>
|
||||
<div className="grid md:grid-cols-3 gap-3">
|
||||
<div><Label>生效月份</Label><Input type="month" value={activeNewVersion.effectiveFrom} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, effectiveFrom: e.target.value })} /></div>
|
||||
<div><Label>城市</Label><Input value={activeNewVersion.city} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, city: e.target.value })} /></div>
|
||||
<div><Label>城市</Label>
|
||||
<select className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm" value={activeNewVersion.city} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, city: e.target.value })}>
|
||||
{[...new Set([city, ...cities])].map((c) => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div><Label>缴费基数下限</Label><Input type="number" value={activeNewVersion.baseMin} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, baseMin: Number(e.target.value) })} /></div>
|
||||
<div><Label>缴费基数上限</Label><Input type="number" value={activeNewVersion.baseMax} onChange={(e) => activeSetNewVersion({ ...activeNewVersion, baseMax: Number(e.target.value) })} /></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user