| | |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="calculateRow(row)" /> |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="白班天数" |
| | |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="calculateRow(row)" /> |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="夜班天数" |
| | |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="calculateRow(row)" /> |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="餐补" |
| | |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="calculateRow(row)" /> |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="社保个人" |
| | |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled |
| | | @input="calculateRow(row)" /> |
| | | disabled /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="公积金个人" |
| | |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="calculateRow(row)" /> |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="社保补缴" |
| | |
| | | staffSalaryMainAdd, |
| | | staffSalaryMainUpdate, |
| | | staffSalaryMainCalculateSalary, |
| | | staffSalaryMainCalculateByEmployeeId, |
| | | } from "@/api/personnelManagement/staffSalaryMain.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | import { listSubsidyConfiguration } from "@/api/personnelManagement/subsidyConfig.js"; |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close"]); |
| | | const props = defineProps({ |
| | |
| | | const selectedEmployees = ref([]); |
| | | const bankOptions = ref([]); |
| | | const userList = ref([]); |
| | | const subsidyStandard = ref({ |
| | | const recalcTimers = new Map(); |
| | | const recalcVersions = new Map(); |
| | | |
| | | const getEmployeeKey = row => row?.staffOnJobId ?? row?.staffId ?? row?.id; |
| | | |
| | | const createEmptySalaryRow = () => ({ |
| | | staffOnJobId: undefined, |
| | | id: undefined, |
| | | staffName: "", |
| | | postName: "", |
| | | deptName: "", |
| | | nation: "", |
| | | basicSalary: 0, |
| | | dayDays: 0, |
| | | nightDays: 0, |
| | | mealAmount: 0, |
| | | nightAmount: 0, |
| | | pieceSalary: 0, |
| | | hourlySalary: 0, |
| | | otherIncome: 0, |
| | | socialPersonal: 0, |
| | | fundPersonal: 0, |
| | | otherDeduct: 0, |
| | | socialSupplementAmount: 0, |
| | | salaryTax: 0, |
| | | grossSalary: 0, |
| | | deductSalary: 0, |
| | | netSalary: 0, |
| | | remark: "", |
| | | }); |
| | | |
| | | const loadSubsidyStandard = () => { |
| | | listSubsidyConfiguration().then(res => { |
| | | if (res.data && res.data.length > 0) { |
| | | subsidyStandard.value = { |
| | | mealAmount: res.data[0].mealAmount || 0, |
| | | nightAmount: res.data[0].nightAmount || 0, |
| | | const normalizeSalaryRow = source => ({ |
| | | ...createEmptySalaryRow(), |
| | | ...source, |
| | | staffOnJobId: source?.staffOnJobId ?? source?.staffId ?? source?.id, |
| | | id: source?.staffOnJobId ?? source?.staffId ?? source?.id, |
| | | basicSalary: parseNum(source?.basicSalary), |
| | | dayDays: parseNum(source?.dayDays ?? source?.dayShiftDays), |
| | | nightDays: parseNum(source?.nightDays ?? source?.nightShiftDays), |
| | | mealAmount: parseNum(source?.mealAmount ?? source?.mealSubsidy), |
| | | nightAmount: parseNum(source?.nightAmount ?? source?.nightSubsidy), |
| | | pieceSalary: parseNum(source?.pieceSalary), |
| | | hourlySalary: parseNum(source?.hourlySalary), |
| | | otherIncome: parseNum(source?.otherIncome), |
| | | socialPersonal: parseNum(source?.socialPersonal), |
| | | fundPersonal: parseNum(source?.fundPersonal), |
| | | otherDeduct: parseNum(source?.otherDeduct), |
| | | socialSupplementAmount: parseNum(source?.socialSupplementAmount), |
| | | salaryTax: parseNum(source?.salaryTax), |
| | | grossSalary: parseNum(source?.grossSalary), |
| | | deductSalary: parseNum(source?.deductSalary), |
| | | netSalary: parseNum(source?.netSalary), |
| | | remark: source?.remark ?? "", |
| | | }); |
| | | |
| | | const applyBackendRow = (targetRow, sourceRow) => { |
| | | const normalized = normalizeSalaryRow(sourceRow); |
| | | Object.assign(targetRow, { |
| | | staffOnJobId: targetRow.staffOnJobId ?? normalized.staffOnJobId, |
| | | id: targetRow.id ?? normalized.id, |
| | | staffName: normalized.staffName || targetRow.staffName || "", |
| | | postName: normalized.postName || targetRow.postName || "", |
| | | deptName: normalized.deptName || targetRow.deptName || "", |
| | | nation: normalized.nation || targetRow.nation || "", |
| | | basicSalary: normalized.basicSalary, |
| | | dayDays: normalized.dayDays, |
| | | nightDays: normalized.nightDays, |
| | | mealAmount: normalized.mealAmount, |
| | | nightAmount: normalized.nightAmount, |
| | | pieceSalary: normalized.pieceSalary, |
| | | hourlySalary: normalized.hourlySalary, |
| | | otherIncome: normalized.otherIncome, |
| | | socialPersonal: normalized.socialPersonal, |
| | | fundPersonal: normalized.fundPersonal, |
| | | otherDeduct: normalized.otherDeduct, |
| | | socialSupplementAmount: normalized.socialSupplementAmount, |
| | | salaryTax: normalized.salaryTax, |
| | | grossSalary: normalized.grossSalary, |
| | | deductSalary: normalized.deductSalary, |
| | | netSalary: normalized.netSalary, |
| | | remark: normalized.remark || targetRow.remark || "", |
| | | }); |
| | | }; |
| | | |
| | | const requestBackendRecalculate = row => { |
| | | const key = String(getEmployeeKey(row) ?? ""); |
| | | if (!key || !form.value.deptIds?.length || !form.value.salaryMonth) { |
| | | return; |
| | | } |
| | | const version = (recalcVersions.get(key) || 0) + 1; |
| | | recalcVersions.set(key, version); |
| | | clearTimeout(recalcTimers.get(key)); |
| | | const payload = { |
| | | staffOnJobId: getEmployeeKey(row), |
| | | salaryMonth: form.value.salaryMonth, |
| | | basicSalary: parseNum(row.basicSalary), |
| | | dayDays: parseNum(row.dayDays), |
| | | nightDays: parseNum(row.nightDays), |
| | | pieceSalary: parseNum(row.pieceSalary), |
| | | hourlySalary: parseNum(row.hourlySalary), |
| | | otherIncome: parseNum(row.otherIncome), |
| | | otherDeduct: parseNum(row.otherDeduct), |
| | | socialPersonal: parseNum(row.socialPersonal), |
| | | fundPersonal: parseNum(row.fundPersonal), |
| | | remark: row.remark ?? "", |
| | | }; |
| | | recalcTimers.set( |
| | | key, |
| | | setTimeout(() => { |
| | | staffSalaryMainCalculateByEmployeeId(payload) |
| | | .then(res => { |
| | | if (recalcVersions.get(key) !== version) return; |
| | | applyBackendRow(row, res?.data || {}); |
| | | }) |
| | | .catch(() => { |
| | | }); |
| | | }, 300) |
| | | ); |
| | | }; |
| | | |
| | | const calculateRow = row => { |
| | | // 确保所有数值字段为数字 |
| | | const handleSalaryInput = row => { |
| | | row.basicSalary = parseNum(row.basicSalary); |
| | | row.dayDays = parseNum(row.dayDays); |
| | | row.nightDays = parseNum(row.nightDays); |
| | | row.pieceSalary = parseNum(row.pieceSalary); |
| | | row.hourlySalary = parseNum(row.hourlySalary); |
| | | row.otherIncome = parseNum(row.otherIncome); |
| | | row.socialPersonal = parseNum(row.socialPersonal); |
| | | row.fundPersonal = parseNum(row.fundPersonal); |
| | | row.otherDeduct = parseNum(row.otherDeduct); |
| | | row.socialSupplementAmount = parseNum(row.socialSupplementAmount); |
| | | row.salaryTax = parseNum(row.salaryTax); |
| | | requestBackendRecalculate(row); |
| | | }; |
| | | |
| | | // 1. 计算夜班补贴:夜班天数 * 标准 |
| | | row.nightAmount = row.nightDays * subsidyStandard.value.nightAmount; |
| | | const resetRecalcState = () => { |
| | | recalcTimers.forEach(timer => clearTimeout(timer)); |
| | | recalcTimers.clear(); |
| | | recalcVersions.clear(); |
| | | }; |
| | | |
| | | // 2. 计算餐补:仅限回族,(白班 + 夜班) * 标准 |
| | | if (row.nation && (row.nation === "回族" || row.nation.includes("回"))) { |
| | | row.mealAmount = |
| | | (row.dayDays + row.nightDays) * subsidyStandard.value.mealAmount; |
| | | } else { |
| | | row.mealAmount = 0; |
| | | } |
| | | |
| | | // 3. 计算应发工资 = 基本工资 + 餐补 + 夜班补助 + 其他收入 |
| | | row.grossSalary = |
| | | row.basicSalary + row.mealAmount + row.nightAmount + row.otherIncome; |
| | | |
| | | // 4. 计算应扣工资 = 社保个人 + 公积金个人 + 其他支出 + 社保补缴 + 工资个税 |
| | | row.deductSalary = |
| | | row.socialPersonal + |
| | | row.fundPersonal + |
| | | row.otherDeduct + |
| | | row.socialSupplementAmount + |
| | | row.salaryTax; |
| | | |
| | | // 5. 计算实发工资 = 应发工资 - 应扣工资 |
| | | row.netSalary = row.grossSalary - row.deductSalary; |
| | | const clearRowRecalcState = row => { |
| | | const key = String(getEmployeeKey(row) ?? ""); |
| | | if (!key) return; |
| | | clearTimeout(recalcTimers.get(key)); |
| | | recalcTimers.delete(key); |
| | | recalcVersions.delete(key); |
| | | }; |
| | | const taxTableData = ref([ |
| | | { level: 1, range: "不超过36000元", rate: 3, quickDeduction: 0 }, |
| | |
| | | |
| | | const openDialog = (type, row) => { |
| | | nextTick(() => { |
| | | resetRecalcState(); |
| | | loadDeptOptions(); |
| | | loadBankOptions(); |
| | | loadUserList(); |
| | | loadSubsidyStandard(); |
| | | employeeList.value = []; |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | |
| | | |
| | | // 如果有员工明细数据,直接反显 |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | | employeeList.value = row.staffSalaryDetailList.map(e => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | id: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName ?? "", |
| | | postName: e.postName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | nation: e.nation ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | dayDays: parseNum(e.dayDays ?? e.dayShiftDays), |
| | | nightDays: parseNum(e.nightDays ?? e.nightShiftDays), |
| | | mealAmount: parseNum(e.mealAmount ?? e.mealSubsidy), |
| | | nightAmount: parseNum(e.nightAmount ?? e.nightSubsidy), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | socialSupplementAmount: parseNum(e.socialSupplementAmount), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })); |
| | | employeeList.value = row.staffSalaryDetailList.map(e => normalizeSalaryRow(e)); |
| | | } |
| | | } |
| | | }); |
| | |
| | | const id = node.staffId ?? node.id; |
| | | if (existIds.has(id)) return; |
| | | existIds.add(id); |
| | | employeeList.value.push({ |
| | | const newRow = { |
| | | ...createEmptySalaryRow(), |
| | | staffOnJobId: id, |
| | | id, |
| | | staffName: node.label, |
| | |
| | | deptName: node.deptName ?? "", |
| | | nation: node.nation ?? "", |
| | | basicSalary: parseNum(node.basicSalary), |
| | | dayDays: 0, |
| | | nightDays: 0, |
| | | mealAmount: 0, |
| | | nightAmount: 0, |
| | | otherIncome: 0, |
| | | socialPersonal: 0, |
| | | fundPersonal: 0, |
| | | otherDeduct: 0, |
| | | socialSupplementAmount: 0, |
| | | salaryTax: 0, |
| | | grossSalary: 0, |
| | | deductSalary: 0, |
| | | netSalary: 0, |
| | | remark: "", |
| | | }); |
| | | }; |
| | | employeeList.value.push(newRow); |
| | | requestBackendRecalculate(newRow); |
| | | }); |
| | | addPersonVisible.value = false; |
| | | }; |
| | | |
| | | const removeEmployee = row => { |
| | | clearRowRecalcState(row); |
| | | employeeList.value = employeeList.value.filter( |
| | | e => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id) |
| | | ); |
| | |
| | | return; |
| | | } |
| | | const ids = new Set(selectedEmployees.value.map(e => e.staffOnJobId || e.id)); |
| | | selectedEmployees.value.forEach(item => clearRowRecalcState(item)); |
| | | employeeList.value = employeeList.value.filter( |
| | | e => !ids.has(e.staffOnJobId || e.id) |
| | | ); |
| | |
| | | proxy.$modal.msgWarning("未计算到工资数据"); |
| | | return; |
| | | } |
| | | employeeList.value = list.map(e => ({ |
| | | ...e, |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName, |
| | | deptName: e.deptName, |
| | | nation: e.nation ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | dayDays: parseNum(e.dayDays ?? e.dayShiftDays), |
| | | nightDays: parseNum(e.nightDays ?? e.nightShiftDays), |
| | | mealAmount: parseNum(e.mealAmount ?? e.mealSubsidy), |
| | | nightAmount: parseNum(e.nightAmount ?? e.nightSubsidy), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | socialSupplementAmount: parseNum(e.socialSupplementAmount), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })); |
| | | employeeList.value = list.map(e => normalizeSalaryRow(e)); |
| | | proxy.$modal.msgSuccess("生成成功"); |
| | | }); |
| | | }; |
| | |
| | | proxy.$modal |
| | | .confirm("确定清空当前员工列表吗?") |
| | | .then(() => { |
| | | resetRecalcState(); |
| | | employeeList.value = []; |
| | | }) |
| | | .catch(() => {}); |
| | |
| | | nightDays: parseNum(e.nightDays), |
| | | mealAmount: parseNum(e.mealAmount), |
| | | nightAmount: parseNum(e.nightAmount), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | resetRecalcState(); |
| | | dialogVisible.value = false; |
| | | emit("close"); |
| | | }; |