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

100% Statements 23/23
92.85% Branches 13/14
100% Functions 8/8
100% Lines 20/20

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                                                    6x                                         6x 14x 14x   14x   27x       14x 6x 6x     14x 4x 4x 3x     14x                                                       2x       42x           5x               2x 2x 2x                  
import { MouseEvent, useState } from "react";
import { CaretDownIcon, CaretUpIcon, CheckIcon } from "@phosphor-icons/react";
import ContextualMenu from "@shared/ContextualMenu/ContextualMenu";
import GiveChip from "@shared/Chip/GiveChip";
import { useAppTheme } from "@theme/v2/Provider";
import {
  EventTicketRow,
  TICKET_PICKUP_LABELS,
  TICKET_PICKUP_OPTIONS,
  TicketPickupStatus,
} from "./ticket.types";
 
type Props = {
  row: EventTicketRow;
  onChange: (row: EventTicketRow, status: TicketPickupStatus) => void;
  isSaving?: boolean;
};
 
/**
 * The pill the three states share (frame 7594-65104): a 132px-wide 40px-radius
 * chip, 6px/16px padding at 12px/14px with a 4px icon gap. The theme's
 * `size="large"` rule drops to 4px/8px padding once `onDelete` is set — that
 * shape is for a dismissible chip, not this one — and `GiveChip` sets a 12px
 * gap, so both are restated here, along with the icon margins MUI adds. Same
 * `!important`/specificity reason as `TicketSummaryCard`'s CHIP_SX.
 */
const CHIP_SX = {
  height: "28px",
  width: "132px",
  maxWidth: "100%",
  fontSize: "12px",
  borderRadius: "40px",
  padding: "6px 16px",
  gap: "4px !important",
  "& .MuiChip-deleteIcon": { margin: 0 },
};
 
/**
 * The "Ticket Picked Up" cell (frame 7594-66313): a status chip that opens a
 * three-option menu — Pending / Collected / Delivered, a tick against the current
 * one.
 *
 * Editable in place because it records something that happened at a desk: the
 * merchant marks a ticket handed over while the holder is standing there, and
 * making them open the detail panel for it would be a step too many. The panel
 * shows the same value read-only.
 */
const TicketPickupCell = ({ row, onChange, isSaving }: Props) => {
  const { palette } = useAppTheme();
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
 
  const current = row.pickupStatus ?? "pending";
  const option =
    TICKET_PICKUP_OPTIONS.find((o) => o.value === current) ??
    TICKET_PICKUP_OPTIONS[0];
 
  // Rows open the detail panel on click — the chip and its menu must not.
  const stop = (event: MouseEvent<HTMLElement>) => {
    event.stopPropagation();
    event.preventDefault();
  };
 
  const handleOpen = (event: MouseEvent<HTMLElement>) => {
    stop(event);
    if (isSaving) return;
    setAnchorEl(event.currentTarget);
  };
 
  return (
    <>
      <GiveChip
        label={TICKET_PICKUP_LABELS[option.value]}
        color={option.color}
        // Each state is the tinted treatment — surface at the `25`/`10` token
        // (the neutral one at `surface.tertiary`), label and caret at `100` —
        // which the theme only applies to `variant="light"`. Without it MUI
        // falls back to its own solid `filled` palette, which is what made
        // "Delivered" read as a solid green badge and "Collected" a solid blue
        // one.
        variant="light"
        size="large"
        onClick={handleOpen}
        disabled={isSaving}
        data-testid={`ticket-pickup-${row.id}`}
        // The caret points at where the menu is, per the open-state chip in the
        // frame.
        deleteIcon={
          anchorEl ? <CaretUpIcon size={16} /> : <CaretDownIcon size={16} />
        }
        // MUI only renders the trailing icon when onDelete is set; the chip as a
        // whole opens the menu, so the caret is decorative and repeats the click.
        onDelete={handleOpen as unknown as () => void}
        sx={{ ...CHIP_SX, cursor: isSaving ? "default" : "pointer" }}
      />
      <ContextualMenu
        anchorEl={anchorEl}
        handleClose={() => setAnchorEl(null)}
        menuWidth={180}
        color="primary"
        texture="solid"
        options={TICKET_PICKUP_OPTIONS.map(({ value, label }) => ({
          text: label,
          // The tick marks what is already recorded (frame 7594-66313).
          IconRight:
            value === current
              ? (iconProps: object) => (
                  <CheckIcon
                    {...iconProps}
                    size={16}
                    color={palette.text?.primary}
                  />
                )
              : undefined,
          onClick: (event: MouseEvent<HTMLDivElement>) => {
            stop(event);
            setAnchorEl(null);
            if (value !== current) onChange(row, value);
          },
        }))}
      />
    </>
  );
};
 
export default TicketPickupCell;