All files / src/features/Settlements/ReconciliationHub/components HubMerchantDropdown.tsx

100% Statements 26/26
100% Branches 22/22
100% Functions 10/10
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120                                3x         3x 36x                                 3x             70x 70x         70x 70x 28x     70x   70x 36x 36x 6x   36x   33x                       70x 3x               70x 70x 20x     70x         70x                           2x                
import { useEffect, useMemo, useRef, useState } from "react";
import GiveThumbnail from "@shared/Thumbnail/GiveThumbnail";
import { addSizeToImage } from "@utils/image.helpers";
import HubDropdown, { HubDropdownOption } from "./HubDropdown";
import { HubFilters } from "../useHubFilters";
import { useReconciliationHubMerchants } from "../hooks/useReconciliationHubMerchants";
import { ReconciliationHubMerchantView } from "../types";
 
interface Props {
  merchantId: number | undefined;
  filters: HubFilters;
  value?: number;
  onChange: (merchantAccId?: number) => void;
  disabled?: boolean;
}
 
const ALL_MERCHANTS: HubDropdownOption = {
  value: "all",
  label: "All Merchants",
};
 
const merchantThumb = (imageUrl: string) => (
  <GiveThumbnail
    imageUrl={addSizeToImage(imageUrl, "thumb")}
    size="icon"
    type="merchant"
  />
);
 
/**
 * Controlled merchant selector for the Reconciliation Hub. The option list is
 * sourced from the SAME processor-scoped endpoint the per-merchant breakdown
 * uses (`/reconciliation-hub/merchants?processorName=…`), so it only lists
 * merchants that belong to the currently-selected processor (and period) — the
 * portfolio-wide `useGetMerchantDropdownOptions` listed every acquirer
 * submerchant regardless of processor. The query is shared/deduped with the
 * breakdown table via react-query. Search is client-side over the returned
 * (already period+processor-scoped) list. `undefined` = all merchants.
 */
const HubMerchantDropdown = ({
  merchantId,
  filters,
  value,
  onChange,
  disabled,
}: Props) => {
  const { data, isLoading } = useReconciliationHubMerchants(merchantId, filters);
  const [search, setSearch] = useState("");
 
  // The option list is re-scoped whenever the processor or period changes; a
  // term typed against the previous scope would filter the new (unrelated)
  // list and could hide every option until manually cleared, so reset it.
  const { processorName, periodType, anchorDate } = filters;
  useEffect(() => {
    setSearch("");
  }, [processorName, periodType, anchorDate]);
 
  const merchants = useMemo(() => data?.merchants ?? [], [data]);
 
  const dropdownOptions = useMemo<HubDropdownOption[]>(() => {
    const term = search.trim().toLowerCase();
    const matches = term
      ? merchants.filter((m) => m.merchantName.toLowerCase().includes(term))
      : merchants;
    return [
      ALL_MERCHANTS,
      ...matches.map((m) => ({
        value: String(m.merchantAccID),
        label: m.merchantName,
        id: m.merchantAccID,
        Image: merchantThumb(m.merchantImageURL),
      })),
    ];
  }, [merchants, search]);
 
  // Resolve the selected label from the UNFILTERED list so an active search
  // (which may hide the selected row) does not blank out the button label.
  const found =
    value != null
      ? merchants.find((m) => m.merchantAccID === value)
      : undefined;
 
  // The KPI queries stay scoped to `value` regardless of what this list
  // contains, so when a refetch under a new period/date (or a background
  // staleTime/window-focus refetch) no longer includes the selected merchant,
  // keep its last-known name — blanking to "All Merchants" would mislabel
  // single-merchant data.
  const lastResolvedRef = useRef<ReconciliationHubMerchantView>();
  useEffect(() => {
    if (found) lastResolvedRef.current = found;
  }, [found]);
  const selected =
    found ??
    (value != null && lastResolvedRef.current?.merchantAccID === value
      ? lastResolvedRef.current
      : undefined);
 
  return (
    <HubDropdown
      dataTestId="hub-merchant-dropdown"
      selectedValue={selected ? String(selected.merchantAccID) : "all"}
      selectedLabel={selected?.merchantName ?? ALL_MERCHANTS.label}
      selectedImg={selected ? merchantThumb(selected.merchantImageURL) : undefined}
      options={dropdownOptions}
      searchBarProps={{
        value: search,
        handleChange: setSearch,
        placeholder: "Search",
      }}
      isLoading={isLoading}
      onSelect={(option) =>
        onChange(option.value === "all" ? undefined : option.id)
      }
      disabled={disabled}
    />
  );
};
 
export default HubMerchantDropdown;