Mobile Phase 3a: read + write-supported Fundraising Grid surface

Adds the mobile-first Fundraising Grid (<768px): a lean MobileFundraisingGrid
that reads /api/fundraising/state once and renders an investor card list over
the active view (name, committed $, pipeline-stage chip, staleness-colored
recency, Existing-Investor accent, Priority corner; graveyard muted) with a
bottom-sheet view picker and search. Tap a card -> full-screen detail with
read-only commitments/contacts/notes plus edit sheets: log a note, pipeline
stage, set a reminder, and a "+ New" investor create flow with client-side
dedup typeahead.

All writes go through the targeted one-row endpoints (log-communication,
pipeline link, opportunities stage PATCH, reminders) — NEVER the whole-grid
PUT, which would race the multi-user grid (BRIEF §3a). FundraisingGridPage is
now a useIsMobile() wrapper over the renamed-but-untouched desktop grid and
the new mobile one (rules-of-hooks-safe; desktop unchanged).

Backend: inject a read-only opportunity_id into grid rows
(opportunity_id_by_source_row; added to both strip points) so the mobile detail
can PATCH a linked opp's stage directly. Earliest-opp-wins ordering keeps it
consistent with pipeline_stage and the link's canonical pick.

Editing an existing investor's name + contact pills stays read-only here
(deferred to P3b — needs a narrow per-row PATCH + pill editor).

Tests: test_grid_pipeline_link extended (opportunity_id inject/strip/round-trip);
36/36 backend green, render-smoke green.
This commit is contained in:
Keysat
2026-06-19 14:49:49 -05:00
parent 984b950f80
commit e34a6fc672
5 changed files with 635 additions and 23 deletions
+25 -7
View File
@@ -352,13 +352,31 @@ migration into each surface's build, behind one shared foundation step. No upfro
Verified: render-smoke green + a throwaway jsdom interaction harness (mounted the real app at 375px,
stubbed `/api/contacts` — list/grouping/sort-sheet/detail/back all asserted, 14/14). **No browser/real-phone
check yet** (same deferral as Phase 1 + view-reorder). **Deploy:** folds into the next s9pk build.
- **Phase 3 — Fundraising Grid (the crux).** ~70 inline styles → classes. Card list + bottom-sheet view
picker + search; full-screen detail with per-field bottom-sheet edits (name, contact pills, stage,
reminder, log note) + the `+`-create flow with client-side dedup typeahead. **Writes per `BRIEF.md`
§3a "Backend reality":** single-investor edits → `POST /api/fundraising/log-communication` (one-row,
no version race; can create investor+contact); stage → `POST /api/fundraising/pipeline/link` (needs ≥1
contact) then `PATCH /api/opportunities/{id}/stage`; commitments/amounts read-only; **never whole-grid
`PUT /state`**. Renders Phase 0's stage chip + Existing-Investor star + staleness ramp.
- **Phase 3 — Fundraising Grid (the crux). P3a BUILT 2026-06-19 (deploy pending); P3b (name/pill edit) deferred.**
Split confirmed with Grant 2026-06-19: P3a ships the readable + already-write-supported surface now;
editing an existing investor's **name + contact pills** is **P3b** (needs a new narrow per-row PATCH +
a pill-editor UI — `log-communication` can't rename/edit pills, and the whole-grid PUT is forbidden on
mobile).
- **P3a (built):** lean **`MobileFundraisingGrid`** (separate component — the desktop grid's debounced
whole-grid-PUT autosave would race on every mobile edit, so it's NOT reused; `FundraisingGridPage` is
now a `useIsMobile()` wrapper → `Desktop`/`Mobile`, desktop untouched). Card list over the **active
view** (ported the desktop view-filter predicate — graveyard/follow-up/lead flags + columnFilters — to
a shared pure helper so it can't drift), tappable view-name → **view-picker sheet**, search, the locked
**card model** (name · committed $ via `formatMoneyMobile` · stage chip · staleness-colored recency ·
Existing-Investor left-accent · Priority corner; graveyard muted). Full-screen detail (read-only:
commitments/funds, contact pills, notes) + **edit sheets**: **log a note** (`log-communication`),
**pipeline stage** (linked → `PATCH /api/opportunities/{id}/stage` via the new injected `opportunity_id`;
unlinked → `pipeline/link` then it; + remove-from-pipeline), **set a reminder** (`POST /api/reminders`),
and **`+ New` investor** (`log-communication` + `create_investor_if_missing`, client-side dedup
typeahead). **Never whole-grid `PUT /state`.** Backend: one small hook — read-only **`opportunity_id`**
injected into grid rows (`opportunity_id_by_source_row`, added to both strip points), so the detail can
PATCH the linked opp directly. Tests: `test_grid_pipeline_link` extended (opp_id inject/strip/round-trip),
36/36 green; render-smoke green; a throwaway stateful jsdom harness drove the real surface at 375px
(view filter, picker, detail, stage-PATCH, log-note, reminder, create+dedup — 18/18). **No real-phone
check yet** (same deferral as P1/P2). **Deploy:** folds into the next s9pk.
- **P3b (deferred):** `POST /api/fundraising/update-row` (version-safe single-row name/contacts mutation,
+test) + the bottom-sheet **pill editor** (add/edit/remove pills, client-side dedup). Then name + pills
become editable on an existing investor, completing BRIEF §3a's editable set.
- **Phase 4 — Pipeline.** ~7 inline styles. Swipe-between-stages (snap-scroll + segmented control +
dots), per-card stage move sharing the Grid detail's opportunities endpoints.
- **Phase 5 — Reminders.** ~18 inline styles. Urgency-grouped list, swipe complete/snooze, add/edit