mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 17:53:12 +00:00
Merge pull request #124 from Sea-Haven-Industries/feat/sh-241-wo-day-pagination
feat(work-orders): add per-day board pagination (SH-241)
This commit is contained in:
commit
632e00a0ce
16 changed files with 647 additions and 43 deletions
124
e2e/work-orders/board-day-pagination.spec.ts
Normal file
124
e2e/work-orders/board-day-pagination.spec.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||||
|
|
||||||
|
const FROZEN_NOW = "2026-08-19T15:00:00.000Z";
|
||||||
|
|
||||||
|
const TEST_USER = {
|
||||||
|
token: "wo-day-pagination-e2e-token",
|
||||||
|
expiration: "2030-01-01T00:00:00.000Z",
|
||||||
|
email: "admin@seahavenind.com",
|
||||||
|
userRoles: "Admin",
|
||||||
|
phoneNumber: "5551234567",
|
||||||
|
fullname: "Work Order E2E Admin",
|
||||||
|
id: "1",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
||||||
|
await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function boardRow(id: number, woNumber: string, scheduledDate: string) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
woNumber,
|
||||||
|
workOrderType: 2,
|
||||||
|
siteCode: woNumber,
|
||||||
|
lifecycleStatusLabel: "Scheduled",
|
||||||
|
scheduledDate,
|
||||||
|
dispatcherId: "1",
|
||||||
|
dispatcherName: "Pat Dispatcher",
|
||||||
|
pm: "HVAC",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mockWorkOrderBoard(page: Page) {
|
||||||
|
const wednesday = Array.from({ length: 60 }, (_, index) =>
|
||||||
|
boardRow(1000 + index, `WO-${1000 + index}`, "2026-08-19"),
|
||||||
|
);
|
||||||
|
const monday = Array.from({ length: 5 }, (_, index) =>
|
||||||
|
boardRow(2000 + index, `MON-${2000 + index}`, "2026-08-17"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.route(
|
||||||
|
(url) => url.pathname.startsWith("/api/"),
|
||||||
|
async (route) => {
|
||||||
|
const url = route.request().url();
|
||||||
|
if (url.includes("workorders/board/search")) {
|
||||||
|
await fulfillJson(route, { items: [], totalCount: 0, page: 1, pageSize: 100 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (url.includes("workorders/board") && route.request().method() === "GET") {
|
||||||
|
const weekStart = new URL(url).searchParams.get("weekStart") ?? "2026-08-17";
|
||||||
|
const scheduled =
|
||||||
|
weekStart === "2026-08-17" ? [...wednesday, ...monday] : monday.slice(0, 1);
|
||||||
|
await fulfillJson(route, {
|
||||||
|
weekStart,
|
||||||
|
weekEnd: weekStart === "2026-08-17" ? "2026-08-21" : "2026-08-14",
|
||||||
|
counts: { returned: scheduled.length, total: scheduled.length },
|
||||||
|
unscheduled: [],
|
||||||
|
scheduled,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fulfillJson(route, { data: [], items: [] });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("work order per-day pagination (SH-241)", () => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.clock.install({ time: new Date(FROZEN_NOW) });
|
||||||
|
await page.clock.resume();
|
||||||
|
await page.addInitScript((user) => {
|
||||||
|
localStorage.setItem("auth", JSON.stringify(user));
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.textContent = ".tsqd-parent-container { display: none !important; }";
|
||||||
|
document.documentElement.appendChild(style);
|
||||||
|
}, TEST_USER);
|
||||||
|
await mockWorkOrderBoard(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("changes rows-per-page, hides on global filter, and resets on day or week change", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const pager = page.getByTestId("wo-day-pagination");
|
||||||
|
await page.goto("/workorders");
|
||||||
|
await expect(page.getByRole("heading", { name: "Work Orders" })).toBeVisible({
|
||||||
|
timeout: 20_000,
|
||||||
|
});
|
||||||
|
await expect(pager).toBeVisible();
|
||||||
|
await expect(page.getByText("Page 1 of 2")).toBeVisible();
|
||||||
|
await expect(page.getByText("WO-1000").first()).toBeVisible();
|
||||||
|
await expect(page.getByText("WO-1050")).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByLabel("Rows per page").selectOption("25");
|
||||||
|
await expect(page.getByText("Page 1 of 3")).toBeVisible();
|
||||||
|
await expect(page.getByText("WO-1025")).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByTestId("wo-day-pagination-next").evaluate((node) => {
|
||||||
|
(node as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
await expect(page.getByText("Page 2 of 3")).toBeVisible();
|
||||||
|
await expect(page.getByText("WO-1025").first()).toBeVisible();
|
||||||
|
await expect(page.getByText("WO-1000")).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByRole("tab", { name: "Mon · 08/17" }).click({ force: true });
|
||||||
|
await expect(page.getByText("Page 1 of 1")).toBeVisible();
|
||||||
|
await expect(page.getByText("MON-2000").first()).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Week 8/17–8/21" }).click({ force: true });
|
||||||
|
await page.getByRole("button", { name: /Aug 10/ }).click({ force: true });
|
||||||
|
await expect(page.getByTestId("wo-day-pagination")).toBeVisible();
|
||||||
|
await expect(page.getByText("Page 1 of")).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Advanced Filters" }).click();
|
||||||
|
await page.getByRole("button", { name: /Indicators/i }).click();
|
||||||
|
await page.getByRole("button", { name: "Rescheduled ≥ 2" }).click();
|
||||||
|
await page.getByRole("button", { name: "Apply filters" }).click();
|
||||||
|
await expect(pager).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Clear all" }).click();
|
||||||
|
await expect(page.getByTestId("wo-day-pagination")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -36,7 +36,13 @@ export function useWoTableRowHandlers({
|
||||||
onScheduleHighlight,
|
onScheduleHighlight,
|
||||||
onDuplicateFound,
|
onDuplicateFound,
|
||||||
}: UseWoTableRowHandlersOptions): WoTableRowHandlers {
|
}: UseWoTableRowHandlersOptions): WoTableRowHandlers {
|
||||||
const { dragState: dragStateRef, setDragViz, reorderRows, endDrag, triggerFlash } = tableData;
|
const {
|
||||||
|
dragState: dragStateRef,
|
||||||
|
setDragViz,
|
||||||
|
reorderRows,
|
||||||
|
endDrag,
|
||||||
|
revealScheduledRow,
|
||||||
|
} = tableData;
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -77,13 +83,13 @@ export function useWoTableRowHandlers({
|
||||||
rescheduleNotify(date, {
|
rescheduleNotify(date, {
|
||||||
onNavigate: () => {
|
onNavigate: () => {
|
||||||
onNavigateToDay(date);
|
onNavigateToDay(date);
|
||||||
setTimeout(() => triggerFlash(id), 180);
|
revealScheduledRow(id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onNavigateToDay(date);
|
onNavigateToDay(date);
|
||||||
triggerFlash(id);
|
revealScheduledRow(id);
|
||||||
},
|
},
|
||||||
onScheduleWeek: (id: string | number, week: string) => {
|
onScheduleWeek: (id: string | number, week: string) => {
|
||||||
onPatch(id, {
|
onPatch(id, {
|
||||||
|
|
@ -132,7 +138,7 @@ export function useWoTableRowHandlers({
|
||||||
setDragViz,
|
setDragViz,
|
||||||
reorderRows,
|
reorderRows,
|
||||||
endDrag,
|
endDrag,
|
||||||
triggerFlash,
|
revealScheduledRow,
|
||||||
weekMonday,
|
weekMonday,
|
||||||
onNavigateToDay,
|
onNavigateToDay,
|
||||||
onOpenAdvancedFilters,
|
onOpenAdvancedFilters,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { NavArrowButton } from "@/components/ui/nav-arrow-button";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import {
|
||||||
|
DAY_PAGE_SIZE_OPTIONS,
|
||||||
|
formatDayPaginationSummary,
|
||||||
|
type DayPageSize,
|
||||||
|
} from "@/domain/work-orders/utils/day-pagination";
|
||||||
|
|
||||||
|
type WoDayPaginationFooterProps = {
|
||||||
|
dayLabel: string;
|
||||||
|
itemCount: number;
|
||||||
|
pageSize: DayPageSize;
|
||||||
|
pageIndex: number;
|
||||||
|
pageCount: number;
|
||||||
|
onPageSizeChange: (pageSize: DayPageSize) => void;
|
||||||
|
onPrev: () => void;
|
||||||
|
onNext: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WoDayPaginationFooter({
|
||||||
|
dayLabel,
|
||||||
|
itemCount,
|
||||||
|
pageSize,
|
||||||
|
pageIndex,
|
||||||
|
pageCount,
|
||||||
|
onPageSizeChange,
|
||||||
|
onPrev,
|
||||||
|
onNext,
|
||||||
|
}: WoDayPaginationFooterProps) {
|
||||||
|
const canPrev = pageIndex > 0;
|
||||||
|
const canNext = pageIndex < pageCount - 1;
|
||||||
|
const pageLabel = `Page ${pageIndex + 1} of ${pageCount}`;
|
||||||
|
const summary = formatDayPaginationSummary(dayLabel, itemCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
className="flex shrink-0 items-center justify-between gap-3"
|
||||||
|
aria-label="Day pagination"
|
||||||
|
data-testid="wo-day-pagination"
|
||||||
|
>
|
||||||
|
<Text variant="caption" tone="muted">
|
||||||
|
{summary}
|
||||||
|
</Text>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<Text variant="caption" tone="muted">
|
||||||
|
Rows per page
|
||||||
|
</Text>
|
||||||
|
<select
|
||||||
|
aria-label="Rows per page"
|
||||||
|
className="rounded-md border border-border bg-card px-2 py-1 text-xs"
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(event) => onPageSizeChange(Number(event.target.value) as DayPageSize)}
|
||||||
|
>
|
||||||
|
{DAY_PAGE_SIZE_OPTIONS.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<NavArrowButton aria-label="Previous page" disabled={!canPrev} onClick={onPrev}>
|
||||||
|
<ChevronLeft size={15} />
|
||||||
|
</NavArrowButton>
|
||||||
|
<Text variant="caption">{pageLabel}</Text>
|
||||||
|
<NavArrowButton
|
||||||
|
aria-label="Next page"
|
||||||
|
data-testid="wo-day-pagination-next"
|
||||||
|
disabled={!canNext}
|
||||||
|
onClick={onNext}
|
||||||
|
>
|
||||||
|
<ChevronRight size={15} />
|
||||||
|
</NavArrowButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ export function WoTableBody({ tableData, renderRow, renderInlineRow }: WoTableBo
|
||||||
<WoTableSectionHeader
|
<WoTableSectionHeader
|
||||||
variant="day"
|
variant="day"
|
||||||
label={group.label}
|
label={group.label}
|
||||||
count={group.items.length}
|
count={group.totalCount ?? group.items.length}
|
||||||
isToday={group.key === tableData.todayKey}
|
isToday={group.key === tableData.todayKey}
|
||||||
onAddWo={() => tableData.startDayDraft(group.key, group.date)}
|
onAddWo={() => tableData.startDayDraft(group.key, group.date)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,20 @@ const CARD_STYLE: React.CSSProperties = {
|
||||||
boxShadow: "0 1px 3px rgba(38,34,98,.05)",
|
boxShadow: "0 1px 3px rgba(38,34,98,.05)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WoTableShell({ children }: { children: React.ReactNode }) {
|
export function WoTableShell({
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
footer?: React.ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-0 flex-1 overflow-hidden px-2 py-4">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden px-2 py-4">
|
||||||
<div className="relative h-full overflow-auto" style={CARD_STYLE}>
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" style={CARD_STYLE}>
|
||||||
{children}
|
<div className="min-h-0 flex-1 overflow-auto">{children}</div>
|
||||||
|
{Boolean(footer) && (
|
||||||
|
<div className="shrink-0 border-t border-border px-3 py-2">{footer}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
import { isUnassignedQueueReady } from "@/domain/work-orders/utils/week-day-nav-utils";
|
import { isUnassignedQueueReady } from "@/domain/work-orders/utils/week-day-nav-utils";
|
||||||
import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog";
|
import { DuplicateWoNumberDialog } from "@/app/(protected)/workorders/_components/list/duplicate-wo-number-dialog";
|
||||||
import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton";
|
import { WoTableSkeleton } from "@/app/(protected)/workorders/_components/loading/wo-table-skeleton";
|
||||||
|
import { WoDayPaginationFooter } from "./wo-day-pagination-footer";
|
||||||
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
import type { ReturnTypeOfUseWorkOrdersTableData } from "./wo-table-types";
|
||||||
import { buildSitePlaceLookups, resolveSitePlace } from "./site-place-lookup";
|
import { buildSitePlaceLookups, resolveSitePlace } from "./site-place-lookup";
|
||||||
import { useWoTableInlineCreate } from "./use-wo-table-inline-create";
|
import { useWoTableInlineCreate } from "./use-wo-table-inline-create";
|
||||||
|
|
@ -69,6 +70,24 @@ function WoTableEmptyShell({ message, actionLabel, onAction }: WoTableEmptyShell
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WoTableDayFooter({ tableData }: { tableData: ReturnTypeOfUseWorkOrdersTableData }) {
|
||||||
|
if (!tableData.showDayPagination) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<WoDayPaginationFooter
|
||||||
|
dayLabel={tableData.dayFooterLabel}
|
||||||
|
itemCount={tableData.dayItemCount}
|
||||||
|
pageSize={tableData.dayPageSize}
|
||||||
|
pageIndex={tableData.dayPageIndex}
|
||||||
|
pageCount={tableData.dayPageCount}
|
||||||
|
onPageSizeChange={tableData.setDayPageSize}
|
||||||
|
onPrev={tableData.goToPrevDayPage}
|
||||||
|
onNext={tableData.goToNextDayPage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function WoTable({
|
export function WoTable({
|
||||||
tableData,
|
tableData,
|
||||||
users,
|
users,
|
||||||
|
|
@ -188,7 +207,7 @@ export function WoTable({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WoTableShell>
|
<WoTableShell footer={WoTableDayFooter({ tableData })}>
|
||||||
<table
|
<table
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
DEFAULT_DAY_PAGE_SIZE,
|
||||||
|
findItemPageIndex,
|
||||||
|
type DayPageSize,
|
||||||
|
} from "@/domain/work-orders/utils/day-pagination";
|
||||||
|
|
||||||
|
type DayPageItem = { id: string | number };
|
||||||
|
|
||||||
|
type UseWorkOrdersDayPaginationInput = {
|
||||||
|
weekMonday: string;
|
||||||
|
selectedDayIso: string;
|
||||||
|
advActive: boolean;
|
||||||
|
dayItems: readonly DayPageItem[];
|
||||||
|
onRevealed: (id: string | number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWorkOrdersDayPagination({
|
||||||
|
weekMonday,
|
||||||
|
selectedDayIso,
|
||||||
|
advActive,
|
||||||
|
dayItems,
|
||||||
|
onRevealed,
|
||||||
|
}: UseWorkOrdersDayPaginationInput) {
|
||||||
|
const [pageSize, setPageSizeState] = useState<DayPageSize>(DEFAULT_DAY_PAGE_SIZE);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [pendingRevealId, setPendingRevealId] = useState<string | number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(0);
|
||||||
|
}, [weekMonday, selectedDayIso, advActive]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingRevealId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextPage = findItemPageIndex(dayItems, pendingRevealId, pageSize);
|
||||||
|
if (nextPage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPage(nextPage);
|
||||||
|
const revealedId = pendingRevealId;
|
||||||
|
setPendingRevealId(null);
|
||||||
|
onRevealed(revealedId);
|
||||||
|
}, [pendingRevealId, dayItems, pageSize, onRevealed]);
|
||||||
|
|
||||||
|
const setPageSize = useCallback((next: DayPageSize) => {
|
||||||
|
setPageSizeState(next);
|
||||||
|
setPage(0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const goToPrevPage = useCallback(() => {
|
||||||
|
setPage((current) => Math.max(0, current - 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const goToNextPage = useCallback(() => {
|
||||||
|
setPage((current) => current + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const revealRow = useCallback((id: string | number) => {
|
||||||
|
setPendingRevealId(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageSize,
|
||||||
|
page,
|
||||||
|
setPageSize,
|
||||||
|
goToPrevPage,
|
||||||
|
goToNextPage,
|
||||||
|
revealRow,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
|
import type { useWorkOrdersListFilters } from "@/app/(protected)/workorders/_hooks/use-work-orders-list-filters";
|
||||||
import { useWorkOrdersBoardSource } from "@/app/(protected)/workorders/_hooks/use-work-orders-board-source";
|
import { useWorkOrdersBoardSource } from "@/app/(protected)/workorders/_hooks/use-work-orders-board-source";
|
||||||
|
import { useWorkOrdersDayPagination } from "@/app/(protected)/workorders/_hooks/use-work-orders-day-pagination";
|
||||||
import { useWorkOrdersTableLocalState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-local-state";
|
import { useWorkOrdersTableLocalState } from "@/app/(protected)/workorders/_hooks/use-work-orders-table-local-state";
|
||||||
import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
|
import { currentWeekMonday } from "@/domain/work-orders/utils/list-week-utils";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils";
|
||||||
|
import {
|
||||||
|
pageSelectedDayGroups,
|
||||||
|
shouldShowDayPagination,
|
||||||
|
} from "@/domain/work-orders/utils/day-pagination";
|
||||||
import {
|
import {
|
||||||
buildSelectedDayGroup,
|
buildSelectedDayGroup,
|
||||||
isDayScopedSearchEmpty,
|
isDayScopedSearchEmpty,
|
||||||
|
|
@ -51,7 +56,7 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
advActive,
|
advActive,
|
||||||
isFetching,
|
isFetching,
|
||||||
});
|
});
|
||||||
const { displayRows } = local;
|
const { displayRows, ...localUi } = local;
|
||||||
|
|
||||||
const advResults = useMemo(() => (advActive ? displayRows : []), [advActive, displayRows]);
|
const advResults = useMemo(() => (advActive ? displayRows : []), [advActive, displayRows]);
|
||||||
|
|
||||||
|
|
@ -85,16 +90,27 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
: [buildSelectedDayGroup(filters.weekMonday, filters.selectedDayIso, scheduled)],
|
: [buildSelectedDayGroup(filters.weekMonday, filters.selectedDayIso, scheduled)],
|
||||||
[advActive, filters.selectedDayIso, filters.weekMonday, scheduled],
|
[advActive, filters.selectedDayIso, filters.weekMonday, scheduled],
|
||||||
);
|
);
|
||||||
|
const pagination = useWorkOrdersDayPagination({
|
||||||
|
weekMonday: filters.weekMonday,
|
||||||
|
selectedDayIso: filters.selectedDayIso,
|
||||||
|
advActive,
|
||||||
|
dayItems: dayGroups[0]?.items ?? EMPTY_WEEKLY_ITEMS,
|
||||||
|
onRevealed: local.triggerFlash,
|
||||||
|
});
|
||||||
|
const pagedDay = useMemo(
|
||||||
|
() => pageSelectedDayGroups(dayGroups, pagination.page, pagination.pageSize),
|
||||||
|
[dayGroups, pagination.page, pagination.pageSize],
|
||||||
|
);
|
||||||
|
|
||||||
const searchActive = filters.search.trim() !== "";
|
const searchActive = filters.search.trim() !== "";
|
||||||
const displayGroups = dayGroups;
|
const displayGroups = pagedDay.groups;
|
||||||
const dayVisibleCount = displayGroups[0]?.items.length ?? 0;
|
const dayTotalCount = pagedDay.totalCount;
|
||||||
const noSearchResults =
|
const noSearchResults =
|
||||||
!advActive &&
|
!advActive &&
|
||||||
isDayScopedSearchEmpty({
|
isDayScopedSearchEmpty({
|
||||||
searchActive,
|
searchActive,
|
||||||
unassignedCount: unassigned.length,
|
unassignedCount: unassigned.length,
|
||||||
dayItemCount: dayVisibleCount,
|
dayItemCount: dayTotalCount,
|
||||||
unassignedQueueReady: isUnassignedQueueReady(unassignedLoading, unassignedError),
|
unassignedQueueReady: isUnassignedQueueReady(unassignedLoading, unassignedError),
|
||||||
});
|
});
|
||||||
const isEmptyWeek =
|
const isEmptyWeek =
|
||||||
|
|
@ -127,30 +143,24 @@ export function useWorkOrdersTableData(filters: FiltersReturn) {
|
||||||
noSearchResults,
|
noSearchResults,
|
||||||
isEmptyWeek,
|
isEmptyWeek,
|
||||||
todayKey,
|
todayKey,
|
||||||
draftDayKey: local.draftDayKey,
|
...localUi,
|
||||||
draft: local.draft,
|
|
||||||
setDraft: local.setDraft,
|
|
||||||
unassignedOpen: local.unassignedOpen,
|
|
||||||
setUnassignedOpen: local.setUnassignedOpen,
|
|
||||||
unschedOpen: local.unschedOpen,
|
|
||||||
setUnschedOpen: local.setUnschedOpen,
|
|
||||||
selectedId: local.selectedId,
|
|
||||||
setSelectedId: local.setSelectedId,
|
|
||||||
selectedRow,
|
selectedRow,
|
||||||
flashId: local.flashId,
|
|
||||||
triggerFlash: local.triggerFlash,
|
|
||||||
dragViz: local.dragViz,
|
|
||||||
setDragViz: local.setDragViz,
|
|
||||||
dragState: local.dragState,
|
|
||||||
patchRow: local.patchRow,
|
|
||||||
clearRowPatch: local.clearRowPatch,
|
|
||||||
reorderRows: local.reorderRows,
|
|
||||||
endDrag: local.endDrag,
|
|
||||||
startDayDraft: local.startDayDraft,
|
|
||||||
cancelDraft: local.cancelDraft,
|
|
||||||
addRow: local.addRow,
|
|
||||||
scopeTotalCount: data?.totalCount ?? 0,
|
scopeTotalCount: data?.totalCount ?? 0,
|
||||||
totalCount: advActive ? (data?.totalCount ?? advResults.length) : dayVisibleCount,
|
totalCount: advActive ? (data?.totalCount ?? advResults.length) : dayTotalCount,
|
||||||
search: filters.search,
|
search: filters.search,
|
||||||
|
showDayPagination: shouldShowDayPagination({
|
||||||
|
flatListMode: advActive,
|
||||||
|
isLoading,
|
||||||
|
noSearchResults,
|
||||||
|
}),
|
||||||
|
dayPageSize: pagination.pageSize,
|
||||||
|
dayPageIndex: pagedDay.pageIndex,
|
||||||
|
dayPageCount: pagedDay.pageCount,
|
||||||
|
dayItemCount: pagedDay.totalCount,
|
||||||
|
dayFooterLabel: displayGroups[0]?.label ?? "",
|
||||||
|
setDayPageSize: pagination.setPageSize,
|
||||||
|
goToPrevDayPage: pagination.goToPrevPage,
|
||||||
|
goToNextDayPage: pagination.goToNextPage,
|
||||||
|
revealScheduledRow: pagination.revealRow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ export type WorkOrderDayGroup = {
|
||||||
label: string;
|
label: string;
|
||||||
date: string;
|
date: string;
|
||||||
items: WorkOrderTableRow[];
|
items: WorkOrderTableRow[];
|
||||||
|
totalCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkOrderTablePatch = Partial<WorkOrderTableRow>;
|
export type WorkOrderTablePatch = Partial<WorkOrderTableRow>;
|
||||||
|
|
|
||||||
80
src/domain/work-orders/utils/day-pagination.ts
Normal file
80
src/domain/work-orders/utils/day-pagination.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import type { WorkOrderDayGroup } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
|
|
||||||
|
export const DAY_PAGE_SIZE_OPTIONS = [25, 50, 100, 200] as const;
|
||||||
|
export const DEFAULT_DAY_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
export type DayPageSize = (typeof DAY_PAGE_SIZE_OPTIONS)[number];
|
||||||
|
|
||||||
|
export type PaginatedSlice<T> = {
|
||||||
|
visible: T[];
|
||||||
|
pageCount: number;
|
||||||
|
pageIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function pageIndexForItem(itemIndex: number, pageSize: number): number {
|
||||||
|
if (itemIndex < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const size = pageSize > 0 ? pageSize : DEFAULT_DAY_PAGE_SIZE;
|
||||||
|
return Math.floor(itemIndex / size);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findItemPageIndex<T extends { id: string | number }>(
|
||||||
|
items: readonly T[],
|
||||||
|
id: string | number,
|
||||||
|
pageSize: number,
|
||||||
|
): number | null {
|
||||||
|
const index = items.findIndex((item) => String(item.id) === String(id));
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pageIndexForItem(index, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function paginateItems<T>(
|
||||||
|
items: readonly T[],
|
||||||
|
pageIndex: number,
|
||||||
|
pageSize: number,
|
||||||
|
): PaginatedSlice<T> {
|
||||||
|
const size = pageSize > 0 ? pageSize : DEFAULT_DAY_PAGE_SIZE;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(items.length / size) || 1);
|
||||||
|
const page = Math.min(Math.max(0, pageIndex), pageCount - 1);
|
||||||
|
const start = page * size;
|
||||||
|
return {
|
||||||
|
visible: items.slice(start, start + size),
|
||||||
|
pageCount,
|
||||||
|
pageIndex: page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowDayPagination(input: {
|
||||||
|
flatListMode: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
noSearchResults: boolean;
|
||||||
|
}): boolean {
|
||||||
|
return !input.flatListMode && !input.isLoading && !input.noSearchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDayPaginationSummary(dayLabel: string, itemCount: number): string {
|
||||||
|
const label = dayLabel.trim().toUpperCase();
|
||||||
|
const noun = itemCount === 1 ? "item" : "items";
|
||||||
|
return `${label} · ${itemCount} ${noun}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pageSelectedDayGroups(
|
||||||
|
dayGroups: WorkOrderDayGroup[],
|
||||||
|
pageIndex: number,
|
||||||
|
pageSize: number,
|
||||||
|
): { groups: WorkOrderDayGroup[]; pageIndex: number; pageCount: number; totalCount: number } {
|
||||||
|
const group = dayGroups[0];
|
||||||
|
if (!group) {
|
||||||
|
return { groups: dayGroups, pageIndex: 0, pageCount: 1, totalCount: 0 };
|
||||||
|
}
|
||||||
|
const slice = paginateItems(group.items, pageIndex, pageSize);
|
||||||
|
return {
|
||||||
|
groups: [{ ...group, items: slice.visible, totalCount: group.items.length }],
|
||||||
|
pageIndex: slice.pageIndex,
|
||||||
|
pageCount: slice.pageCount,
|
||||||
|
totalCount: group.items.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ function buildTableData() {
|
||||||
endDrag: vi.fn(),
|
endDrag: vi.fn(),
|
||||||
setUnschedOpen: vi.fn(),
|
setUnschedOpen: vi.fn(),
|
||||||
triggerFlash: vi.fn(),
|
triggerFlash: vi.fn(),
|
||||||
|
revealScheduledRow: vi.fn(),
|
||||||
setSelectedId: vi.fn(),
|
setSelectedId: vi.fn(),
|
||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ vi.mock("@/app/(protected)/workorders/_components/list/table/reschedule-notify",
|
||||||
weekMoveNotify,
|
weekMoveNotify,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function buildTableData(triggerFlash = vi.fn()) {
|
function buildTableData(revealScheduledRow = vi.fn()) {
|
||||||
return {
|
return {
|
||||||
dragState: { current: { dragId: null, overId: null } },
|
dragState: { current: { dragId: null, overId: null } },
|
||||||
setDragViz: vi.fn(),
|
setDragViz: vi.fn(),
|
||||||
reorderRows: vi.fn(),
|
reorderRows: vi.fn(),
|
||||||
endDrag: vi.fn(),
|
endDrag: vi.fn(),
|
||||||
triggerFlash,
|
revealScheduledRow,
|
||||||
setSelectedId: vi.fn(),
|
setSelectedId: vi.fn(),
|
||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
|
|
@ -28,11 +28,11 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
|
||||||
it("selects the scheduled weekday in the same week so the row stays visible", () => {
|
it("selects the scheduled weekday in the same week so the row stays visible", () => {
|
||||||
const onNavigateToDay = vi.fn();
|
const onNavigateToDay = vi.fn();
|
||||||
const onPatch = vi.fn();
|
const onPatch = vi.fn();
|
||||||
const triggerFlash = vi.fn();
|
const revealScheduledRow = vi.fn();
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWoTableRowHandlers({
|
useWoTableRowHandlers({
|
||||||
tableData: buildTableData(triggerFlash),
|
tableData: buildTableData(revealScheduledRow),
|
||||||
weekMonday: "2026-05-18",
|
weekMonday: "2026-05-18",
|
||||||
onNavigateToDay,
|
onNavigateToDay,
|
||||||
onOpenSlideOver: vi.fn(),
|
onOpenSlideOver: vi.fn(),
|
||||||
|
|
@ -46,16 +46,17 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
|
||||||
|
|
||||||
expect(onPatch).toHaveBeenCalledWith("wo-1", applySchedulePatch("2026-05-21", false));
|
expect(onPatch).toHaveBeenCalledWith("wo-1", applySchedulePatch("2026-05-21", false));
|
||||||
expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-21");
|
expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-21");
|
||||||
expect(triggerFlash).toHaveBeenCalledWith("wo-1");
|
expect(revealScheduledRow).toHaveBeenCalledWith("wo-1");
|
||||||
expect(rescheduleNotify).not.toHaveBeenCalled();
|
expect(rescheduleNotify).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends cross-week view-it navigation to the target day, not Monday", () => {
|
it("sends cross-week view-it navigation to the target day, not Monday", () => {
|
||||||
const onNavigateToDay = vi.fn();
|
const onNavigateToDay = vi.fn();
|
||||||
|
const revealScheduledRow = vi.fn();
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useWoTableRowHandlers({
|
useWoTableRowHandlers({
|
||||||
tableData: buildTableData(),
|
tableData: buildTableData(revealScheduledRow),
|
||||||
weekMonday: "2026-05-18",
|
weekMonday: "2026-05-18",
|
||||||
onNavigateToDay,
|
onNavigateToDay,
|
||||||
onOpenSlideOver: vi.fn(),
|
onOpenSlideOver: vi.fn(),
|
||||||
|
|
@ -76,6 +77,7 @@ describe("useWoTableRowHandlers schedule navigation (SH-240)", () => {
|
||||||
const { onNavigate } = rescheduleNotify.mock.calls[0][1] as { onNavigate: () => void };
|
const { onNavigate } = rescheduleNotify.mock.calls[0][1] as { onNavigate: () => void };
|
||||||
onNavigate();
|
onNavigate();
|
||||||
expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-27");
|
expect(onNavigateToDay).toHaveBeenCalledWith("2026-05-27");
|
||||||
|
expect(revealScheduledRow).toHaveBeenCalledWith("wo-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens Advanced Filters for week-only moves instead of the removed Unscheduled section", () => {
|
it("opens Advanced Filters for week-only moves instead of the removed Unscheduled section", () => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { act, renderHook } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { useWorkOrdersDayPagination } from "@/app/(protected)/workorders/_hooks/use-work-orders-day-pagination";
|
||||||
|
import { DEFAULT_DAY_PAGE_SIZE } from "@/domain/work-orders/utils/day-pagination";
|
||||||
|
|
||||||
|
describe("useWorkOrdersDayPagination (SH-241)", () => {
|
||||||
|
const onRevealed = vi.fn();
|
||||||
|
const base = {
|
||||||
|
weekMonday: "2026-08-17",
|
||||||
|
selectedDayIso: "2026-08-17",
|
||||||
|
advActive: false,
|
||||||
|
dayItems: [] as { id: string }[],
|
||||||
|
onRevealed,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("defaults to 50 rows per page", () => {
|
||||||
|
const { result } = renderHook(() => useWorkOrdersDayPagination(base));
|
||||||
|
expect(result.current.pageSize).toBe(DEFAULT_DAY_PAGE_SIZE);
|
||||||
|
expect(result.current.page).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets to page 1 when switching days", () => {
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
(props: typeof base) => useWorkOrdersDayPagination(props),
|
||||||
|
{ initialProps: base },
|
||||||
|
);
|
||||||
|
act(() => result.current.goToNextPage());
|
||||||
|
expect(result.current.page).toBe(1);
|
||||||
|
rerender({ ...base, selectedDayIso: "2026-08-18" });
|
||||||
|
expect(result.current.page).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets to page 1 when the week changes", () => {
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
(props: typeof base) => useWorkOrdersDayPagination(props),
|
||||||
|
{ initialProps: base },
|
||||||
|
);
|
||||||
|
act(() => result.current.goToNextPage());
|
||||||
|
rerender({ ...base, weekMonday: "2026-08-10" });
|
||||||
|
expect(result.current.page).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets to page 1 when rows-per-page changes", () => {
|
||||||
|
const { result } = renderHook(() => useWorkOrdersDayPagination(base));
|
||||||
|
act(() => result.current.goToNextPage());
|
||||||
|
act(() => result.current.setPageSize(25));
|
||||||
|
expect(result.current.page).toBe(0);
|
||||||
|
expect(result.current.pageSize).toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets to page 1 when a global filter is applied", () => {
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
(props: typeof base) => useWorkOrdersDayPagination(props),
|
||||||
|
{ initialProps: base },
|
||||||
|
);
|
||||||
|
act(() => result.current.goToNextPage());
|
||||||
|
rerender({ ...base, advActive: true });
|
||||||
|
expect(result.current.page).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("selects the page that contains a revealed row beyond the first page", () => {
|
||||||
|
const dayItems = Array.from({ length: 60 }, (_, index) => ({ id: `wo-${index}` }));
|
||||||
|
const revealed = vi.fn();
|
||||||
|
const { result, rerender } = renderHook(
|
||||||
|
(props: typeof base) => useWorkOrdersDayPagination(props),
|
||||||
|
{ initialProps: { ...base, onRevealed: revealed } },
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => result.current.revealRow("wo-55"));
|
||||||
|
rerender({ ...base, dayItems, onRevealed: revealed, selectedDayIso: "2026-08-18" });
|
||||||
|
|
||||||
|
expect(result.current.page).toBe(1);
|
||||||
|
expect(revealed).toHaveBeenCalledWith("wo-55");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { WoDayPaginationFooter } from "@/app/(protected)/workorders/_components/list/table/wo-day-pagination-footer";
|
||||||
|
|
||||||
|
describe("WoDayPaginationFooter (SH-241)", () => {
|
||||||
|
it("changes rows-per-page and navigates pages", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onPageSizeChange = vi.fn();
|
||||||
|
const onNext = vi.fn();
|
||||||
|
render(
|
||||||
|
<WoDayPaginationFooter
|
||||||
|
dayLabel="Mon · 06/01"
|
||||||
|
itemCount={60}
|
||||||
|
pageSize={50}
|
||||||
|
pageIndex={0}
|
||||||
|
pageCount={2}
|
||||||
|
onPageSizeChange={onPageSizeChange}
|
||||||
|
onPrev={vi.fn()}
|
||||||
|
onNext={onNext}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("MON · 06/01 · 60 items")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Page 1 of 2")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
|
||||||
|
await user.selectOptions(screen.getByLabelText("Rows per page"), "25");
|
||||||
|
expect(onPageSizeChange).toHaveBeenCalledWith(25);
|
||||||
|
await user.click(screen.getByRole("button", { name: "Next page" }));
|
||||||
|
expect(onNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -43,6 +43,7 @@ function tableData(
|
||||||
selectedRow: null,
|
selectedRow: null,
|
||||||
flashId: null,
|
flashId: null,
|
||||||
triggerFlash: vi.fn(),
|
triggerFlash: vi.fn(),
|
||||||
|
revealScheduledRow: vi.fn(),
|
||||||
dragViz: null,
|
dragViz: null,
|
||||||
setDragViz: vi.fn(),
|
setDragViz: vi.fn(),
|
||||||
dragState: { current: { dragId: null, overId: null } },
|
dragState: { current: { dragId: null, overId: null } },
|
||||||
|
|
@ -56,6 +57,15 @@ function tableData(
|
||||||
scopeTotalCount: 0,
|
scopeTotalCount: 0,
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
search: "no-match",
|
search: "no-match",
|
||||||
|
showDayPagination: false,
|
||||||
|
dayPageSize: 50,
|
||||||
|
dayPageIndex: 0,
|
||||||
|
dayPageCount: 1,
|
||||||
|
dayItemCount: 0,
|
||||||
|
dayFooterLabel: "Mon · 05/18",
|
||||||
|
setDayPageSize: vi.fn(),
|
||||||
|
goToPrevDayPage: vi.fn(),
|
||||||
|
goToNextDayPage: vi.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
} as ReturnTypeOfUseWorkOrdersTableData;
|
} as ReturnTypeOfUseWorkOrdersTableData;
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +89,7 @@ function renderTable(data: ReturnTypeOfUseWorkOrdersTableData) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("WoTable search empty vs Unassigned queue (SH-165/SH-240)", () => {
|
describe("WoTable search empty vs Unassigned queue (SH-165/SH-241)", () => {
|
||||||
it("shows empty search only after the Unassigned queue has settled empty", () => {
|
it("shows empty search only after the Unassigned queue has settled empty", () => {
|
||||||
const { getByText, queryByText } = renderTable(tableData());
|
const { getByText, queryByText } = renderTable(tableData());
|
||||||
|
|
||||||
|
|
|
||||||
84
src/test/domain/work-orders/utils/day-pagination.test.ts
Normal file
84
src/test/domain/work-orders/utils/day-pagination.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
DAY_PAGE_SIZE_OPTIONS,
|
||||||
|
DEFAULT_DAY_PAGE_SIZE,
|
||||||
|
findItemPageIndex,
|
||||||
|
formatDayPaginationSummary,
|
||||||
|
pageIndexForItem,
|
||||||
|
paginateItems,
|
||||||
|
shouldShowDayPagination,
|
||||||
|
} from "@/domain/work-orders/utils/day-pagination";
|
||||||
|
|
||||||
|
describe("day pagination utils (SH-241)", () => {
|
||||||
|
it("defaults to 50 and exposes 25/50/100/200", () => {
|
||||||
|
expect(DEFAULT_DAY_PAGE_SIZE).toBe(50);
|
||||||
|
expect(DAY_PAGE_SIZE_OPTIONS).toEqual([25, 50, 100, 200]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("paginates 60 items at 50 per page", () => {
|
||||||
|
const items = Array.from({ length: 60 }, (_, index) => index);
|
||||||
|
const first = paginateItems(items, 0, 50);
|
||||||
|
expect(first.visible).toHaveLength(50);
|
||||||
|
expect(first.pageCount).toBe(2);
|
||||||
|
const second = paginateItems(items, 1, 50);
|
||||||
|
expect(second.visible).toEqual(items.slice(50));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-paginates immediately when page size changes", () => {
|
||||||
|
const items = Array.from({ length: 60 }, (_, index) => index);
|
||||||
|
expect(paginateItems(items, 0, 25).visible).toHaveLength(25);
|
||||||
|
expect(paginateItems(items, 0, 25).pageCount).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps an out-of-range page to the last page", () => {
|
||||||
|
const paged = paginateItems([1, 2, 3], 9, 2);
|
||||||
|
expect(paged.pageIndex).toBe(1);
|
||||||
|
expect(paged.visible).toEqual([3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows pagination in day view even when the page is not full", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowDayPagination({
|
||||||
|
flatListMode: false,
|
||||||
|
isLoading: false,
|
||||||
|
noSearchResults: false,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides pagination in flat-list / loading / empty search", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowDayPagination({
|
||||||
|
flatListMode: true,
|
||||||
|
isLoading: false,
|
||||||
|
noSearchResults: false,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldShowDayPagination({
|
||||||
|
flatListMode: false,
|
||||||
|
isLoading: true,
|
||||||
|
noSearchResults: false,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
shouldShowDayPagination({
|
||||||
|
flatListMode: false,
|
||||||
|
isLoading: false,
|
||||||
|
noSearchResults: true,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps a row past the first page onto page 2 at the default size", () => {
|
||||||
|
const items = Array.from({ length: 60 }, (_, index) => ({ id: `wo-${index}` }));
|
||||||
|
expect(pageIndexForItem(50, 50)).toBe(1);
|
||||||
|
expect(findItemPageIndex(items, "wo-55", 50)).toBe(1);
|
||||||
|
expect(findItemPageIndex(items, "missing", 50)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats the AAP footer summary", () => {
|
||||||
|
expect(formatDayPaginationSummary("Mon · 06/01", 9)).toBe("MON · 06/01 · 9 items");
|
||||||
|
expect(formatDayPaginationSummary("Tue · 06/02", 1)).toBe("TUE · 06/02 · 1 item");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue