|
- "use client";
-
- import type { PoResult } from "@/app/api/po";
- import type { RecordsRes } from "@/app/api/utils";
- import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
- import { NEXT_PUBLIC_API_URL } from "@/config/api";
- import { mapPoResultToListRow } from "@/components/PoWorkbench/search/poWorkbenchMapPoResult";
- import {
- buildWorkbenchPoListSearchParams,
- PO_WORKBENCH_LIST_PAGE_SIZE,
- } from "@/components/PoWorkbench/search/poWorkbenchPoListQuery";
- import type {
- PoWorkbenchAdvancedFilters,
- PoWorkbenchListRow,
- } from "@/components/PoWorkbench/types";
- import { useCallback, useEffect, useRef, useState } from "react";
-
- const PO_LIST_PATH = "/po/list";
-
- /** Bumps after each keystroke delay; triggers a new `/po/list` first page. */
- function useDebouncedValue(value: string, delayMs: number): string {
- const [debounced, setDebounced] = useState(value);
- useEffect(() => {
- const id = window.setTimeout(() => setDebounced(value), delayMs);
- return () => window.clearTimeout(id);
- }, [value, delayMs]);
- return debounced;
- }
-
- async function fetchPoListPage(
- poNumber: string,
- advanced: PoWorkbenchAdvancedFilters,
- pageNum: number,
- pageSize: number,
- signal?: AbortSignal,
- ): Promise<{ rows: PoWorkbenchListRow[]; total: number }> {
- const params = buildWorkbenchPoListSearchParams(
- poNumber,
- advanced,
- pageNum,
- pageSize,
- );
- const url = `${NEXT_PUBLIC_API_URL}${PO_LIST_PATH}?${params.toString()}`;
- const response = await clientAuthFetch(url, { method: "GET", signal });
- if (!response.ok) {
- throw new Error(`PO list failed: HTTP ${response.status}`);
- }
- const body = (await response.json()) as RecordsRes<PoResult[]>;
- const records = Array.isArray(body.records) ? body.records : [];
- const total = typeof body.total === "number" ? body.total : records.length;
- return {
- rows: records.map(mapPoResultToListRow),
- total,
- };
- }
-
- export interface UsePoWorkbenchListSearchArgs {
- poNumberQuery: string;
- advancedFilters: PoWorkbenchAdvancedFilters;
- /** Delay after typing in the PO field before hitting the API (ms). */
- poNumberDebounceMs?: number;
- pageSize?: number;
- }
-
- export interface UsePoWorkbenchListSearchResult {
- listRows: readonly PoWorkbenchListRow[];
- /** Total rows matching filters (from API), not how many are loaded yet. */
- totalMatches: number;
- isLoading: boolean;
- isLoadingMore: boolean;
- loadError: string | null;
- hasMore: boolean;
- /** Loads the next page; no-op if nothing more to load or a request is in flight. */
- loadMore: () => void;
- }
-
- /**
- * Fetches `/po/list` for the workbench: first page when debounced filters change, then `loadMore` appends pages.
- * Stale HTTP responses are ignored via `searchGenerationRef`. Does not use legacy `PoSearch`.
- */
- export function usePoWorkbenchListSearch({
- poNumberQuery,
- advancedFilters,
- poNumberDebounceMs = 350,
- pageSize = PO_WORKBENCH_LIST_PAGE_SIZE,
- }: UsePoWorkbenchListSearchArgs): UsePoWorkbenchListSearchResult {
- const debouncedPoNumber = useDebouncedValue(
- poNumberQuery,
- poNumberDebounceMs,
- );
-
- const [listRows, setListRows] = useState<PoWorkbenchListRow[]>([]);
- const [totalMatches, setTotalMatches] = useState(0);
- const [isLoading, setIsLoading] = useState(true);
- const [isLoadingMore, setIsLoadingMore] = useState(false);
- const [loadError, setLoadError] = useState<string | null>(null);
- /** Highest 1-based page number successfully merged into listRows. */
- const loadedPageRef = useRef(0);
- const loadMoreInFlightRef = useRef(false);
- /** Incremented on each new search so stale responses never mutate state. */
- const searchGenerationRef = useRef(0);
- const abortRef = useRef<AbortController | null>(null);
-
- useEffect(() => {
- abortRef.current?.abort();
- const controller = new AbortController();
- abortRef.current = controller;
-
- const generation = ++searchGenerationRef.current;
- loadedPageRef.current = 0;
- loadMoreInFlightRef.current = false;
-
- setIsLoading(true);
- setLoadError(null);
- setListRows([]);
- setTotalMatches(0);
-
- (async () => {
- try {
- const { rows, total } = await fetchPoListPage(
- debouncedPoNumber,
- advancedFilters,
- 1,
- pageSize,
- controller.signal,
- );
- if (generation !== searchGenerationRef.current) {
- return;
- }
- loadedPageRef.current = 1;
- setListRows(rows);
- setTotalMatches(total);
- } catch (e) {
- if (
- controller.signal.aborted ||
- generation !== searchGenerationRef.current
- ) {
- return;
- }
- const message = e instanceof Error ? e.message : "Unknown error";
- setLoadError(message);
- setListRows([]);
- setTotalMatches(0);
- } finally {
- if (generation === searchGenerationRef.current) {
- setIsLoading(false);
- }
- }
- })();
-
- return () => {
- controller.abort();
- };
- }, [advancedFilters, debouncedPoNumber, pageSize]);
-
- const hasMore = listRows.length < totalMatches;
-
- const loadMore = useCallback(async () => {
- if (!hasMore || isLoading || loadMoreInFlightRef.current) {
- return;
- }
- const generation = searchGenerationRef.current;
- const nextPage = loadedPageRef.current + 1;
- loadMoreInFlightRef.current = true;
- setIsLoadingMore(true);
- setLoadError(null);
-
- try {
- const { rows } = await fetchPoListPage(
- debouncedPoNumber,
- advancedFilters,
- nextPage,
- pageSize,
- );
- if (generation !== searchGenerationRef.current) {
- return;
- }
- loadedPageRef.current = nextPage;
- setListRows((prev) => {
- const seen = new Set(prev.map((r) => r.id));
- const merged = [...prev];
- for (const row of rows) {
- if (!seen.has(row.id)) {
- seen.add(row.id);
- merged.push(row);
- }
- }
- return merged;
- });
- } catch (e) {
- if (generation === searchGenerationRef.current) {
- const message = e instanceof Error ? e.message : "Unknown error";
- setLoadError(message);
- }
- } finally {
- loadMoreInFlightRef.current = false;
- if (generation === searchGenerationRef.current) {
- setIsLoadingMore(false);
- }
- }
- }, [advancedFilters, debouncedPoNumber, hasMore, isLoading, pageSize]);
-
- return {
- listRows,
- totalMatches,
- isLoading,
- isLoadingMore,
- loadError,
- hasMore,
- loadMore,
- };
- }
|