feat: P2+P3 薪酬模板驱动计算 - SYSTEM注册表 + 前端动态化

P2: 社保/公积金/个税改为 SYSTEM 注册表驱动
- 新增 SystemCalcContext 和 systemCalculators 注册表
- ensureSocialHousingCalculated 缓存社保公积金计算
- calcTaxSystem 提取个税计算为系统函数
- calcBatchEntry 重构为模板拓扑排序调度

P3: 前端批次表格动态化 + 模板编辑器增强
- 后端模板 CRUD 支持 calcType/dependencies 字段
- TemplateTab 表格增加计算方式列
- TemplateTab 编辑表单增加 calcType 选择(公式/系统)
- BatchTab editableFields 从模板 isEditable 动态获取
This commit is contained in:
freedakgmail
2026-08-19 08:32:57 +08:00
parent 8bcf1460cf
commit 60951e7fd6
4 changed files with 302 additions and 197 deletions
+20 -4
View File
@@ -497,6 +497,13 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
},
})
const { data: templateItems } = useQuery<any[]>({
queryKey: ['payslip-template'],
queryFn: async () => {
return await payrollApi.template()
},
})
const updateEntryMutation = useMutation({
mutationFn: ({ employeeId, data }: { employeeId: string; data: any }) =>
payrollApi.updateBatchEntry(batchId, employeeId, data),
@@ -729,10 +736,19 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
}
}
// 可编辑的输入项字段
const editableFields = isBonus
? ['bonus']
: ['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg']
// 可编辑的输入项字段:从模板动态获取,回退到硬编码
const editableFields = (() => {
if (templateItems && templateItems.length > 0) {
const editable = templateItems.filter((i: any) => i.isEditable).map((i: any) => i.code)
// 奖金批次只允许编辑 bonus
if (isBonus) return ['bonus']
// 社保/公积金允许手动覆盖
return [...editable, 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg']
}
return isBonus
? ['bonus']
: ['baseSalary', 'performanceSalary', 'overtimePay', 'allowance', 'deduction', 'bonus', 'socialEmp', 'housingEmp', 'socialOrg', 'housingOrg']
})()
// 点击单元格进入编辑
const startEdit = (employeeId: string, field: string, currentValue: number) => {
+43 -11
View File
@@ -23,6 +23,7 @@ export function TemplateManager() {
code: '',
type: 'INPUT' as 'INPUT' | 'CALCULATED',
formula: '',
calcType: 'FORMULA' as 'FORMULA' | 'SYSTEM',
order: 99,
isEditable: true,
})
@@ -67,7 +68,7 @@ export function TemplateManager() {
/** 打开新增表单 */
const handleAdd = () => {
setEditingItem(null)
setForm({ name: '', code: '', type: 'INPUT', formula: '', order: items?.length ? items.length + 1 : 99, isEditable: true })
setForm({ name: '', code: '', type: 'INPUT', formula: '', calcType: 'FORMULA', order: items?.length ? items.length + 1 : 99, isEditable: true })
setShowForm(true)
}
@@ -79,6 +80,7 @@ export function TemplateManager() {
code: item.code,
type: item.type,
formula: item.formula || '',
calcType: item.calcType || 'FORMULA',
order: item.order,
isEditable: item.isEditable,
})
@@ -91,10 +93,11 @@ export function TemplateManager() {
toast.error('名称和字段代码不能为空')
return
}
const payload = {
const payload: any = {
name: form.name.trim(),
type: form.type,
formula: form.type === 'CALCULATED' ? form.formula.trim() || null : null,
calcType: form.type === 'CALCULATED' ? form.calcType : 'FORMULA',
order: Number(form.order),
isEditable: form.isEditable,
}
@@ -128,6 +131,7 @@ export function TemplateManager() {
<th className="py-2 px-2 w-24"></th>
<th className="py-2 px-2 w-32"></th>
<th className="py-2 px-2 w-20"></th>
<th className="py-2 px-2 w-24"></th>
<th className="py-2 px-2 w-48"></th>
<th className="py-2 px-2 w-16"></th>
<th className="py-2 px-2 text-right w-24"></th>
@@ -144,6 +148,13 @@ export function TemplateManager() {
{item.type === 'INPUT' ? '输入项' : '计算项'}
</span>
</td>
<td className="py-2 px-2">
{item.type === 'CALCULATED' && (
<span className={`px-2 py-0.5 rounded text-xs whitespace-nowrap ${(item.calcType || 'FORMULA') === 'SYSTEM' ? 'bg-amber-50 text-amber-600' : 'bg-teal-50 text-teal-600'}`}>
{(item.calcType || 'FORMULA') === 'SYSTEM' ? '系统计算' : '公式计算'}
</span>
)}
</td>
<td className="py-2 px-2 text-gray-500 font-mono text-xs max-w-48 truncate" title={item.formula || ''}>{item.formula || '—'}</td>
<td className="py-2 px-2">
<span className={`text-xs ${item.isEditable ? 'text-safe' : 'text-gray-500'}`}>
@@ -240,15 +251,36 @@ export function TemplateManager() {
</div>
</div>
{form.type === 'CALCULATED' && (
<div>
<Label></Label>
<Input
type="text"
value={form.formula}
onChange={(e) => setForm({ ...form, formula: e.target.value })}
placeholder="如:baseSalary + overtimePay + allowance - deduction"
/>
<p className="text-xs text-gray-500 mt-1"></p>
<div className="space-y-2">
<div>
<Label></Label>
<Select
value={form.calcType}
onChange={(e) => setForm({ ...form, calcType: e.target.value as 'FORMULA' | 'SYSTEM' })}
disabled={!!editingItem && editingItem.isDefault}
>
<option value="FORMULA"></option>
<option value="SYSTEM">//</option>
</Select>
{editingItem && editingItem.isDefault && (
<p className="text-xs text-gray-500 mt-1"></p>
)}
</div>
<div>
<Label>{form.calcType === 'SYSTEM' ? '系统函数标识' : '计算公式'}</Label>
<Input
type="text"
value={form.formula}
onChange={(e) => setForm({ ...form, formula: e.target.value })}
placeholder={form.calcType === 'SYSTEM' ? '如:SOCIAL_EMP' : '如:baseSalary + overtimePay + allowance - deduction'}
disabled={!!editingItem && editingItem.isDefault && form.calcType === 'SYSTEM'}
/>
<p className="text-xs text-gray-500 mt-1">
{form.calcType === 'SYSTEM'
? '系统计算标识:SOCIAL_EMP(个人社保)、HOUSING_EMP(个人公积金)、TAX(个税)'
: '可引用其他字段代码进行加减乘除运算,支持 min()、max()、round() 函数'}
</p>
</div>
</div>
)}
<div>