FormItemTable: gate the global quantity field on row availability


GOAL

Current state: In FormItemTable a row is flagged disabled when no tier rate applies and no
custom rate is set. That flag reaches only the expanded panel (ExpandedRowContent, and from
there ItemTableField and the inline notes field). The global quantity input and the custom
rate input are rendered without it, so the quantity of an unavailable row stays fully
editable. Consumers gate meta field validation on quantity rather than on availability: in
summit-admin the sponsor cart edit form makes every meta field required once the row
quantity is above zero. A user can therefore raise the quantity on a disabled row, trigger
required-field errors, and find the very fields carrying those errors locked. The row then
auto-expands to display errors the user has no way to clear, and the form cannot be
submitted until the quantity is put back to zero.

Target state: availability drives one consistent gate across the whole row, so a row that
cannot be priced cannot accumulate a quantity or unclearable validation errors, while the
custom rate input stays editable so an unavailable row can always be given a rate.

Note: PR 327 made the disabled flag a live function of the custom rate value, which opened a
new path into this state (set a custom rate, raise the quantity, clear the custom rate). The
underlying gap predates that PR and is not caused by it.


TASKS

- Decide the gating mechanism: pass the availability flag into GlobalQuantityField, or keep
  the quantity editable and instead relax the consumer-side meta field validation for
  unavailable rows. Prefer the first if quantity validation survives it (see Development
  Notes).
- Apply the gate in FormItemTable so the quantity input follows the same availability rule
  as the expanded panel.
- Confirm the custom rate input stays ungated. Gating it would make an unavailable row
  impossible to rescue with a custom rate.
- Handle the transition case: a row that already carries a quantity above zero and then
  becomes unavailable because the custom rate was cleared. Decide whether the quantity
  resets to zero or is preserved read only.
- Add tests for the new gate, including the set rate, raise quantity, clear rate sequence.
- Verify in a browser against the summit-admin sponsor cart edit form that no unclearable
  validation state remains.


ACCEPTANCE CRITERIA

- With no applicable tier rate and no custom rate, the user cannot raise the row quantity
  above zero.
- With a custom rate set, the quantity input, the custom rate input and the expanded fields
  are all editable.
- Clearing the custom rate on a row with no applicable tier rate returns the whole row to
  the gated state without leaving behind a validation error the user cannot clear.
- The custom rate input is editable on every row regardless of availability.
- The quantity field still validates against its own schema (summit-admin marks it
  required) after the gating change.
- npx jest src/components/mui/FormItemTable passes with 0 failures.
- A test covers the set custom rate, raise quantity, clear custom rate sequence and asserts
  no unclearable required-field error remains.
- The summit-admin sponsor cart edit form submits successfully for an expired-tier row that
  carries a custom rate, verified in a browser.


DEVELOPMENT NOTES

Key files:

  src/components/mui/FormItemTable/index.js
    Computes the disabled flag in the row render loop; renders GlobalQuantityField and the
    custom rate MuiFormikPriceField, neither of which receives it.

  src/components/mui/FormItemTable/components/GlobalQuantityField.js
    Already accepts a disabled prop defaulting to false. FormItemTable never passes it.

  src/components/mui/FormItemTable/components/ExpandedRowContent.js
    The only place the disabled flag currently reaches.

  src/components/mui/FormItemTable/helpers.js
    isItemAvailable, the availability rule itself.

  summit-admin: src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/edit-form/index.js
    buildValidationSchema, where meta field validation is gated on quantity above zero and
    the quantity schema is marked required.

Gotchas:

  GlobalQuantityField carries the comment "using readOnly since formik won't validate
  disabled fields" and deliberately uses readOnly, not disabled, for the driven-quantity
  case. Passing disabled naively will suppress formik validation for the quantity field
  itself, and summit-admin marks that field required. Confirm the required validation still
  fires before settling on the disabled approach; readOnly plus the greyed-out styling the
  component already applies may be the correct mechanism.

  GlobalQuantityField runs helpers.setValue(value) inside a useEffect on every value change.
  Any reset-to-zero behaviour must not fight that effect.

  Zero is the agreed "no custom rate" sentinel on both sides of the wire. purchases-api
  apps/cart/models/cart_form_item.py uses "if custom_rate" in create_with_defaults and
  clears the custom rate on a falsy value in update. Do not switch one side to a null check.

  sponsor-services keeps its own fork of FormItemTable and its helpers, with no custom rate
  column at all. Changes here do not reach it and do not need to.

Risks:

  Risk: gating the quantity with disabled silently removes its required validation, letting
  an invalid form submit.
  Mitigation: fall back to readOnly with the existing styling, and assert the quantity
  validation still fires in a test.

  Risk: resetting the quantity to zero when a row becomes unavailable discards user input on
  what may be a transient state change.
  Mitigation: preserve the value read only, and reset only if product confirms.

Out of scope:

  The custom rate pricing fix itself, covered by PR 327.
  The sponsor-services fork of FormItemTable.
  Any change to purchases-api rate resolution.
