All files / src/features/Events/EventDetail/tickets EventTicketsTab.tsx

100% Statements 11/11
100% Branches 8/8
100% Functions 1/1
100% Lines 11/11

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                                                                                                    4x                                               12x   12x         12x       12x   12x                       12x                             12x 1x             11x   11x                                                                                        
import { ReactNode } from "react";
import Table from "componentsV2/Table/Table";
import { useTable } from "componentsV2/Table/hooks/useTable";
import { QKEY_EVENT_TICKETS } from "@constants/queryKeys";
import { FilterPagesEnum } from "@shared/GiveFilter/constants";
import { useFilterButton } from "@shared/GiveFilter/hooks/useFilterButton";
import { EVENT_TICKET_DETAIL_MODAL } from "modals/modal_names";
import {
  EVENT_BANNER_ANIMATION_PAGE,
  STORED_EVENT_TICKETS_ROWS_PER_PAGE,
} from "../constants";
import TabErrorState from "../TabErrorState";
import { useEventTicketColumns } from "./useEventTicketColumns";
import { TICKET_SORT_KEYS, useEventTickets } from "./useEventTickets";
import { useTicketPickup } from "./useTicketPickup";
 
type Props = {
  eventId?: string;
  /**
   * The shared banner head (breadcrumb offset, cards, tabs, search). A render
   * function because only this tab knows whether its list is empty, and the
   * empty state is drawn without the search / Export row.
   */
  renderHead: (options?: { hideToolbar?: boolean }) => ReactNode;
  onScroll?: (e: React.UIEvent<HTMLDivElement>) => void;
  searchQuery?: string;
  handleSearchChange?: (value: string) => void;
  /**
   * The event offers printed tickets, so the "Ticket Picked Up" column applies
   * (frame 7594-66313). Read from the event, not a feature flag.
   */
  showPickupColumn?: boolean;
  /**
   * The collapsed header revealed once the banner scrolls away (frame
   * 8122-61150) — built by the page so both tabs share one instance.
   */
  compactHead?: ReactNode;
  /** The event name, shown in the mobile top bar while scrolled. */
  compactTitle?: ReactNode;
  /** Told when the collapsed header crosses its reveal threshold. */
  onCompactVisibleChange?: (isVisible: boolean) => void;
  /** Page gutters, so the collapsed header's labels align to the columns. */
  tablePadding?: string;
};
 
/**
 * PayBuilder 032 — the Tickets tab (frames 7397-90412/90421/90442): the
 * per-ticket list with check-in, on the same V2 table the Transactions tab
 * uses so skeleton, empty state (frame 7397-90358) and pagination match.
 */
const EventTicketsTab = ({
  eventId,
  renderHead,
  onScroll,
  searchQuery,
  handleSearchChange,
  showPickupColumn,
  compactHead,
  compactTitle,
  onCompactVisibleChange,
  tablePadding,
}: Props) => {
  const {
    allRows,
    totalRows,
    rowsPerPage,
    page,
    setPage,
    setPageDispatcher,
    isLoading,
    isFetching,
    isError,
    checkIn,
    checkingInId,
  } = useEventTickets(eventId, showPickupColumn);
 
  const { setPickupStatus, savingId } = useTicketPickup(eventId);
 
  // The same panel the toolbar's Filter button opens, read here for its count:
  // the tab is the only place that knows both how many filters are applied and
  // whether the list came back empty, which together decide the empty state.
  const { filtersAmount, handleResetFilters } = useFilterButton({
    filterPage: FilterPagesEnum.EVENT_TICKETS,
    productId: eventId,
  });
  const hasFilters = filtersAmount > 0;
 
  const { columns } = useEventTicketColumns({
    onCheckIn: checkIn,
    checkingInId,
    sortKeys: TICKET_SORT_KEYS,
    showPickupColumn,
    onPickupChange: setPickupStatus,
    savingPickupId: savingId,
  });
 
  // Row click opens the detail panel; `useTable` also drives its up/down
  // navigation by moving the selected row (and paging when it runs off the end).
  const { setSelectedRowIdx, selectedRowIdx, loadMore, numberOfPages } =
    useTable({
      openModal: EVENT_TICKET_DETAIL_MODAL,
      allRows,
      isLoading,
      isFetching,
      totalRows,
      page,
      setPage,
      setPageDispatcher,
      useIdForRows: true,
      storageKey: STORED_EVENT_TICKETS_ROWS_PER_PAGE,
    });
 
  // The error replaces the list, never the page: the head (and with it the
  // tabs) stays mounted so Transactions remains reachable.
  if (isError && !isLoading)
    return <TabErrorState head={renderHead({ hideToolbar: true })} />;
 
  // Frame 7397-90358: an event with no tickets shows only the banner, tabs and
  // the empty state. A search OR a filter that matched nothing keeps the toolbar
  // — hiding it would strand the user with no way to clear what narrowed the
  // list (frame 8238-62023, "Filter / No results").
  const hideToolbar =
    !isLoading && !searchQuery && !hasFilters && allRows.length === 0;
 
  return (
    <Table
      type="event-tickets"
      sectionName="Tickets"
      allRows={allRows}
      totalRows={totalRows}
      rowsPerPage={rowsPerPage}
      isLoading={isLoading}
      isFetching={isFetching}
      page={page}
      setPage={setPage}
      setPageDispatcher={setPageDispatcher}
      columns={columns}
      openModal={EVENT_TICKET_DETAIL_MODAL}
      queryKey={QKEY_EVENT_TICKETS}
      sortReduxKey={QKEY_EVENT_TICKETS}
      defaultSortKey={TICKET_SORT_KEYS.purchaseDate}
      storageKey={STORED_EVENT_TICKETS_ROWS_PER_PAGE}
      searchQuery={searchQuery}
      handleSearchChange={handleSearchChange}
      // Turns the empty state from "No tickets purchased yet" into the filtered
      // one — "No results" over a Clear Filters button (frame 8238-62023).
      hasFilters={hasFilters}
      handleResetFilters={handleResetFilters}
      selectedRowIdx={selectedRowIdx}
      setSelectedRowIdx={setSelectedRowIdx}
      loadMore={loadMore}
      numberOfPages={numberOfPages}
      Head={renderHead({ hideToolbar })}
      onScroll={onScroll}
      useIdForRows
      // The content sits on the page's animated backdrop, so the column
      // header scrolls off with it, transparent (see Table).
      scrollingHeader
      CompactHead={compactHead}
      compactTitle={compactTitle}
      onCompactVisibleChange={onCompactVisibleChange}
      compactAnimationPage={EVENT_BANNER_ANIMATION_PAGE}
      tablePadding={tablePadding}
    />
  );
};
 
export default EventTicketsTab;