feat: 节假日配置、排班管理独立页面、考勤加班显示优化
- 新增 HolidayConfig 模型,支持法定节假日和调休工作日配置 - 加班费同步逻辑改用 HolidayConfig 判断日期类型 - 员工端考勤显示加班工时、费率和日期类型 - 周末/节假日出勤状态显示为"周末出勤"/"节假日出勤" - 新增 Employee.defaultShiftId 字段,支持长期排班(工作日班次) - 排班管理拆分为独立页面(班次管理+排班),考勤管理保留出勤相关功能 - 排班和每日出勤页面增加身份证号列 - 修复岗位和部门编辑失败问题(POST 改 PUT) - 新增 backfill 脚本:合同薪资回填、默认班次回填 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,14 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [fileType, setFileType] = useState<'ID_CARD' | 'BANK_CARD' | 'EDUCATION' | 'CERTIFICATE' | 'CONTRACT' | 'PHOTO' | 'OTHER'>('ID_CARD')
|
||||
|
||||
// 判断最新合同类型,劳务/实习/兼职/外包等非劳动合同不缴纳社保公积金
|
||||
const latestContract = profile.contracts?.[0]
|
||||
const isNoSocialContract = latestContract && ['LABOR', 'INTERNSHIP', 'PARTTIME', 'OUTSOURCING', 'UNSIGNED'].includes(latestContract.contractType)
|
||||
|
||||
// 从在保记录中提取社保/公积金账户名
|
||||
const activeSocialAccount = profile.socialInsRecords?.find((r: any) => !r.endMonth)?.account
|
||||
const activeHousingAccount = profile.housingFundRecords?.find((r: any) => !r.endMonth)?.account
|
||||
|
||||
const addAttachmentMutation = useMutation({
|
||||
mutationFn: (data: any) => attachmentApi.add(data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['roster-profile', employeeId] }),
|
||||
@@ -136,6 +144,18 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
enabled: !editing && !!profile.socialInsBase && !!profile.city,
|
||||
})
|
||||
|
||||
// 查询公积金费用明细
|
||||
const { data: housingDetail } = useQuery<any>({
|
||||
queryKey: ['housing-calc', profile.id, profile.housingFundBase, profile.city],
|
||||
queryFn: async () => {
|
||||
if (!profile.housingFundBase || !profile.city) return null
|
||||
try {
|
||||
return await socialInsuranceApi.housingCalculate(Number(profile.housingFundBase), profile.city)
|
||||
} catch { return null }
|
||||
},
|
||||
enabled: !editing && !!profile.housingFundBase && !!profile.city,
|
||||
})
|
||||
|
||||
const handleSave = async () => {
|
||||
if (form.city !== (profile.city || '') && !form.cityChangeReason.trim()) {
|
||||
toast.error('参保城市变更必须填写变更原因')
|
||||
@@ -344,26 +364,27 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 薪税信息 */}
|
||||
{/* 社保公积金信息(劳务/实习/兼职/外包等非劳动合同不显示) */}
|
||||
{!isNoSocialContract && (
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h3 className="text-xs font-medium text-gray-600 mb-3">薪税信息</h3>
|
||||
<h3 className="text-xs font-medium text-gray-600 mb-3">社保公积金信息</h3>
|
||||
{!editing ? (
|
||||
<div className="grid md:grid-cols-4 gap-4">
|
||||
<div className="flex justify-between border-b pb-2 text-xs">
|
||||
<span className="text-gray-500">参保城市</span>
|
||||
<span className="font-medium">{profile.city || '未设置'}</span>
|
||||
<span className="text-gray-500">社保账户</span>
|
||||
<span className="font-medium">{activeSocialAccount?.name || profile.city || '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-2 text-xs">
|
||||
<span className="text-gray-500">社保缴费基数</span>
|
||||
<span className="font-medium">{profile.socialInsBase ? `¥${fmt(profile.socialInsBase)}` : '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-2 text-xs">
|
||||
<span className="text-gray-500">公积金缴费基数</span>
|
||||
<span className="font-medium">{profile.housingFundBase ? `¥${fmt(profile.housingFundBase)}` : '未设置'}</span>
|
||||
<span className="text-gray-500">公积金账户</span>
|
||||
<span className="font-medium">{activeHousingAccount?.name || profile.city || '未设置'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b pb-2 text-xs">
|
||||
<span className="text-gray-500">专项附加扣除</span>
|
||||
<span className="font-medium">{profile.specialDeduction ? `¥${fmt(profile.specialDeduction)}/月` : '¥0.00/月'}</span>
|
||||
<span className="text-gray-500">公积金缴费基数</span>
|
||||
<span className="font-medium">{profile.housingFundBase ? `¥${fmt(profile.housingFundBase)}` : '未设置'}</span>
|
||||
</div>
|
||||
{socialDetail?.items?.length > 0 && (
|
||||
<div className="md:col-span-4 mt-2">
|
||||
@@ -384,24 +405,45 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{housingDetail && (
|
||||
<div className="md:col-span-4 mt-2">
|
||||
<div className="text-xs font-medium text-gray-600 mb-2">公积金费用明细</div>
|
||||
<div className="grid md:grid-cols-3 gap-2">
|
||||
<div className="px-2 py-1.5 rounded bg-gray-50 text-xs">
|
||||
<div className="font-medium text-gray-700">企业公积金({housingDetail.orgRate}%)</div>
|
||||
<div className="text-gray-500 mt-0.5">¥{fmt(housingDetail.housingOrg)}</div>
|
||||
</div>
|
||||
<div className="px-2 py-1.5 rounded bg-gray-50 text-xs">
|
||||
<div className="font-medium text-gray-700">个人公积金({housingDetail.empRate}%)</div>
|
||||
<div className="text-gray-500 mt-0.5">¥{fmt(housingDetail.housingEmp)}</div>
|
||||
</div>
|
||||
<div className="px-2 py-1.5 rounded bg-gray-50 text-xs">
|
||||
<div className="font-medium text-gray-700">合计</div>
|
||||
<div className="text-gray-500 mt-0.5">¥{fmt(housingDetail.total)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{housingDetail.capped && <div className="text-xs text-amber-600 mt-1">提示:公积金基数已封顶(上限 ¥{fmt(housingDetail.actualBase)})</div>}
|
||||
{housingDetail.floored && <div className="text-xs text-amber-600 mt-1">提示:公积金基数已保底(下限 ¥{fmt(housingDetail.actualBase)})</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Label>参保城市</Label>
|
||||
<Input placeholder="如 北京" value={form.city || ''} onChange={(e) => setForm({ ...form, city: e.target.value })} />
|
||||
<Label>社保账户</Label>
|
||||
<Input placeholder="如 北京社保" value={form.city || ''} onChange={(e) => setForm({ ...form, city: e.target.value })} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>社保缴费基数</Label>
|
||||
<Input type="number" placeholder="按人核定" value={form.socialInsBase} onChange={(e) => setForm({ ...form, socialInsBase: e.target.value })} onFocus={(e) => e.target.select()} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>公积金缴费基数</Label>
|
||||
<Input type="number" placeholder="按人核定" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} />
|
||||
<Label>公积金账户</Label>
|
||||
<Input placeholder="如 北京公积金" value={form.city || ''} onChange={(e) => setForm({ ...form, city: e.target.value })} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<Label>专项附加扣除(元/月)</Label>
|
||||
<Input type="number" placeholder="子女教育、赡养老人等" value={form.specialDeduction} onChange={(e) => setForm({ ...form, specialDeduction: Number(e.target.value) || 0 })} />
|
||||
<Label>公积金缴费基数</Label>
|
||||
<Input type="number" placeholder="按人核定" value={form.housingFundBase} onChange={(e) => setForm({ ...form, housingFundBase: e.target.value })} onFocus={(e) => e.target.select()} />
|
||||
</div>
|
||||
{form.city !== (profile.city || '') && (
|
||||
<div className="md:col-span-4">
|
||||
@@ -411,7 +453,7 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-2">社保/公积金基数按上年度月均工资核定,每年7月调整。专项附加扣除由员工在portal端填报,无则为0。</p>
|
||||
<p className="text-xs text-gray-400 mt-2">社保/公积金基数按上年度月均工资核定,每年7月调整。</p>
|
||||
{!editing && (!profile.socialInsBase || !profile.housingFundBase) && (
|
||||
<div className="mt-2 flex items-center gap-2 px-3 py-2 rounded-md bg-amber-50 text-warning text-xs">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
@@ -419,6 +461,26 @@ export default function BasicInfo({ profile, employeeId, attachments }: { profil
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 专项附加扣除(独立区域,所有合同类型都显示) */}
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
<h3 className="text-xs font-medium text-gray-600 mb-3">专项附加扣除</h3>
|
||||
{!editing ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-500">每月扣除额</span>
|
||||
<span className="text-sm font-medium">{profile.specialDeduction ? `¥${fmt(profile.specialDeduction)}/月` : '¥0.00/月'}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<Label>专项附加扣除(元/月)</Label>
|
||||
<Input type="number" placeholder="子女教育、赡养老人等" value={form.specialDeduction} onChange={(e) => setForm({ ...form, specialDeduction: Number(e.target.value) || 0 })} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-gray-400 mt-2">由员工在portal端填报,无则为0。</p>
|
||||
</div>
|
||||
|
||||
{/* 特殊状态 */}
|
||||
<div className="mt-4 pt-4 border-t">
|
||||
|
||||
Reference in New Issue
Block a user