feat(plan): listEntries/listOverrides with employee scoping
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
|||||||
createOverride,
|
createOverride,
|
||||||
updateOverride,
|
updateOverride,
|
||||||
deleteOverride,
|
deleteOverride,
|
||||||
|
listEntries,
|
||||||
|
listOverrides,
|
||||||
} from "../services/plan.service";
|
} from "../services/plan.service";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -21,6 +23,7 @@ import {
|
|||||||
const N = "wh_plan_";
|
const N = "wh_plan_";
|
||||||
|
|
||||||
let adminUserId: number;
|
let adminUserId: number;
|
||||||
|
let noPermUserId: number;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Pick the first admin user from the test database as our fixture.
|
// Pick the first admin user from the test database as our fixture.
|
||||||
@@ -31,6 +34,12 @@ beforeAll(async () => {
|
|||||||
});
|
});
|
||||||
if (!admin) throw new Error("Test setup: admin user not found");
|
if (!admin) throw new Error("Test setup: admin user not found");
|
||||||
adminUserId = admin.id;
|
adminUserId = admin.id;
|
||||||
|
|
||||||
|
// For list-scoping tests: any non-admin user that is NOT the admin we use
|
||||||
|
// for create tests. Just pick the second user.
|
||||||
|
const allUsers = await prisma.users.findMany({ take: 2 });
|
||||||
|
noPermUserId =
|
||||||
|
allUsers.find((u) => u.id !== adminUserId)?.id ?? allUsers[0].id;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -490,3 +499,70 @@ describe("plan.service.deleteOverride", () => {
|
|||||||
expect(still?.is_deleted).toBe(true);
|
expect(still?.is_deleted).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// listEntries / listOverrides (employee scoping)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("plan.service.listEntries (employee scoping)", () => {
|
||||||
|
it("returns entries for the actor user when scoped", async () => {
|
||||||
|
await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2098-01-01",
|
||||||
|
date_to: "2098-01-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}scope test`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const rows = await listEntries(
|
||||||
|
{ user_id: adminUserId, date_from: "2098-01-01", date_to: "2098-01-31" },
|
||||||
|
adminUserId,
|
||||||
|
true, // isAdmin
|
||||||
|
);
|
||||||
|
expect(rows.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scopes non-admin to actor user_id when querying for another user", async () => {
|
||||||
|
await createEntry(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
date_from: "2098-02-01",
|
||||||
|
date_to: "2098-02-05",
|
||||||
|
category: "work",
|
||||||
|
note: `${N}admin entry`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const rows = await listEntries(
|
||||||
|
{ user_id: adminUserId, date_from: "2098-02-01", date_to: "2098-02-28" },
|
||||||
|
noPermUserId,
|
||||||
|
false, // not admin
|
||||||
|
);
|
||||||
|
expect(rows).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("plan.service.listOverrides (employee scoping)", () => {
|
||||||
|
it("scopes non-admin to actor user_id", async () => {
|
||||||
|
await createOverride(
|
||||||
|
{
|
||||||
|
user_id: adminUserId,
|
||||||
|
shift_date: "2098-03-01",
|
||||||
|
category: "leave",
|
||||||
|
note: `${N}admin override`,
|
||||||
|
},
|
||||||
|
adminUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const rows = await listOverrides(
|
||||||
|
{ user_id: adminUserId, date_from: "2098-03-01", date_to: "2098-03-31" },
|
||||||
|
noPermUserId,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(rows).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -541,3 +541,48 @@ export async function deleteOverride(
|
|||||||
|
|
||||||
return { data: { ok: true }, oldData: existing };
|
return { data: { ok: true }, oldData: existing };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Raw-row list endpoints
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** List raw work_plan_entries rows in a date range, excluding soft-deleted.
|
||||||
|
* When isAdmin is false, the user_id filter is forced to the actor's own id
|
||||||
|
* (so a non-admin cannot read another user's plan via the user_id query param). */
|
||||||
|
export async function listEntries(
|
||||||
|
query: { user_id?: number; date_from: string; date_to: string },
|
||||||
|
actorUserId: number,
|
||||||
|
isAdmin: boolean,
|
||||||
|
) {
|
||||||
|
const effectiveUserId = isAdmin ? query.user_id : actorUserId;
|
||||||
|
return prisma.work_plan_entries.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: effectiveUserId,
|
||||||
|
is_deleted: false,
|
||||||
|
date_from: { lte: toDateOnly(query.date_to) },
|
||||||
|
date_to: { gte: toDateOnly(query.date_from) },
|
||||||
|
},
|
||||||
|
orderBy: [{ date_from: "asc" }, { id: "asc" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** List raw work_plan_overrides rows in a date range, excluding soft-deleted.
|
||||||
|
* Same employee-scoping rules as listEntries. */
|
||||||
|
export async function listOverrides(
|
||||||
|
query: { user_id?: number; date_from: string; date_to: string },
|
||||||
|
actorUserId: number,
|
||||||
|
isAdmin: boolean,
|
||||||
|
) {
|
||||||
|
const effectiveUserId = isAdmin ? query.user_id : actorUserId;
|
||||||
|
return prisma.work_plan_overrides.findMany({
|
||||||
|
where: {
|
||||||
|
user_id: effectiveUserId,
|
||||||
|
is_deleted: false,
|
||||||
|
shift_date: {
|
||||||
|
gte: toDateOnly(query.date_from),
|
||||||
|
lte: toDateOnly(query.date_to),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [{ shift_date: "asc" }, { id: "asc" }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user