feat: 工作台概览优化 - 人力成本展示、解聘记录完善、数据准确性修复

- 解聘列表显示所有状态记录(含已撤销/已完成)
- 月度解聘人数排除CANCELLED和DRAFT状态
- 工作台新增当月人力成本和年度累计成本卡片
- 年度累计成本包含当前月未归档数据
- 企业总成本公式加入加班费
- 统计卡片月加班费改为工资条数量,避免与动态区重复
- 本月工作动态与风险分布改为左右两列布局
- 人力成本区域优化:2列网格+突出总成本
This commit is contained in:
selfrelease
2026-07-25 23:11:38 +08:00
parent f74b2808a3
commit 9cb0d1f63b
4 changed files with 127 additions and 9 deletions
+46 -4
View File
@@ -278,12 +278,15 @@ export async function getDashboardData(orgId: string) {
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59)
const yearStart = new Date(now.getFullYear(), 0, 1)
const yearEnd = new Date(now.getFullYear(), 11, 31, 23, 59, 59)
const [
employeeCount, highRisks, pendingRisks, riskItems, resolvedItems,
overtimeRecords, payslips, batchEntries, socialConfig, housingConfig,
monthContracts, monthTerminations, monthDisciplinary, monthAttendance,
monthSeverancePay,
yearBatchEntries, yearOvertimeRecords, yearSeverancePay,
] = await Promise.all([
prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }),
prisma.riskItem.count({ where: { orgId, status: 'PENDING', level: 'HIGH', type: { in: ['CONTRACT', 'TERMINATION'] } } }),
@@ -319,7 +322,7 @@ export async function getDashboardData(orgId: string) {
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } },
}),
prisma.terminationRecord.count({
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } },
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd }, status: { notIn: ['CANCELLED', 'DRAFT'] } },
}),
prisma.disciplinaryRecord.count({
where: { orgId, violationDate: { gte: monthStart, lte: monthEnd } },
@@ -328,7 +331,22 @@ export async function getDashboardData(orgId: string) {
where: { orgId, date: { gte: monthStart, lte: monthEnd } },
}),
prisma.terminationRecord.aggregate({
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd } },
where: { orgId, createdAt: { gte: monthStart, lte: monthEnd }, status: { notIn: ['CANCELLED', 'DRAFT'] } },
_sum: { compensation: true },
}),
// 年度累计:已归档批次条目
prisma.batchEntry.findMany({
where: { orgId, batch: { month: { startsWith: `${now.getFullYear()}-` }, status: 'ARCHIVED' } },
select: { baseSalary: true, overtimePay: true, allowance: true, deduction: true, bonus: true, totalPay: true, socialEmp: true, socialOrg: true, housingEmp: true, housingOrg: true, tax: true, netPay: true, employeeId: true },
}),
// 年度累计:加班记录
prisma.overtimeRecord.findMany({
where: { orgId, month: { startsWith: `${now.getFullYear()}-` } },
select: { totalPay: true, weekdayHours: true, weekendHours: true, holidayHours: true },
}),
// 年度累计:补偿金
prisma.terminationRecord.aggregate({
where: { orgId, createdAt: { gte: yearStart, lte: yearEnd }, status: { notIn: ['CANCELLED', 'DRAFT'] } },
_sum: { compensation: true },
}),
])
@@ -444,8 +462,8 @@ export async function getDashboardData(orgId: string) {
housingEmp: housingEmpTotal,
estimatedTax,
severancePay: monthSeverancePay._sum.compensation || 0,
// 企业总成本 = 工资总额 + 企业社保 + 企业公积金 + 经济补偿金
orgTotalCost: totalPay + socialOrgTotal + housingOrgTotal + (monthSeverancePay._sum.compensation || 0),
// 企业总成本 = 工资总额 + 企业社保 + 企业公积金 + 加班费 + 经济补偿金
orgTotalCost: totalPay + socialOrgTotal + housingOrgTotal + monthlyOvertimePay + (monthSeverancePay._sum.compensation || 0),
// 员工实发 = 工资总额 - 个人社保 - 个人公积金 - 个税
empNetPay: useArchivedData ? totalNetPay : totalPay - socialEmpTotal - housingEmpTotal - estimatedTax,
}
@@ -461,6 +479,29 @@ export async function getDashboardData(orgId: string) {
overtimePay: monthlyOvertimePay,
}
// 年度累计人力资源成本 = 已归档批次汇总 + 当前月数据(当前月可能未归档)
const yearArchivedPay = yearBatchEntries.reduce((s: number, e: typeof yearBatchEntries[number]) => s + e.totalPay, 0)
const yearArchivedSocialOrg = yearBatchEntries.reduce((s: number, e: typeof yearBatchEntries[number]) => s + e.socialOrg, 0)
const yearArchivedHousingOrg = yearBatchEntries.reduce((s: number, e: typeof yearBatchEntries[number]) => s + e.housingOrg, 0)
const yearOvertimePay = yearOvertimeRecords.reduce((s: number, r: typeof yearOvertimeRecords[number]) => s + r.totalPay, 0)
const yearSeverance = yearSeverancePay._sum.compensation || 0
// 当前月是否已包含在归档批次中
const currentMonthArchived = batchEntries.length > 0
const yearTotalPay = yearArchivedPay + (currentMonthArchived ? 0 : totalPay)
const yearSocialOrg = yearArchivedSocialOrg + (currentMonthArchived ? 0 : socialOrgTotal)
const yearHousingOrg = yearArchivedHousingOrg + (currentMonthArchived ? 0 : housingOrgTotal)
const yearCostSummary = {
year: now.getFullYear().toString(),
totalPay: yearTotalPay,
socialOrg: yearSocialOrg,
housingOrg: yearHousingOrg,
overtimePay: yearOvertimePay,
severancePay: yearSeverance,
orgTotalCost: yearTotalPay + yearSocialOrg + yearHousingOrg + yearOvertimePay + yearSeverance,
}
const riskDistribution = {
contract: riskItems.filter((r: typeof riskItems[number]) => r.type === 'CONTRACT').length,
salary: riskItems.filter((r: typeof riskItems[number]) => r.type === 'SALARY').length,
@@ -521,5 +562,6 @@ export async function getDashboardData(orgId: string) {
aiPrediction: null,
payrollSummary,
monthlyActivities,
yearCostSummary,
}
}
+1 -1
View File
@@ -743,7 +743,7 @@ export async function getDrafts(orgId: string, status?: string) {
if (status) {
where.status = status
} else {
where.status = { in: ['DRAFT', 'PENDING_APPROVAL', 'APPROVED', 'REJECTED'] }
where.status = { in: ['DRAFT', 'PENDING_APPROVAL', 'APPROVED', 'REJECTED', 'EXECUTING', 'COMPLETED', 'CANCELLED'] }
}
const records = await prisma.terminationRecord.findMany({
+71 -4
View File
@@ -116,15 +116,36 @@ export default function Dashboard() {
if (!data) return null
const payroll = data.payrollSummary
const activities = data.monthlyActivities
const yearCost = data.yearCostSummary
const stats = [
{ label: '在管员工', value: data.stats.employeeCount, icon: Users, color: 'text-primary' },
{ label: '高风险', value: data.stats.highRiskCount, icon: AlertTriangle, color: 'text-danger' },
{ label: '待办事项', value: data.stats.todoCount, icon: CheckSquare, color: 'text-warning' },
{ label: '月加班费', value: fmt(data.stats.monthlyOvertimePay), icon: DollarSign, color: 'text-safe' },
{ label: '工资条', value: payroll?.payslipCount ?? 0, icon: Receipt, color: 'text-safe' },
]
const payroll = data.payrollSummary
const activities = data.monthlyActivities
// 当月人力成本
const monthCostItems = [
{ label: '工资总额', value: payroll?.totalPay ?? 0, color: 'text-primary' },
{ label: '企业社保', value: payroll?.socialOrg ?? 0, color: 'text-blue-600' },
{ label: '企业公积金', value: payroll?.housingOrg ?? 0, color: 'text-purple-600' },
{ label: '加班费', value: payroll?.overtimePay ?? 0, color: 'text-safe' },
{ label: '补偿金', value: payroll?.severancePay ?? 0, color: 'text-orange-600' },
]
const monthTotalCost = payroll?.orgTotalCost ?? 0
// 年度累计人力成本
const yearCostItems = [
{ label: '工资总额', value: yearCost?.totalPay ?? 0, color: 'text-primary' },
{ label: '企业社保', value: yearCost?.socialOrg ?? 0, color: 'text-blue-600' },
{ label: '企业公积金', value: yearCost?.housingOrg ?? 0, color: 'text-purple-600' },
{ label: '加班费', value: yearCost?.overtimePay ?? 0, color: 'text-safe' },
{ label: '补偿金', value: yearCost?.severancePay ?? 0, color: 'text-orange-600' },
]
const yearTotalCost = yearCost?.orgTotalCost ?? 0
const activityItems = [
{ label: '新签合同', value: activities?.newContracts ?? 0, icon: FileText, color: 'text-primary' },
@@ -213,6 +234,49 @@ export default function Dashboard() {
})}
</div>
{/* 人力成本概览:当月 + 年度累计 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{/* 当月人力成本 */}
<Card>
<div className="flex items-center justify-between mb-3">
<h2 className="font-medium flex items-center gap-1.5"><Wallet className="w-4 h-4 text-primary" /></h2>
<span className="text-xs text-gray-400">{payroll?.month}</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
{monthCostItems.map((item) => (
<div key={item.label} className="flex items-center justify-between">
<span className="text-xs text-gray-500">{item.label}</span>
<span className={`text-xs font-semibold ${item.color}`}>{fmt(item.value)}</span>
</div>
))}
</div>
<div className="mt-3 pt-2 border-t flex items-center justify-between bg-danger/5 -mx-4 -mb-4 px-4 py-2.5 rounded-b-lg">
<span className="text-sm font-medium text-gray-700"></span>
<span className="text-lg font-bold text-danger">{fmt(monthTotalCost)}</span>
</div>
</Card>
{/* 年度累计人力成本 */}
<Card>
<div className="flex items-center justify-between mb-3">
<h2 className="font-medium flex items-center gap-1.5"><TrendingUp className="w-4 h-4 text-primary" /></h2>
<span className="text-xs text-gray-400">{yearCost?.year}</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
{yearCostItems.map((item) => (
<div key={item.label} className="flex items-center justify-between">
<span className="text-xs text-gray-500">{item.label}</span>
<span className={`text-xs font-semibold ${item.color}`}>{fmt(item.value)}</span>
</div>
))}
</div>
<div className="mt-3 pt-2 border-t flex items-center justify-between bg-danger/5 -mx-4 -mb-4 px-4 py-2.5 rounded-b-lg">
<span className="text-sm font-medium text-gray-700"></span>
<span className="text-lg font-bold text-danger">{fmt(yearTotalCost)}</span>
</div>
</Card>
</div>
{/* 合同到期预警 */}
{expiringContracts && expiringContracts.length > 0 && (
<Link to="/roster?contractStatus=expiring">
@@ -240,13 +304,15 @@ export default function Dashboard() {
</Link>
)}
{/* 本月工作动态 + 风险分布 左右两列 */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{/* 本月工作动态 */}
<Card>
<div className="flex items-center justify-between mb-3">
<h2 className="font-medium flex items-center gap-1.5"><Briefcase className="w-4 h-4" /></h2>
<span className="text-xs text-gray-500">{activities?.month}</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{activityItems.map((item) => {
const Icon = item.icon
return (
@@ -353,6 +419,7 @@ export default function Dashboard() {
</div>
)}
</Card>
</div>
</div>
)}
+9
View File
@@ -147,6 +147,15 @@ export interface DashboardData {
overtimeHours: number
overtimePay: number
}
yearCostSummary: {
year: string
totalPay: number
socialOrg: number
housingOrg: number
overtimePay: number
severancePay: number
orgTotalCost: number
}
}
export interface TerminationRecord {