Files
fuj-management/docs/plans/2026-05-08-1220-go-m6-4-payments-page.md
Jan Novak cb8a09b571
All checks were successful
Deploy to K8s / deploy (push) Successful in 6s
feat(go): M6.4 — Go-native /payments page (grouped-by-person ledger)
- Extract AssemblePayments(ctx) from ServePayments in api/handler.go,
  mirroring the AssembleAdults/AssembleJuniors pattern
- Add PaymentsPageData view-model wrapper in render.go
- Rewire html_handler.go ServePayments to call AssemblePayments and
  render with PaymentsPageData
- Replace payments.tmpl placeholder with real grouped-by-person ledger:
  alphabetical member blocks, txn-table (Date/Amount/Purpose/Message),
  newest-first rows, Unmatched/Unknown bucket
- Append ledger CSS classes to app.css (.ledger-container, .member-block,
  .txn-table, .txn-date/amount/purpose/message, tr:hover)
- Add TestPaymentsPage markup test

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 12:29:32 +02:00

8.9 KiB

M6.4 — Go-native /payments page (grouped-by-person ledger)

Plan-mode note: per project convention this plan should live at docs/plans/2026-05-08-1220-go-m6-4-payments-page.md. The first execution step after approval is to copy this file there.

Context

The Go port of the Flask app is at milestone M6.4: replace the placeholder /payments template (go/internal/web/templates/payments.tmpl) with a feature-equivalent grouped-by-person ledger view, matching the Python templates/payments.html page.

