R47 A1-A8:完成薪资管理模块

这个提交包含在:
2026-03-29 19:05:55 +08:00
父节点 c0384b5bbf
当前提交 7ba72ca87d
共修改 25 个文件,包含 3598 行新增和 4 行删除
+67 -1
查看文件
@@ -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,
};
}