Files
isra el 99a3a79b3b feat: rebuild bulk send as a guided 4-step flow with validation
The old screen was one 425-line component with correctness bugs, not just a
weak layout.

Fixed defects:
- Empty recipients were sent. handleSendBulkSMS mapped every parsed row into
  the payload, so a blank phone cell produced a message addressed to "".
  There was no filtering, no phone validation and no dedupe, so the same
  number listed twice was texted twice.
- User-facing copy printed raw bytes: "max 1048576 bytes".
- The row limit could be wrong. maxRows fell back to 50 while the
  subscription query was still loading, so an unlimited-plan user could be
  told their file was too big. The cap is now only enforced once the real
  limit is known.
- Steps were gated with opacity-50 + pointer-events-none, which leaves
  controls in the tab order and unannounced. Locked steps now use `inert`.
- Data came from raw useQuery(['devices']) + devices?.data instead of the
  typed useDevices(), the same shared-cache-key shape hazard that caused the
  devices?.filter crash in round 1.

The new flow: upload (with parse summary and a preview table of the first 5
rows), map (auto-detected phone column plus live valid/invalid/duplicate
counts), compose (clickable variable chips, SMS segment counter, preview that
cycles through real recipients) and review (explicit count, an itemised list
of skipped rows and why, nothing sent until confirmed).

bulk-csv.ts holds the rules that decide who receives a message, as pure
functions with 24 unit tests. Those tests caught a real bug during
development: the short "to" hint substring-matched a column named "total",
which would have silently auto-selected the wrong column. Short hints are now
exact-match only.

Sample CSV at /samples/bulk-sms-sample.csv using 555 reserved-for-fiction US
and Canada numbers, so an unedited upload cannot text a real person, with an
order-confirmation template that demonstrates multi-variable interpolation.

E2e asserts the actual request payload contains exactly the two valid
recipients from a file seeded with a blank, an invalid and a duplicate row,
and that the parsed-CSV table does not reintroduce sideways scroll at 375px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:42:55 +03:00
..