Compared to M6.2 (/adults) and M6.3 (/juniors), this page is structurally much simpler:

  • No filters (no name input, no month-range selects).
  • No totals / credits / debts / unmatched-as-extra-section.
  • No JS (no filters.js, no modal — modal is M6.5 territory and Python's payments page itself has none).
  • No QR / Pay buttons.
  • Single grouping dimension: by person, sorted alphabetically, with a synthetic "Unmatched / Unknown" bucket appended.
  • Each person's transactions sorted newest-first.
  • Columns: Date, Amount (right-aligned, green), Purpose, Bank Message.

The JSON API side is already done (/api/payments at go/internal/web/api/handler.go:78-86), returning a PaymentsResponse{ GroupedPayments, SortedPeople, AttendanceURL, PaymentsURL }. The HTML side just needs to consume it.

Approach

Mirror the M6.2/M6.3 pattern: extract an AssembleX(ctx) method, add a typed PageData wrapper, render a real template, lift the page-specific CSS into the shared stylesheet.

1. Extract AssemblePayments(ctx) (PaymentsResponse, error)

In go/internal/web/api/handler.go, refactor ServePayments (currently inlines the load+build at lines 78-86) to mirror AssembleAdults / AssembleJuniors:

func (h *Handler) ServePayments(w http.ResponseWriter, r *http.Request) {
    resp, err := h.AssemblePayments(r.Context())
    if err != nil { h.writeError(w, r, err); return }
    writeJSON(w, resp)
}

func (h *Handler) AssemblePayments(ctx context.Context) (PaymentsResponse, error) {
    txns, err := h.Sources.LoadTransactions(ctx)
    if err != nil { return PaymentsResponse{}, fmt.Errorf("load transactions: %w", err) }
    return buildPaymentsResponse(txns, h.allMemberNames(ctx)), nil
}

Pure refactor — make parity should report zero diffs on /api/payments.

2. Add PaymentsPageData wrapper

In go/internal/web/render.go, alongside AdultsPageData / JuniorsPageData:

type PaymentsPageData struct {
    PageData
    Data  api.PaymentsResponse
    Error string
}

No changes needed to pageNames"payments" is already registered.

3. Rewire HTML handler

Replace go/internal/web/html_handler.go:50-52:

func (h *HTMLHandler) ServePayments(w http.ResponseWriter, r *http.Request) {
    data := PaymentsPageData{ PageData: PageData{Active: "payments", Build: h.build} }
    resp, err := h.apiHandler.AssemblePayments(r.Context())
    if err != nil { data.Error = err.Error() } else { data.Data = resp }
    h.renderer.Render(w, "payments", data)
}

4. Replace template stub

Rewrite go/internal/web/templates/payments.tmpl (currently a 5-line placeholder). Structure:

{{define "title"}}Payments Ledger{{end}}
{{define "content"}}
<h1>Payments Ledger</h1>
<p class="description">
  All bank transactions from the
  <a href="{{.Data.PaymentsURL}}" target="_blank">payments sheet</a>,
  grouped by member. Names are matched against the
  <a href="{{.Data.AttendanceURL}}" target="_blank">attendance sheet</a>.
</p>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<div class="ledger-container">
{{range $person := .Data.SortedPeople}}
  <div class="member-block">
    <h2>{{$person}}</h2>
    <table class="txn-table">
      <thead><tr>
        <th class="txn-date">Date</th>
        <th class="txn-amount">Amount</th>
        <th class="txn-purpose">Purpose</th>
        <th class="txn-message">Bank Message</th>
      </tr></thead>
      <tbody>
      {{range $tx := index $.Data.GroupedPayments $person}}
        <tr>
          <td class="txn-date">{{$tx.Date}}</td>
          <td class="txn-amount">{{printf "%.0f" $tx.Amount}} CZK</td>
          <td class="txn-purpose">{{$tx.Purpose}}</td>
          <td class="txn-message">{{$tx.Message}}</td>
        </tr>
      {{end}}
      </tbody>
    </table>
  </div>
{{end}}
</div>
{{end}}

Notes:

  • printf "%.0f" matches the visual style of integer CZK amounts (Python prints the float as-is, but bank-row amounts are always whole CZK). If the user prefers exact Python parity (750.0 CZK), drop the printf and use {{$tx.Amount}}.
  • Iterating a map requires using .Data.SortedPeople to drive order then index $.Data.GroupedPayments $person — Go templates don't preserve map insertion order.

5. Add CSS classes to app.css

Append the page-specific rules from templates/payments.html:111-164 to go/internal/web/static/css/app.css: .ledger-container, .member-block, .txn-table, .txn-table th/td, .txn-date, .txn-amount, .txn-purpose, .txn-message, tr:hover. The terminal-green palette already matches.

6. Unit test

Add a markup-level test to go/internal/web/html_handler_test.go (the file created in M6.2/M6.3) following the same pattern: stub Sources returning a fixed transaction list, assert response is HTML 200, assert key strings appear (person names as <h2>, "Payments Ledger" <h1>, expected amount, "Unmatched / Unknown" bucket when an unattributed tx exists).

Critical files

Action Path
edit go/internal/web/api/handler.go — extract AssemblePayments
edit go/internal/web/render.go — add PaymentsPageData
edit go/internal/web/html_handler.go — rewire ServePayments
rewrite go/internal/web/templates/payments.tmpl — real template
edit go/internal/web/static/css/app.css — append ledger CSS
edit go/internal/web/html_handler_test.go — payments markup test

No changes to server.go, assets.go, Sources interface, buildPaymentsResponse, or any domain code — these were completed in M5 / M6.1.

Reused existing helpers

Verification

End-to-end:

  1. cd go && make go-build go-test go-lint — all green.
  2. make parity — confirm the AssemblePayments extraction is byte-equal on /api/payments.
  3. make web-go & then:
    • curl -i localhost:8080/payments | head -30 — HTTP 200, text/html.
    • Browser load http://localhost:8080/payments:
      • "Payments Ledger" heading + description with two clickable sheet links.
      • Alphabetical member blocks, each with a table.
      • Amounts right-aligned, green, "CZK" suffix.
      • Newest-first ordering inside each block.
      • "Unmatched / Unknown" bucket sorts in alphabetical position (capital U → near end).
      • Hover on a row darkens the background.
      • Nav highlights [Payments Ledger] as the active link.
    • Cross-check with Python: make web-py &http://localhost:5001/payments should look visually identical except for nav styling.
  4. make web-py & make web-go & then tea pr create once committed (per project branch-per-feature workflow).

Branching

Create feat/go-m6-4-payments-page off main, push, open MR via tea. Follow project conventions: Co-Authored-By trailer, CHANGELOG entry once user confirms it works, tick M6.4 in docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md:116 with the merge SHA after the user merges in Gitea.

Out of scope (deferred)

  • Modal / [i] info button — M6.5 (not present in Python /payments either).
  • Period selector/payments has none in Python; not adding one.
  • embed.FS deploy verification — M6.7.
  • Shared partial extraction — payments structurally diverges from adults/juniors enough that there's no obvious common partial to lift; defer.