From b3e2abf9b2f7dd4ef4aa2bc43543de02e12511a8 Mon Sep 17 00:00:00 2001 From: BOHA Date: Wed, 10 Jun 2026 10:20:40 +0200 Subject: [PATCH] fix(attendance): admin month view no longer truncates at 100 records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The KPI cards and on-screen summary totals (computeUserTotals) are computed client-side from one month fetch. The hook requested limit=1000 but parsePagination silently clamped it to 100, so once a month exceeded 100 attendance rows (~5 employees x 21 shifts) the newest-first list dropped the EARLIEST days of the month — screen totals undercounted while the print path (complete server fetch) stayed correct. This is the data-volume cliff that made the summary counting look like it changed without any formula change: git archaeology confirms every counting formula is bit-identical from v1.9.1 through HEAD. - routes/admin/attendance.ts: list endpoint passes maxLimit 2000 to parsePagination (complete-month view; 2000 ~ 64 employees x 31 days) - useAttendanceAdmin: month fetch requests limit=2000 with the constraint documented - regression test: a 120-row month returns all 120 records Co-Authored-By: Claude Fable 5 --- src/__tests__/attendance.test.ts | 36 +++++++++++++++++++++++++++ src/admin/hooks/useAttendanceAdmin.ts | 5 +++- src/routes/admin/attendance.ts | 9 ++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/__tests__/attendance.test.ts b/src/__tests__/attendance.test.ts index 0c77eca..05ee731 100644 --- a/src/__tests__/attendance.test.ts +++ b/src/__tests__/attendance.test.ts @@ -222,6 +222,42 @@ describe("POST /api/admin/attendance (combined datetimes)", () => { }); }); +describe("GET /api/admin/attendance (complete month, no truncation)", () => { + it("returns every record of a month with more than 100 rows", async () => { + // The admin month view computes the KPI cards and summary totals from + // this single fetch, so it must cover the COMPLETE month. With the old + // 100-row pagination cap, months with >100 records were silently + // truncated (newest-first) and the screen totals dropped the earliest + // days while the print stayed complete. + const rows = []; + for (let day = 1; day <= 30; day++) { + for (let s = 0; s < 4; s++) { + rows.push({ + user_id: adminUserId, + shift_date: new Date(2098, 4, day, 12, 0, 0), + leave_type: "work" as const, + arrival_time: new Date(2098, 4, day, 6 + s, 0, 0), + departure_time: new Date(2098, 4, day, 10 + s, 0, 0), + notes: "attendance_wire_test bulk month", + }); + } + } + await prisma.attendance.createMany({ data: rows }); + + const res = await app.inject({ + method: "GET", + url: "/api/admin/attendance?year=2098&month=5&limit=2000", + headers: { Authorization: `Bearer ${adminToken}` }, + }); + + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.success).toBe(true); + expect(body.data).toHaveLength(120); + expect(body.pagination.total).toBe(120); + }); +}); + describe("PUT /api/admin/attendance/:id (combined datetimes)", () => { it("updates a record with combined datetimes (incl. overnight departure)", async () => { const created = await prisma.attendance.create({ diff --git a/src/admin/hooks/useAttendanceAdmin.ts b/src/admin/hooks/useAttendanceAdmin.ts index 7b06912..cea3370 100644 --- a/src/admin/hooks/useAttendanceAdmin.ts +++ b/src/admin/hooks/useAttendanceAdmin.ts @@ -867,7 +867,10 @@ export default function useAttendanceAdmin({ alert }: AlertContext) { try { const [yearStr, monthStr] = month.split("-"); - let recordsUrl = `${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=1000`; + // The KPI cards + summary totals are computed from THIS fetch — it + // must cover the complete month (the server allows limit up to 2000 + // for exactly this view; 2000 ≈ 64 employees × 31 days). + let recordsUrl = `${API_BASE}/attendance?year=${yearStr}&month=${monthStr}&limit=2000`; if (filterUserId) recordsUrl += `&user_id=${filterUserId}`; const [recordsResponse, balancesResponse] = await Promise.all([ diff --git a/src/routes/admin/attendance.ts b/src/routes/admin/attendance.ts index dddacf9..a2bed38 100644 --- a/src/routes/admin/attendance.ts +++ b/src/routes/admin/attendance.ts @@ -219,7 +219,14 @@ export default async function attendanceRoutes( } // --- Default: paginated records list --- - const { page, limit, skip, order } = parsePagination(query); + // maxLimit must allow a COMPLETE month: the admin month view fetches all + // records at once and computes the KPI cards / summary totals client-side + // (useAttendanceAdmin). With the default 100-row cap, months with more + // than 100 records were silently truncated (newest-first), so the screen + // totals dropped the earliest days while the print stayed complete. + const { page, limit, skip, order } = parsePagination(query, { + maxLimit: 2000, + }); const isAdmin = authData.permissions.includes("attendance.manage"); const parsedUserId = query.user_id ? Number(query.user_id) : undefined; const userId =