R47 A1-A8:完成薪资管理模块
这个提交包含在:
@@ -6,6 +6,10 @@ function addDays(date, days) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function shiftMonth(date, months) {
|
||||
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));
|
||||
}
|
||||
|
||||
function toNumber(value) {
|
||||
return value == null ? null : Number(value);
|
||||
}
|
||||
@@ -24,13 +28,29 @@ function buildPerformanceDistribution(rows) {
|
||||
|
||||
async function buildEmployeeStats(prisma, options = {}) {
|
||||
const timeZone = options.timeZone ?? "Asia/Shanghai";
|
||||
const includeSalarySummary = options.includeSalarySummary ?? true;
|
||||
const todayString = currentDateString(timeZone);
|
||||
const today = parseDateOnlyString(todayString);
|
||||
const last30DateFrom = addDays(today, -29);
|
||||
const next30DateTo = addDays(today, 30);
|
||||
const currentMonthStart = parseDateOnlyString(`${todayString.slice(0, 7)}-01`);
|
||||
const previousMonthStart = shiftMonth(currentMonthStart, -1);
|
||||
const activeEmployeeWhere = { status: "active" };
|
||||
|
||||
const [employmentStatusRows, departmentRows, departments, recentHires, recentDepartures, expiringContracts, validCertificateHolders, expiringCertificates, activeCycle, completedCycle] = await Promise.all([
|
||||
const [
|
||||
employmentStatusRows,
|
||||
departmentRows,
|
||||
departments,
|
||||
recentHires,
|
||||
recentDepartures,
|
||||
expiringContracts,
|
||||
validCertificateHolders,
|
||||
expiringCertificates,
|
||||
activeCycle,
|
||||
completedCycle,
|
||||
currentMonthSalaryAggregate,
|
||||
previousMonthSalaryAggregate,
|
||||
] = await Promise.all([
|
||||
prisma.employee.groupBy({
|
||||
by: ["employment_status"],
|
||||
where: activeEmployeeWhere,
|
||||
@@ -102,6 +122,28 @@ async function buildEmployeeStats(prisma, options = {}) {
|
||||
where: { status: "completed" },
|
||||
orderBy: [{ completed_at: "desc" }, { end_date: "desc" }],
|
||||
}),
|
||||
includeSalarySummary
|
||||
? prisma.payslip.aggregate({
|
||||
where: {
|
||||
pay_month: currentMonthStart,
|
||||
status: "issued",
|
||||
},
|
||||
_sum: { net_amount: true },
|
||||
_avg: { net_amount: true },
|
||||
_count: { _all: true },
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
includeSalarySummary
|
||||
? prisma.payslip.aggregate({
|
||||
where: {
|
||||
pay_month: previousMonthStart,
|
||||
status: "issued",
|
||||
},
|
||||
_sum: { net_amount: true },
|
||||
_avg: { net_amount: true },
|
||||
_count: { _all: true },
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const departmentMap = new Map(departments.map((item) => [item.id, item.name]));
|
||||
@@ -142,6 +184,29 @@ async function buildEmployeeStats(prisma, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const salarySummary = includeSalarySummary
|
||||
? {
|
||||
currentMonth:
|
||||
currentMonthSalaryAggregate?._count?._all > 0
|
||||
? {
|
||||
month: currentMonthStart.toISOString().slice(0, 7),
|
||||
totalNetAmount: toNumber(currentMonthSalaryAggregate._sum.net_amount) ?? 0,
|
||||
averageNetAmount: toNumber(currentMonthSalaryAggregate._avg.net_amount) ?? 0,
|
||||
issuedCount: currentMonthSalaryAggregate._count._all,
|
||||
}
|
||||
: null,
|
||||
previousMonth:
|
||||
previousMonthSalaryAggregate?._count?._all > 0
|
||||
? {
|
||||
month: previousMonthStart.toISOString().slice(0, 7),
|
||||
totalNetAmount: toNumber(previousMonthSalaryAggregate._sum.net_amount) ?? 0,
|
||||
averageNetAmount: toNumber(previousMonthSalaryAggregate._avg.net_amount) ?? 0,
|
||||
issuedCount: previousMonthSalaryAggregate._count._all,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
asOfDate: todayString,
|
||||
employmentStatusSummary: {
|
||||
@@ -177,6 +242,7 @@ async function buildEmployeeStats(prisma, options = {}) {
|
||||
validHolderCount: validCertificateHolders.length,
|
||||
expiringSoonCount: expiringCertificates,
|
||||
},
|
||||
salarySummary,
|
||||
performanceSummary,
|
||||
};
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户