import { useEffect, useMemo, useState } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; import { buildBatchSummary, buildBulkPurchaseOrders, findLargestAddressGroup, } from './display'; import { ORDERS_SHIPPING_CONTEXT_ENTITY } from './constants'; import { toError } from './store'; import { useAutoAssignedPackages } from './use-auto-assigned-packages'; import type { AddressGrouping, BatchSummary, BulkPurchaseOrder, ManualPackageSelections, OrderShippingContextRecord, } from './types'; const EMPTY_MANUAL_SELECTIONS: ManualPackageSelections = {}; interface UseBulkPurchaseOrdersResult { isResolving: boolean; error: Error | null; records: OrderShippingContextRecord[]; orders: BulkPurchaseOrder[]; summary: BatchSummary; grouping: AddressGrouping | null; } const EMPTY_RECORDS: OrderShippingContextRecord[] = []; /** * Reactive hook that pulls orders shipping context via core-data's * useEntityRecords, then layers on the WOOSHIP-2133 fields the * endpoint doesn't yet provide (service, cost, status, note) so the * modal can render every column. Components reading this hook won't * have to change once those fields land for real. */ export const useBulkPurchaseOrders = ( orderIds: number[], manualSelections: ManualPackageSelections = EMPTY_MANUAL_SELECTIONS ): UseBulkPurchaseOrdersResult => { // Stringify so a fresh array reference with the same IDs doesn't // re-trigger the fetch. const idsKey = orderIds.join( ',' ); const hasIds = orderIds.length > 0; const [ records, setRecords ] = useState< OrderShippingContextRecord[] >( EMPTY_RECORDS ); const [ isShippingContextResolving, setIsShippingContextResolving ] = useState( false ); const [ error, setError ] = useState< Error | null >( null ); useEffect( () => { if ( ! hasIds ) { setRecords( EMPTY_RECORDS ); setError( null ); setIsShippingContextResolving( false ); return; } let isCurrent = true; setRecords( EMPTY_RECORDS ); setError( null ); setIsShippingContextResolving( true ); void apiFetch< OrderShippingContextRecord[] >( { path: addQueryArgs( ORDERS_SHIPPING_CONTEXT_ENTITY.baseURL, { ids: orderIds, } ), } ) .then( ( nextRecords ) => { if ( ! isCurrent ) { return; } setRecords( Array.isArray( nextRecords ) ? nextRecords : EMPTY_RECORDS ); } ) .catch( ( fetchError ) => { if ( ! isCurrent ) { return; } setError( toError( fetchError ) ); } ) .finally( () => { if ( isCurrent ) { setIsShippingContextResolving( false ); } } ); return () => { isCurrent = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps -- idsKey covers orderIds contents. }, [ idsKey, hasIds ] ); const safeRecords = useMemo( () => records ?? EMPTY_RECORDS, [ records ] ); const usableRecords = useMemo( () => safeRecords.filter( ( record ) => ! record.error ), [ safeRecords ] ); // Only ask the box-packer for orders that actually loaded — orders that // errored out of the shipping-context fetch are skipped entirely so we // don't waste a batch slot on rows the modal will never render. const assignableOrderIds = useMemo( () => usableRecords.map( ( record ) => record.order_id ), [ usableRecords ] ); const { isResolving: isAutoAssignResolving, results: autoAssignedPackages, error: autoAssignError, } = useAutoAssignedPackages( assignableOrderIds ); const orders = useMemo( () => buildBulkPurchaseOrders( usableRecords, autoAssignedPackages, manualSelections ), [ usableRecords, autoAssignedPackages, manualSelections ] ); const summary = useMemo( () => buildBatchSummary( orders ), [ orders ] ); const grouping = useMemo( () => findLargestAddressGroup( usableRecords ), [ usableRecords ] ); return { // Hold the modal in its loading state until both the shipping-context // fetch and the box-packer auto-assign call have settled, so the // table doesn't flash placeholder packages before the suggestion // arrives. isResolving: hasIds && ( isShippingContextResolving || isAutoAssignResolving ), // Surface a failed auto-assign as a modal-level error rather than // letting rows fall back to placeholder packages and read as // "ready". The shipping-context error takes precedence since // without records there's nothing to suggest packages for. error: error ?? autoAssignError, records: safeRecords, orders, summary, grouping, }; };