From ef404e43628f7ef6d201653fac7d022f3a43e9aa Mon Sep 17 00:00:00 2001 From: BOHA Date: Fri, 5 Jun 2026 12:33:20 +0200 Subject: [PATCH] feat(plan): listEntries/listOverrides with employee scoping --- src/__tests__/plan.test.ts | 76 ++++++++++++++++++++++++++++++++++++ src/services/plan.service.ts | 45 +++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/src/__tests__/plan.test.ts b/src/__tests__/plan.test.ts index bb84c10..104eefc 100644 --- a/src/__tests__/plan.test.ts +++ b/src/__tests__/plan.test.ts @@ -11,6 +11,8 @@ import { createOverride, updateOverride, deleteOverride, + listEntries, + listOverrides, } from "../services/plan.service"; // --------------------------------------------------------------------------- @@ -21,6 +23,7 @@ import { const N = "wh_plan_"; let adminUserId: number; +let noPermUserId: number; beforeAll(async () => { // 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"); 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 () => { @@ -490,3 +499,70 @@ describe("plan.service.deleteOverride", () => { 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([]); + }); +}); diff --git a/src/services/plan.service.ts b/src/services/plan.service.ts index 9f33848..994d07e 100644 --- a/src/services/plan.service.ts +++ b/src/services/plan.service.ts @@ -541,3 +541,48 @@ export async function deleteOverride( 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" }], + }); +}