feat: 工作日历/考勤管理重构/AI人力报告/工作台员工分布/筛选优化/导入导出增强
- 新增工作日历页面(月历视图、事件管理、自定义事件) - 考勤管理重构为6 Tab模块(班次/排班/每日出勤/月度报表/休假记录) - AI顾问新增人力报告Tab,支持流式生成+Word导出 - 工作台总览新增员工分布统计(性别/年龄/学历/司龄饼图)+部门成本拆分 - 花名册/合同/解聘补偿新增部门和状态筛选 - 薪税管理新增工资表导入模板下载、银行代发CSV导出 - 社保公积金支持多公积金账户类型显示 - 数据导出新增花名册/解聘记录导出,中文文件名编码修复 - 数据导入新增模板下载(员工/增减员/工资表)+错误日志导出 - 移除工作台日历卡片(已迁移至独立工作日历页面) - 新增20260728/20260729更新测试指导文档
This commit is contained in:
@@ -654,7 +654,24 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => window.open('/api/v1/import/payroll-template', '_blank')}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1'
|
||||
const res = await fetch(`${baseURL}/import/payroll-template`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = '工资表导入模板.xlsx'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
toast.error('下载模板失败')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />下载模板
|
||||
</Button>
|
||||
@@ -696,11 +713,87 @@ function BatchDetail({ batchId, onBack }: { batchId: string; onBack: () => void
|
||||
)}
|
||||
{isArchived && (
|
||||
<div className="flex gap-2">
|
||||
<a href={`/api/v1/payroll2/batches/${batchId}/export?format=csv`} download>
|
||||
<Button variant="secondary" size="sm">
|
||||
<Download className="w-4 h-4 mr-1" />银行代发文件
|
||||
</Button>
|
||||
</a>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const token = useAuthStore.getState().accessToken
|
||||
const baseURL = import.meta.env.DEV ? 'http://localhost:3000/api/v1' : '/api/v1'
|
||||
const res = await fetch(`${baseURL}/payroll2/batches/${batchId}/export?format=csv`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
if (!res.ok) throw new Error('导出失败')
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `银行代发文件-${batch.month}-批次${batch.batchNo}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { toast.error('导出失败') }
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />银行代发文件
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await api.get(`/payroll/batch/${batchId}/summary`) as any
|
||||
const { departments, grandTotal } = res.data
|
||||
const headers = ['部门', '人数', '应发合计', '实发合计', '个人社保', '单位社保', '个人公积金', '单位公积金', '个税合计']
|
||||
const rows = departments.map((d: any) => [
|
||||
d.department, d.headcount, d.totalPay.toFixed(2), d.totalNetPay.toFixed(2),
|
||||
d.totalSocialEmp.toFixed(2), d.totalSocialOrg.toFixed(2),
|
||||
d.totalHousingEmp.toFixed(2), d.totalHousingOrg.toFixed(2), d.totalTax.toFixed(2),
|
||||
])
|
||||
rows.push(['合计', grandTotal.headcount, grandTotal.totalPay.toFixed(2), grandTotal.totalNetPay.toFixed(2),
|
||||
grandTotal.totalSocialEmp.toFixed(2), grandTotal.totalSocialOrg.toFixed(2),
|
||||
grandTotal.totalHousingEmp.toFixed(2), grandTotal.totalHousingOrg.toFixed(2), grandTotal.totalTax.toFixed(2)])
|
||||
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `salary-summary-${batch.month}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { toast.error('导出汇总表失败') }
|
||||
}}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-1" />薪资汇总表
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await api.get(`/payroll/batch/${batchId}/detail`) as any
|
||||
const { details } = res.data
|
||||
const headers = ['姓名', '部门', '基本工资', '岗位工资', '绩效工资', '工龄工资', '加班费', '交通补贴', '餐补', '住房补贴', '通讯补贴', '其他津贴', '奖金', '扣款', '其他扣款', '个人社保', '个人公积金', '个税', '应发合计', '实发工资']
|
||||
const rows = details.map((d: any) => [
|
||||
d.name, d.department,
|
||||
d.baseSalary.toFixed(2), d.positionSalary.toFixed(2), d.performanceSalary.toFixed(2),
|
||||
d.senioritySalary.toFixed(2), d.overtimePay.toFixed(2),
|
||||
d.transportAllowance.toFixed(2), d.mealAllowance.toFixed(2), d.housingAllowance.toFixed(2),
|
||||
d.communicationAllowance.toFixed(2), d.allowance.toFixed(2), d.bonus.toFixed(2),
|
||||
d.deduction.toFixed(2), d.otherDeduction.toFixed(2),
|
||||
d.socialEmp.toFixed(2), d.housingEmp.toFixed(2), d.tax.toFixed(2),
|
||||
d.totalPay.toFixed(2), d.netPay.toFixed(2),
|
||||
])
|
||||
const csv = [headers, ...rows].map(r => r.join(',')).join('\n')
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `salary-detail-${batch.month}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch { toast.error('导出明细表失败') }
|
||||
}}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-1" />薪资明细表
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user