# Member modal — raw payments debug list ## Context When a payer's bank message doesn't follow our convention, [`infer_payments.py`](scripts/infer_payments.py) may map the transfer to the wrong period (or none), and today the member detail modal hides this — it only shows the post-allocation, per-month splits produced by [`reconcile()`](scripts/match_payments.py:295). To diagnose these cases the user needs to see the **original sheet rows** that were attributed to a member: full `Amount`, `Inferred Amount`, `Person`, `Purpose`, `Sender`, `Message`, `Bank ID`, `manual fix`. The list should be hidden by default and revealed by a small toggle, since it is only relevant during debugging. ## Approach Reuse the grouping logic that already exists in the [`/payments` route](app.py:540-553): group raw `tx` dicts by parsed `Person`, expose that mapping to the modal, and render it on demand under a new collapsible section. ### 1. Backend — group raw txs by member In [`app.py`](app.py): - Factor the existing per-person grouping in [`payments()`](app.py:530-568) into a small helper near the top of the file: ```python def group_payments_by_person(transactions): grouped = {} for tx in transactions: person = str(tx.get("person", "")).strip() if not person: continue # unmatched rows are not tied to a member for p in person.split(","): p = re.sub(r"\[\?\]\s*", "", p).strip() if not p: continue grouped.setdefault(p, []).append(tx) for rows in grouped.values(): rows.sort(key=lambda t: str(t.get("date", "")), reverse=True) return grouped ``` Call it from [`payments()`](app.py:530), [`adults_view()`](app.py:160) and [`juniors_view()`](app.py:326) — the existing `payments()` body collapses to one line. - In `adults_view()` and `juniors_view()`, after `transactions = get_cached_data(...)`, build `raw_payments_by_person = group_payments_by_person(transactions)` and pass it to `render_template` as `raw_payments_json=json.dumps(raw_payments_by_person)`. - Note: rows where `Person` is empty are skipped on purpose — those have no member to attach to and are already shown by the dashboard's `Unmatched` block. ### 2. Templates — add a collapsible raw section to the modal In [`templates/adults.html`](templates/adults.html) and [`templates/juniors.html`](templates/juniors.html), make the same structural and JS changes (the modal markup is mirrored in both files — `adults.html:677-682` and `juniors.html:658-663`). - Inject the new dataset alongside the existing `memberData`: ```html const rawPaymentsByPerson = {{ raw_payments_json| safe }}; ``` (next to [`adults.html:696`](templates/adults.html#L696)). - Add a new section directly **after** the Payment History block: ```html ``` Add a small CSS rule for `.raw-toggle` (muted color, smaller font, `margin-left: 8px`) — a few lines next to the existing `.modal-section-title` style. Don't restyle the whole modal. - In `showMemberDetails(name)`: - Reset the toggle to `[show]` and the `#modalRawList` to `display: none` on every open (so the state doesn't leak between members). - Populate `#modalRawList` from `rawPaymentsByPerson[name] || []`. For each row render: `Date | Purpose` on the meta line, `Amount CZK` (with `Inferred: X CZK` annotation when `inferred_amount` differs from `amount`), `Sender`, `Person` (full string — useful when split between multiple people), `Message`, and a small footer with `Bank ID` and a `[manual fix]` marker if `manual_fix` is truthy. Reuse the existing `tx-item` / `tx-meta` / `tx-main` / `tx-msg` styles to match the rest of the modal. - When the list is empty, render `
No raw payments tied to this member.
` (same idiom used at [`adults.html:813`](templates/adults.html#L813)). - Add the toggle handler near `closeModal`: ```js function toggleRawPayments(ev) { ev.preventDefault(); const list = document.getElementById('modalRawList'); const link = document.getElementById('rawPaymentsToggle'); const hidden = list.style.display === 'none'; list.style.display = hidden ? 'block' : 'none'; link.textContent = hidden ? '[hide]' : '[show]'; } ``` ### 3. Why not extend `reconcile()` instead `reconcile()` already collapses each row into per-month allocated shares and drops `purpose`, `inferred_amount`, `bank_id`, `manual_fix`, and the gross `amount` ([trace](scripts/match_payments.py:436-469)). Carrying the raw `tx` through `reconcile()` would inflate the contract for every consumer when only the modal needs it. Grouping the already-fetched `transactions` list at the route level is one extra dict per request and reuses the cached payments data — no new sheet reads. ## Critical files - [app.py](app.py) — add `group_payments_by_person()` helper; call it in `adults_view()`, `juniors_view()`, and `payments()`; pass `raw_payments_json` to the two dashboard templates. - [templates/adults.html](templates/adults.html) — modal section + JS + tiny CSS for the toggle link. - [templates/juniors.html](templates/juniors.html) — same changes as adults.html. ## Verification 1. `make web-debug` and open `http://localhost:5001/adults`. 2. Pick a member known to have multiple payments (use the existing `/payments` page as a cross-reference). 3. Click `[i]` → modal opens, raw list is hidden, link shows `[show]`. Click the link → list appears with the raw rows; click again → hides, link returns to `[show]`. 4. Switch to another member via keyboard (ArrowDown) — the toggle resets to hidden and the list updates to the new member's rows (no leaking). 5. Compare the raw rows in the modal against the `/payments` page grouping for the same person — same set of rows, same `Date`/`Amount`/`Message`. 6. Pick a row with a non-conformant message (e.g. one where `Person` was inferred to multiple people) — confirm `Person` shows the full comma-separated string and `Inferred Amount` is visible when it differs from `Amount`. 7. Repeat the click-through on `/juniors` to confirm parity. 8. `make test` — no backend behavior change is expected, but run to catch template/route smoke breakage.