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() 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() 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