All files / src/shared/Banner GiveMerchantBanner.tsx

100% Statements 33/33
76.19% Branches 32/42
100% Functions 9/9
100% Lines 29/29

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 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273                                                                                                11x                                 543x               543x 543x 543x 543x   543x   543x   8x             58x                         58x 8x   8x 46x 8x   38x     46x         8x 8x 8x 9x       8x                                 8x 7x                           1x                     1x                           535x                                                                                                                                                   535x           535x                      
import { Box, Stack } from "@mui/material";
import { PlusIcon } from "@phosphor-icons/react";
import CompactStat from "@shared/Banner/CompactStat";
import { styled, useAppTheme } from "@theme/v2/Provider";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import { GiveTooltipProps } from "@shared/Tooltip/GiveTooltip.types";
import GiveTabs from "@shared/Tabs/GiveTabs";
import { useAppSelector } from "@redux/hooks";
import { selectMerchantTab } from "@redux/slices/enterprise/merchants";
import { GiveTabItem } from "@shared/Tabs/GiveTab.types";
import GiveTableActionsHandler from "componentsV2/Table/GiveTableActionsHandler";
import GiveLargeIconButton from "@shared/LargeIconButton/GiveLargeIconButton";
import { ExportRecordsTotal } from "@common/Table/helpers";
import { TabNames } from "features/Merchants/MerchantsTable/hooks/useTabs";
import useThemeSettings from "@components/EnterpriseSettings/Branding/hooks/useThemeSettings";
import { StyledTitle } from "./styles";
import { WIDTH as SIDE_MENU_WIDTH } from "componentsV2/SideMenu/SideMenuDrawer";
import { useDrawer } from "componentsV2/SideMenu/DrawerContext";
 
type Props = {
  title: string;
  filters: GiveTabItem[];
  action: string;
  handleCreate?: () => void;
  disableCreate?: boolean;
  tooltipProps?: Partial<GiveTooltipProps>;
  actualTotalRows?: ExportRecordsTotal | number;
  totalRows?: number;
  type: "provider" | "merchant" | "developer";
  handleSearchChange?: (value: string) => void;
  search: string;
  defaultTab?: string;
  /**
   * Compact-on-scroll banner variant (AC004): a single slim row showing the
   * same tab counts as stats, with search + actions + tab filters collapsed
   * into a search icon + kebab menu.
   */
  compact?: boolean;
  /** Per-tab counts keyed by tab value, used to render the compact stats. */
  countsMap?: Record<string, number>;
  /**
   * Explicit compact stats, overriding the tab-derived ones. Used by screens
   * that have a single count instead of tab counts (e.g. Developer API → "API
   * Keys"). The first stat is pinned left; any others sit in the right cluster.
   */
  compactStats?: { key?: string; value: number | string; label: string }[];
};
 
