fix: 新增后端 /api/v1/salary/dashboard 路由
- 创建 salary.routes.ts,提供薪酬分析数据 - 基于 Payslip 模型计算部门对比、月度趋势、同比环比 - 在 app.ts 中挂载 /api/v1/salary 路由
This commit is contained in:
@@ -65,6 +65,7 @@ import specialStatusRoutes from './routes/special-status.routes'
|
||||
import companyFileRoutes from './routes/company-file.routes'
|
||||
import acceptanceTestRoutes from './routes/acceptance-test.routes'
|
||||
import leaveRoutes from './routes/leave.routes'
|
||||
import salaryRoutes from './routes/salary.routes'
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
app.use('/api/v1/dashboard', dashboardRoutes)
|
||||
app.use('/api/v1/employees', employeeRoutes)
|
||||
@@ -93,6 +94,7 @@ app.use('/api/v1/special-statuses', specialStatusRoutes)
|
||||
app.use('/api/v1/company-files', companyFileRoutes)
|
||||
app.use('/api/v1/acceptance-tests', acceptanceTestRoutes)
|
||||
app.use('/api/v1/leaves', leaveRoutes)
|
||||
app.use('/api/v1/salary', salaryRoutes)
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Router } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/dashboard', authMiddleware, async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const year = Number(req.query.year) || new Date().getFullYear()
|
||||
const yearPrefix = `${year}-`
|
||||
|
||||
// 获取该年度所有工资条
|
||||
const payslips = await prisma.payslip.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
month: { startsWith: yearPrefix },
|
||||
},
|
||||
include: {
|
||||
employee: { select: { department: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// 概览
|
||||
const totalEmployees = new Set(payslips.map(p => p.employeeId)).size
|
||||
const allNetPays = payslips.map(p => p.netPay).sort((a, b) => a - b)
|
||||
const avgSalary = payslips.length > 0 ? payslips.reduce((s, p) => s + p.netPay, 0) / payslips.length : 0
|
||||
const medianSalary = allNetPays.length > 0
|
||||
? allNetPays.length % 2 === 0
|
||||
? (allNetPays[allNetPays.length / 2 - 1] + allNetPays[allNetPays.length / 2]) / 2
|
||||
: allNetPays[Math.floor(allNetPays.length / 2)]
|
||||
: 0
|
||||
const totalAnnual = payslips.reduce((s, p) => s + p.netPay, 0)
|
||||
|
||||
// 同比:与上一年度对比
|
||||
const prevYearPrefix = `${year - 1}-`
|
||||
const prevPayslips = await prisma.payslip.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
month: { startsWith: prevYearPrefix },
|
||||
},
|
||||
})
|
||||
const prevTotal = prevPayslips.reduce((s, p) => s + p.netPay, 0)
|
||||
const yoy = prevTotal > 0 ? ((totalAnnual - prevTotal) / prevTotal) * 100 : undefined
|
||||
|
||||
// 环比:与上个月对比(12月 vs 11月)
|
||||
const decPayslips = payslips.filter(p => p.month === `${year}-12`)
|
||||
const novPayslips = payslips.filter(p => p.month === `${year}-11`)
|
||||
const decTotal = decPayslips.reduce((s, p) => s + p.netPay, 0)
|
||||
const novTotal = novPayslips.reduce((s, p) => s + p.netPay, 0)
|
||||
const mom = novTotal > 0 ? ((decTotal - novTotal) / novTotal) * 100 : undefined
|
||||
|
||||
// 部门薪酬对比
|
||||
const deptMap = new Map<string, { count: number; total: number }>()
|
||||
for (const p of payslips) {
|
||||
const dept = p.employee?.department || '未分配'
|
||||
const cur = deptMap.get(dept) || { count: 0, total: 0 }
|
||||
cur.count += 1
|
||||
cur.total += p.netPay
|
||||
deptMap.set(dept, cur)
|
||||
}
|
||||
const departments = Array.from(deptMap.entries())
|
||||
.map(([name, v]) => ({ name, count: v.count, avgSalary: v.count > 0 ? v.total / v.count : 0 }))
|
||||
.sort((a, b) => b.avgSalary - a.avgSalary)
|
||||
|
||||
// 月度趋势
|
||||
const monthMap = new Map<string, number>()
|
||||
for (const p of payslips) {
|
||||
monthMap.set(p.month, (monthMap.get(p.month) || 0) + p.netPay)
|
||||
}
|
||||
const monthlyTrend = Array.from(monthMap.entries())
|
||||
.map(([month, total]) => ({ month: month.slice(5), total }))
|
||||
.sort((a, b) => a.month.localeCompare(b.month))
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: { totalEmployees, avgSalary, medianSalary, totalAnnual, yoy, mom },
|
||||
departments,
|
||||
monthlyTrend,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: { code: 'INTERNAL_ERROR', message: '获取薪酬分析数据失败' } })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
Reference in New Issue
Block a user