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 =