const GiveMerchantBanner = ({
  title,
  action,
  filters,
  handleCreate,
  disableCreate,
  tooltipProps,
  actualTotalRows,
  totalRows,
  type,
  handleSearchChange,
  search,
  defaultTab = TabNames.all,
  compact = false,
  countsMap,
  compactStats,
}: Props) => {
  const { isMobileView, isWideView } = useCustomThemeV2();
  const {
    textColor,
    bgColorDefault,
    bgColorHighlight,
    isCustomColor,
    colors,
    isGradientText,
  } = useThemeSettings();
  const tab = useAppSelector(selectMerchantTab);
  const theme = useAppTheme();
  const { open } = useDrawer();
 
  const selectedTab = tab || defaultTab;
 
  if (compact) {
    const actions = (
      <GiveTableActionsHandler
        totalRows={actualTotalRows}
        totalFilteredRows={totalRows}
        type={type}
        handleSearchChange={handleSearchChange}
        search={search}
        compact
        filters={filters.map((t) => ({
          label: String(t.label),
          value: t.value,
          hidden: t.hidden,
          onClickItem: t.onClickItem,
        }))}
        selectedTab={selectedTab}
        handleCreate={handleCreate}
        createLabel={action}
        disableCreate={disableCreate}
      />
    );
 
    const visibleTabs = filters.filter((t) => !t.hidden);
    const statColor = isCustomColor ? textColor : bgColorDefault;
 
    const getStatLabel = (t: (typeof visibleTabs)[number]) => {
      if (t.value === defaultTab) {
        return title;
      }
      return String(t.label).replace(/\s*\(.*\)\s*$/, "");
    };
 
    const derivedStats = visibleTabs.map((t) => ({
      key: t.value,
      value: countsMap?.[t.value] ?? 0,
      label: getStatLabel(t),
    }));
    const stats = compactStats ?? derivedStats;
    const [firstStat, ...otherStats] = stats;
    const statValue = (s: (typeof stats)[number]) =>
      typeof s.value === "number" ? s.value.toLocaleString() : s.value;
 
    // The primary stat (default tab, e.g. "225 Providers") pinned left — shared
    // by the desktop compact row and the ≤768 collapsed row.
    const primaryStat = firstStat ? (
      <CompactStat
        value={statValue(firstStat)}
        label={firstStat.label}
        align="left"
        textColor={statColor}
      />
    ) : (
      <span />
    );
 
    // TRA013: below the wide breakpoint (< 1024px — the same point the table body
    // switches to cards) the per-tab stats overflow and push the search +
    // three-dots off screen, so collapse to just the primary stat and drop the
    // remaining columns from the right, letting the actions cluster (search +
    // kebab) fill the rest of the row. Covers phones (hosted in the mobile top
    // bar) and tablet portrait (hosted in the inline compact bar). See the mockup.
    if (!isWideView) {
      return (
        <Stack
          direction="row"
          alignItems="center"
          width="100%"
          gap="16px"
          sx={{ "& > :last-child": { flexGrow: 1, minWidth: 0 } }}
        >
          {primaryStat}
          {actions}
        </Stack>
      );
    }
 
    return (
      <Stack
        direction="row"
        alignItems="center"
        justifyContent="space-between"
        width="100%"
        gap="24px"
      >
        {primaryStat}
        <Stack direction="row" alignItems="center" gap="40px">
          {otherStats.map((s, i) => (
            <CompactStat
              key={s.key ?? `${s.label}-${i}`}
              value={statValue(s)}
              label={s.label}
              align="right"
              textColor={statColor}
            />
          ))}
          {actions}
        </Stack>
      </Stack>
    );
  }
 
  return (
    <StyledWrapper>
      <ContentContainer>
        <StyledTitle
          theme={theme}
          textColor={isCustomColor ? textColor : bgColorDefault}
          variant="h1"
        >
          {title}
        </StyledTitle>
        <GiveLargeIconButton
          Icon={PlusIcon}
          label={isWideView ? action : ""}
          tooltipProps={tooltipProps}
          onClick={handleCreate}
          disabled={disableCreate}
          defaultBgColor={isCustomColor ? textColor : bgColorDefault}
          highlightBgColor={isCustomColor ? textColor : bgColorHighlight}
          iconSize={isWideView ? 36 : 30.86}
          hug={isWideView ? 56 : 48}
        />
      </ContentContainer>
      <Box
        pb="20px"
        sx={{ borderTop: `1px solid ${theme.palette.border?.primary}` }}
      >
        <GiveTableActionsHandler
          totalRows={actualTotalRows}
          totalFilteredRows={totalRows}
          type={type}
          handleSearchChange={handleSearchChange}
          search={search}
        />
 
        <GiveTabs
          items={filters}
          type="pill"
          selected={tab || defaultTab}
          variant="gradient"
          containerSx={{
            display: "flex",
            overflow: "auto",
            scrollbarWidth: "none",
            whiteSpace: "nowrap",
            [theme.breakpoints.down("v2_sm")]: {
              width: "calc( 100vw - 40px )",
              paddingTop: "16px",
            },
            [theme.breakpoints.between("v2_sm", "v2_lg")]: {
              width: open
                ? `calc( 100vw - 80px - ${SIDE_MENU_WIDTH}px )`
                : "calc( 100vw - 132px - 80px )",
            },
            gap: "4px",
          }}
          tabSx={
            isMobileView
              ? {
                  flex: 1,
                  justifyContent: "center",
                  alignItems: "center",
                }
              : {}
          }
          selectedTabSx={{
            textColor: isGradientText ? colors?.[0] : textColor,
            backgroundColor: bgColorHighlight,
          }}
        />
      </Box>
    </StyledWrapper>
  );
};
 
const StyledWrapper = styled(Box)(() => ({
  width: "100%",
  position: "relative",
  paddingTop: "120px",
}));
 
const ContentContainer = styled(Box)(({ theme }) => ({
  display: "flex",
  justifyContent: "space-between",
  alignItems: "center",
  marginBottom: "20px",
  [theme.breakpoints.down("new_lg")]: {
    marginBottom: "24px",
  },
}));
 
export default GiveMerchantBanner;