| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="工作日"> |
| | | <el-input v-model.number="monthWorkDays" |
| | | type="number" |
| | | placeholder="自动计算" |
| | | @input="handleMonthWorkDaysInput" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="选择部门" |
| | | prop="deptIds"> |
| | | <el-select v-model="form.deptIds" |
| | |
| | | <el-option v-for="item in deptOptions" |
| | | :key="item.deptId" |
| | | :label="item.deptName" |
| | | :value="item.deptId" |
| | | :disabled="isDeptDisabled(item.deptId)" /> |
| | | :value="item.deptId" |
| | | :disabled="isDeptDisabled(item.deptId)" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | </el-row> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="备注" |
| | | prop="remark"> |
| | | <el-input v-model="form.remark" |
| | |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-col :span="4"> |
| | | <el-form-item label="支付银行" |
| | | prop="payBank"> |
| | | <el-select v-model="form.payBank" |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="审核人" |
| | | prop="auditUserId"> |
| | | <el-select v-model="form.auditUserId" |
| | |
| | | <el-table-column label="员工姓名" |
| | | prop="staffName" |
| | | minWidth="100" /> |
| | | <el-table-column label="入司日期" |
| | | prop="entryDate" |
| | | minWidth="110" /> |
| | | <el-table-column label="部门" |
| | | prop="deptName" |
| | | minWidth="100" /> |
| | |
| | | disabled /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="月时间合计" |
| | | <el-table-column label="日计工资" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.monthTimeTotal" |
| | | <el-input v-model.number="row.dailySalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="月标准工时" |
| | | minWidth="110"> |
| | | <el-table-column label="出勤天数" |
| | | minWidth="130"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.standardHours" |
| | | <el-input v-model.number="row.workDays" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | disabled /> |
| | | style="width: 100%" |
| | | @input="handleWorkDaysInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="折合倍数" |
| | | <el-table-column label="效益" |
| | | minWidth="100"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.conversionRatio" |
| | |
| | | disabled /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计件工资" |
| | | <el-table-column label="月时间合计" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.pieceSalary" |
| | | <el-input v-model.number="row.monthTimeTotal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.pieceSalary = parseNum(row.pieceSalary)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计时工资" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.hourlySalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.hourlySalary = parseNum(row.hourlySalary)" /> |
| | | @input="handlePieceSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="其他收入" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.otherIncome" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleSalaryInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="事假" |
| | | minWidth="90"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.personalLeave" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleLeaveInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="病假" |
| | | minWidth="90"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.sickLeave" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleLeaveInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="丧假" |
| | | minWidth="90"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.bereavementLeave" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleLeaveInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="婚假" |
| | | minWidth="90"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.marriageLeave" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="handleLeaveInput(row)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="津贴" |
| | | minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input v-model.number="row.allowance" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | |
| | | deptName: "", |
| | | basicSalary: 0, |
| | | attendanceBonus: 100, |
| | | pieceSalary: 0, |
| | | entryDate: "", |
| | | workDays: 0, |
| | | dailySalary: 0, |
| | | hourlySalary: 0, |
| | | actualOutput: 0, |
| | | standardQuantity: 0, |
| | | conversionFactor: 1, |
| | | convertedWorkHours: 0, |
| | | monthTimeTotal: 0, |
| | | standardHours: 0, |
| | | conversionRatio: 0, |
| | | otherIncome: 0, |
| | | personalLeave: 0, |
| | | sickLeave: 0, |
| | | bereavementLeave: 0, |
| | | marriageLeave: 0, |
| | | allowance: 0, |
| | | socialPersonal: 0, |
| | | fundPersonal: 0, |
| | | otherDeduct: 0, |
| | |
| | | deptName: source?.deptName ?? fallback.deptName ?? "", |
| | | basicSalary: parseNum(source?.basicSalary ?? fallback.basicSalary), |
| | | attendanceBonus: parseNum(source?.attendanceBonus ?? fallback.attendanceBonus ?? 100) || 100, |
| | | pieceSalary: parseNum(source?.pieceSalary ?? fallback.pieceSalary), |
| | | entryDate: source?.entryDate ?? source?.contractStartTime ?? fallback.entryDate ?? "", |
| | | workDays: parseNum(source?.workDays ?? fallback.workDays), |
| | | dailySalary: parseNum(source?.dailySalary ?? fallback.dailySalary), |
| | | hourlySalary: parseNum(source?.hourlySalary ?? fallback.hourlySalary), |
| | | actualOutput: parseNum(source?.actualOutput ?? fallback.actualOutput), |
| | | standardQuantity: parseNum(source?.standardQuantity ?? fallback.standardQuantity), |
| | | conversionFactor: parseNum(source?.conversionFactor ?? fallback.conversionFactor) || 1, |
| | | convertedWorkHours: parseNum(source?.convertedWorkHours ?? fallback.convertedWorkHours), |
| | | monthTimeTotal: parseNum(source?.monthTimeTotal ?? fallback.monthTimeTotal), |
| | | standardHours: parseNum(source?.standardHours ?? fallback.standardHours), |
| | | conversionRatio: parseNum(source?.conversionRatio ?? fallback.conversionRatio), |
| | | otherIncome: parseNum(source?.otherIncome ?? fallback.otherIncome), |
| | | personalLeave: parseNum(source?.personalLeave ?? fallback.personalLeave), |
| | | sickLeave: parseNum(source?.sickLeave ?? fallback.sickLeave), |
| | | bereavementLeave: parseNum(source?.bereavementLeave ?? fallback.bereavementLeave), |
| | | marriageLeave: parseNum(source?.marriageLeave ?? fallback.marriageLeave), |
| | | allowance: parseNum(source?.allowance ?? fallback.allowance), |
| | | socialPersonal: parseNum(source?.socialPersonal ?? fallback.socialPersonal), |
| | | fundPersonal: parseNum(source?.fundPersonal ?? fallback.fundPersonal), |
| | | otherDeduct: parseNum(source?.otherDeduct ?? fallback.otherDeduct), |
| | |
| | | remark: source?.remark ?? fallback.remark ?? "", |
| | | }); |
| | | |
| | | const applyBackendRow = (targetRow, sourceRow) => { |
| | | const recalculateRowByWorkDays = row => { |
| | | const workDayCount = parseNum(monthWorkDays.value); |
| | | const monthTimeTotal = parseNum(row.monthTimeTotal); |
| | | const standardHours = workDayCount > 0 ? workDayCount * 8 : 0; |
| | | if (workDayCount > 0 && parseNum(row.workDays) > workDayCount) { |
| | | row.workDays = workDayCount; |
| | | } |
| | | if (standardHours > 0) { |
| | | row.conversionRatio = (monthTimeTotal / standardHours).toFixed(4); |
| | | } else { |
| | | row.conversionRatio = 0; |
| | | } |
| | | }; |
| | | |
| | | const applyBackendRow = (targetRow, sourceRow, options = {}) => { |
| | | const preservedConversionRatio = options.preserveConversionRatio |
| | | ? parseNum(targetRow.conversionRatio) |
| | | : null; |
| | | Object.assign(targetRow, normalizeEmployeeRow(sourceRow, targetRow)); |
| | | if (options.preserveConversionRatio) { |
| | | targetRow.conversionRatio = preservedConversionRatio; |
| | | } |
| | | recalculateRowByWorkDays(targetRow); |
| | | }; |
| | | |
| | | const recalcRowsByCurrentWorkDays = () => { |
| | | employeeList.value.forEach(row => { |
| | | recalculateRowByWorkDays(row); |
| | | }); |
| | | }; |
| | | |
| | | const clearRowRecalcState = row => { |
| | |
| | | recalcVersions.clear(); |
| | | }; |
| | | |
| | | const requestEmployeeRecalculate = row => { |
| | | const requestEmployeeRecalculate = (row, options = {}) => { |
| | | const key = String(row?.staffOnJobId ?? row?.id ?? ""); |
| | | if (!key || !form.value.salaryMonth) return; |
| | | const version = (recalcVersions.get(key) || 0) + 1; |
| | | recalcVersions.set(key, version); |
| | | const manualWorkDays = parseNum(row.workDays); |
| | | clearTimeout(recalcTimers.get(key)); |
| | | const payload = { |
| | | staffOnJobId: row.staffOnJobId ?? row.id, |
| | | salaryMonth: form.value.salaryMonth, |
| | | monthWorkDays: parseNum(monthWorkDays.value), |
| | | staffName: row.staffName ?? "", |
| | | postName: row.postName ?? "", |
| | | deptName: row.deptName ?? "", |
| | | entryDate: row.entryDate ?? row.contractStartTime ?? "", |
| | | basicSalary: parseNum(row.basicSalary), |
| | | attendanceBonus: parseNum(row.attendanceBonus), |
| | | pieceSalary: parseNum(row.pieceSalary), |
| | | workDays: parseNum(row.workDays), |
| | | dailySalary: parseNum(row.dailySalary), |
| | | hourlySalary: parseNum(row.hourlySalary), |
| | | actualOutput: parseNum(row.actualOutput), |
| | | standardQuantity: parseNum(row.standardQuantity), |
| | | conversionFactor: parseNum(row.conversionFactor) || 1, |
| | | convertedWorkHours: parseNum(row.convertedWorkHours), |
| | | monthTimeTotal: parseNum(row.monthTimeTotal), |
| | | standardHours: parseNum(row.standardHours), |
| | | conversionRatio: parseNum(row.conversionRatio), |
| | | otherIncome: parseNum(row.otherIncome), |
| | | personalLeave: parseNum(row.personalLeave), |
| | | sickLeave: parseNum(row.sickLeave), |
| | | bereavementLeave: parseNum(row.bereavementLeave), |
| | | marriageLeave: parseNum(row.marriageLeave), |
| | | allowance: parseNum(row.allowance), |
| | | otherDeduct: parseNum(row.otherDeduct), |
| | | socialSupplementAmount: parseNullableNum(row.socialSupplementAmount), |
| | | remark: row.remark ?? "", |
| | |
| | | setTimeout(() => { |
| | | staffSalaryMainCalculateByEmployeeId(payload).then(res => { |
| | | if (recalcVersions.get(key) !== version) return; |
| | | applyBackendRow(row, res?.data || {}); |
| | | applyBackendRow(row, res?.data || {}, options); |
| | | if (manualWorkDays > 0) { |
| | | row.workDays = manualWorkDays; |
| | | row.dailySalary = manualWorkDays > 0 ? parseNum(row.basicSalary) / manualWorkDays : 0; |
| | | recalculateRowByWorkDays(row); |
| | | } |
| | | }); |
| | | }, 250) |
| | | ); |
| | |
| | | row.otherIncome = parseNum(row.otherIncome); |
| | | row.actualOutput = parseNum(row.actualOutput); |
| | | row.standardQuantity = parseNum(row.standardQuantity); |
| | | row.conversionFactor = parseNum(row.conversionFactor) || 1; |
| | | row.workDays = parseNum(row.workDays); |
| | | row.dailySalary = row.workDays > 0 |
| | | ? (parseNum(row.basicSalary) / row.workDays).toFixed(3) |
| | | : 0; |
| | | requestEmployeeRecalculate(row, { preserveConversionRatio: true }); |
| | | }; |
| | | |
| | | const handlePieceSalaryInput = row => { |
| | | row.monthTimeTotal = parseNum(row.monthTimeTotal); |
| | | requestEmployeeRecalculate(row); |
| | | }; |
| | | |
| | | const handleWorkDaysInput = row => { |
| | | row.workDays = parseNum(row.workDays); |
| | | const maxWorkDays = parseNum(monthWorkDays.value); |
| | | if (maxWorkDays > 0 && row.workDays > maxWorkDays) { |
| | | row.workDays = maxWorkDays; |
| | | proxy.$modal.msgWarning("出勤天数不能大于工作日"); |
| | | } |
| | | enforceLeaveAndWorkDayLimit(row); |
| | | row.dailySalary = row.workDays > 0 |
| | | ? (parseNum(row.basicSalary) / row.workDays).toFixed(3) |
| | | : 0; |
| | | requestEmployeeRecalculate(row); |
| | | }; |
| | | |
| | | const handleLeaveInput = row => { |
| | | row.personalLeave = parseNum(row.personalLeave); |
| | | row.sickLeave = parseNum(row.sickLeave); |
| | | row.bereavementLeave = parseNum(row.bereavementLeave); |
| | | row.marriageLeave = parseNum(row.marriageLeave); |
| | | enforceLeaveAndWorkDayLimit(row); |
| | | requestEmployeeRecalculate(row); |
| | | }; |
| | | |
| | | const enforceLeaveAndWorkDayLimit = row => { |
| | | const maxWorkDays = parseNum(monthWorkDays.value); |
| | | if (maxWorkDays <= 0) { |
| | | return true; |
| | | } |
| | | const leaveTotal = parseNum(row.personalLeave) |
| | | + parseNum(row.sickLeave) |
| | | + parseNum(row.bereavementLeave) |
| | | + parseNum(row.marriageLeave) |
| | | + parseNum(row.workDays); |
| | | if (leaveTotal > maxWorkDays) { |
| | | proxy.$modal.msgWarning("请假合计加出勤天数不能超过工作日"); |
| | | const allowedWorkDays = Math.max(0, maxWorkDays |
| | | - parseNum(row.personalLeave) |
| | | - parseNum(row.sickLeave) |
| | | - parseNum(row.bereavementLeave) |
| | | - parseNum(row.marriageLeave)); |
| | | row.workDays = allowedWorkDays; |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | const handleMonthWorkDaysInput = () => { |
| | | monthWorkDays.value = parseNum(monthWorkDays.value); |
| | | employeeList.value.forEach(row => { |
| | | const standardHours = monthWorkDays.value > 0 ? monthWorkDays.value * 8 : 0; |
| | | if (parseNum(row.workDays) > monthWorkDays.value) { |
| | | row.workDays = monthWorkDays.value; |
| | | row.dailySalary = monthWorkDays.value > 0 |
| | | ? (parseNum(row.basicSalary) / monthWorkDays.value).toFixed(3) |
| | | : 0; |
| | | } |
| | | if (parseNum(row.monthTimeTotal) > 0 && standardHours > 0) { |
| | | row.conversionRatio = (parseNum(row.monthTimeTotal) / standardHours).toFixed(4); |
| | | } else { |
| | | row.conversionRatio = 0; |
| | | } |
| | | requestEmployeeRecalculate(row); |
| | | }); |
| | | }; |
| | | |
| | | // 基础资料表单 |
| | |
| | | const totalSalary = computed(() => { |
| | | return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0); |
| | | }); |
| | | |
| | | const monthWorkDays = ref(0); |
| | | |
| | | const calculateMonthWorkDays = salaryMonth => { |
| | | if (!salaryMonth) return 0; |
| | | const [yearStr, monthStr] = String(salaryMonth).split("-"); |
| | | const year = Number(yearStr); |
| | | const month = Number(monthStr); |
| | | if (!year || !month) return 0; |
| | | const start = new Date(year, month - 1, 1); |
| | | const end = new Date(year, month, 0); |
| | | let workDays = 0; |
| | | for (let day = new Date(start); day <= end; day.setDate(day.getDate() + 1)) { |
| | | const weekday = day.getDay(); |
| | | if (weekday !== 0 && weekday !== 6) { |
| | | workDays += 1; |
| | | } |
| | | } |
| | | return workDays; |
| | | }; |
| | | |
| | | // 根据审核人ID获取审核人名称 |
| | | const auditUserName = computed(() => { |
| | |
| | | loadUserList(); |
| | | loadAllDeptOptions(); |
| | | employeeList.value = []; |
| | | monthWorkDays.value = 0; |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | salaryTitle: "", |
| | |
| | | form.value.remark = row.remark ?? ""; |
| | | form.value.payBank = row.payBank ?? ""; |
| | | form.value.auditUserId = row.auditUserId ?? undefined; |
| | | // 如果有员工明细数据,直接反显 |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | | employeeList.value = row.staffSalaryDetailList.map(e => normalizeEmployeeRow(e)); |
| | | monthWorkDays.value = parseNum(row.monthWorkDays) || calculateMonthWorkDays(form.value.salaryMonth); |
| | | // 如果有员工明细数据,直接反显 |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | | employeeList.value = row.staffSalaryDetailList.map(e => { |
| | | const normalized = normalizeEmployeeRow(e); |
| | | recalculateRowByWorkDays(normalized); |
| | | return normalized; |
| | | }); |
| | | } |
| | | recalcRowsByCurrentWorkDays(); |
| | | } else if (form.value.salaryMonth) { |
| | | monthWorkDays.value = calculateMonthWorkDays(form.value.salaryMonth); |
| | | recalcRowsByCurrentWorkDays(); |
| | | } |
| | | // 编辑状态且有月份时,加载可用部门列表 |
| | | if (type === "edit" && form.value.salaryMonth) { |
| | |
| | | // 切换月份时清空已选择的部门 |
| | | if (newMonth !== oldMonth) { |
| | | form.value.deptIds = []; |
| | | } |
| | | if (!monthWorkDays.value || newMonth !== oldMonth) { |
| | | monthWorkDays.value = calculateMonthWorkDays(newMonth); |
| | | recalcRowsByCurrentWorkDays(); |
| | | } |
| | | loadAvailableDepts(newMonth); |
| | | }); |
| | |
| | | staffName: node.label, |
| | | postName: node.postName ?? node.post ?? "", |
| | | deptName: node.deptName ?? "", |
| | | entryDate: node.contractStartTime ?? node.entryDate ?? "", |
| | | remark: "", |
| | | socialSupplementAmount: null, |
| | | attendanceBonus: node.attendanceBonus, |
| | |
| | | }; |
| | | |
| | | const saveData = status => { |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | | deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "", |
| | | salaryMonth: form.value.salaryMonth, |
| | | remark: form.value.remark, |
| | | payBank: form.value.payBank, |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | totalSalary: totalSalary.value, |
| | | const invalidRow = employeeList.value.find(row => { |
| | | const maxWorkDays = parseNum(monthWorkDays.value); |
| | | const leaveTotal = parseNum(row.personalLeave) |
| | | + parseNum(row.sickLeave) |
| | | + parseNum(row.bereavementLeave) |
| | | + parseNum(row.marriageLeave) |
| | | + parseNum(row.workDays); |
| | | return maxWorkDays > 0 && leaveTotal > maxWorkDays; |
| | | }); |
| | | if (invalidRow) { |
| | | proxy.$modal.msgWarning("请假合计加出勤天数不能超过工作日"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | | deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "", |
| | | salaryMonth: form.value.salaryMonth, |
| | | monthWorkDays: parseNum(monthWorkDays.value), |
| | | remark: form.value.remark, |
| | | payBank: form.value.payBank, |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | totalSalary: totalSalary.value, |
| | | staffSalaryDetailList: employeeList.value.map(e => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | attendanceBonus: parseNum(e.attendanceBonus), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | dailySalary: parseNum(e.dailySalary), |
| | | workDays: parseNum(e.workDays), |
| | | personalLeave: parseNum(e.personalLeave), |
| | | sickLeave: parseNum(e.sickLeave), |
| | | bereavementLeave: parseNum(e.bereavementLeave), |
| | | marriageLeave: parseNum(e.marriageLeave), |
| | | allowance: parseNum(e.allowance), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | actualOutput: parseNum(e.actualOutput), |
| | | standardQuantity: parseNum(e.standardQuantity), |
| | | conversionFactor: parseNum(e.conversionFactor) || 1, |
| | | convertedWorkHours: parseNum(e.convertedWorkHours), |
| | | monthTimeTotal: parseNum(e.monthTimeTotal), |
| | | standardHours: parseNum(e.standardHours), |
| | | conversionRatio: parseNum(e.conversionRatio), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |