fix(attendance): admin month view no longer truncates at 100 records
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)", () => {
|
describe("PUT /api/admin/attendance/:id (combined datetimes)", () => {
|
||||||
it("updates a record with combined datetimes (incl. overnight departure)", async () => {
|
it("updates a record with combined datetimes (incl. overnight departure)", async () => {
|
||||||
const created = await prisma.attendance.create({
|
const created = await prisma.attendance.create({
|
||||||
|
|||||||
@@ -867,7 +867,10 @@ export default function useAttendanceAdmin({ alert }: AlertContext) {
|
|||||||
try {
|
try {
|
||||||
const [yearStr, monthStr] = month.split("-");
|
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}`;
|
if (filterUserId) recordsUrl += `&user_id=${filterUserId}`;
|
||||||
|
|
||||||
const [recordsResponse, balancesResponse] = await Promise.all([
|
const [recordsResponse, balancesResponse] = await Promise.all([
|
||||||
|
|||||||
@@ -219,7 +219,14 @@ export default async function attendanceRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Default: paginated records list ---
|
// --- 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 isAdmin = authData.permissions.includes("attendance.manage");
|
||||||
const parsedUserId = query.user_id ? Number(query.user_id) : undefined;
|
const parsedUserId = query.user_id ? Number(query.user_id) : undefined;
|
||||||
const userId =
|
const userId =
|
||||||
|
|||||||
Reference in New Issue
Block a user