feat: 工作台概览优化 - 人力成本展示、解聘记录完善、数据准确性修复
- 解聘列表显示所有状态记录(含已撤销/已完成) - 月度解聘人数排除CANCELLED和DRAFT状态 - 工作台新增当月人力成本和年度累计成本卡片 - 年度累计成本包含当前月未归档数据 - 企业总成本公式加入加班费 - 统计卡片月加班费改为工资条数量,避免与动态区重复 - 本月工作动态与风险分布改为左右两列布局 - 人力成本区域优化:2列网格+突出总成本
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user