R47 A1-A8:完成薪资管理模块
这个提交包含在:
@@ -53,6 +53,18 @@ async function buildApp({ permissionCodes = ["system.users"], stats } = {}) {
|
||||
{ overall_score: 4, _count: { _all: 3 } },
|
||||
{ overall_score: 4.5, _count: { _all: 2 } },
|
||||
]);
|
||||
const payslipAggregate = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
_sum: { net_amount: 18800 },
|
||||
_avg: { net_amount: 9400 },
|
||||
_count: { _all: 2 },
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
_sum: { net_amount: 17600 },
|
||||
_avg: { net_amount: 8800 },
|
||||
_count: { _all: 2 },
|
||||
});
|
||||
|
||||
app.decorate("prisma", {
|
||||
employee: {
|
||||
@@ -80,6 +92,9 @@ async function buildApp({ permissionCodes = ["system.users"], stats } = {}) {
|
||||
aggregate: stats?.performanceReviewAggregate ?? performanceReviewAggregate,
|
||||
groupBy: stats?.performanceReviewGroupBy ?? performanceReviewGroupBy,
|
||||
},
|
||||
payslip: {
|
||||
aggregate: stats?.payslipAggregate ?? payslipAggregate,
|
||||
},
|
||||
});
|
||||
app.decorate("authenticate", async (request) => {
|
||||
request.employee = {
|
||||
@@ -123,11 +138,12 @@ async function buildApp({ permissionCodes = ["system.users"], stats } = {}) {
|
||||
performanceCycleFindFirst,
|
||||
performanceReviewAggregate,
|
||||
performanceReviewGroupBy,
|
||||
payslipAggregate,
|
||||
};
|
||||
}
|
||||
|
||||
describe("employees stats route", () => {
|
||||
it("returns aggregated employee stats for read-only viewers", async () => {
|
||||
it("returns aggregated employee stats for read-only viewers without salary data", async () => {
|
||||
const {
|
||||
app,
|
||||
employeeGroupBy,
|
||||
@@ -139,6 +155,7 @@ describe("employees stats route", () => {
|
||||
performanceCycleFindFirst,
|
||||
performanceReviewAggregate,
|
||||
performanceReviewGroupBy,
|
||||
payslipAggregate,
|
||||
} = await buildApp({
|
||||
permissionCodes: ["report.dashboard.view"],
|
||||
});
|
||||
@@ -187,6 +204,7 @@ describe("employees stats route", () => {
|
||||
{ score: 5, count: 0 },
|
||||
],
|
||||
});
|
||||
expect(payload.data.salarySummary).toBeNull();
|
||||
expect(employeeGroupBy).toHaveBeenCalledTimes(2);
|
||||
expect(employeeCount).toHaveBeenCalledTimes(2);
|
||||
expect(departmentFindMany).toHaveBeenCalledTimes(1);
|
||||
@@ -196,6 +214,37 @@ describe("employees stats route", () => {
|
||||
expect(performanceCycleFindFirst).toHaveBeenCalledTimes(2);
|
||||
expect(performanceReviewAggregate).toHaveBeenCalledTimes(1);
|
||||
expect(performanceReviewGroupBy).toHaveBeenCalledTimes(1);
|
||||
expect(payslipAggregate).not.toHaveBeenCalled();
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("returns salary summary for system.users viewers", async () => {
|
||||
const { app, payslipAggregate } = await buildApp({
|
||||
permissionCodes: ["system.users"],
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/employees/stats",
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json().data.salarySummary).toEqual({
|
||||
currentMonth: {
|
||||
month: expect.any(String),
|
||||
totalNetAmount: 18800,
|
||||
averageNetAmount: 9400,
|
||||
issuedCount: 2,
|
||||
},
|
||||
previousMonth: {
|
||||
month: expect.any(String),
|
||||
totalNetAmount: 17600,
|
||||
averageNetAmount: 8800,
|
||||
issuedCount: 2,
|
||||
},
|
||||
});
|
||||
expect(payslipAggregate).toHaveBeenCalledTimes(2);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户