All checks were successful
Deploy to K8s / deploy (push) Successful in 6s
- 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>
153 lines
4.4 KiB
Go
153 lines
4.4 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"fuj-management/go/internal/config"
|
|
"fuj-management/go/internal/services/membership"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
domreconcile "fuj-management/go/internal/domain/reconcile"
|
|
)
|
|
|
|
// Handler holds the shared dependencies for all /api/* routes.
|
|
type Handler struct {
|
|
BuildVersion string
|
|
BuildCommit string
|
|
BuildDate string
|
|
Sources membership.Sources
|
|
Config config.Config
|
|
Logger *slog.Logger
|
|
}
|
|
|
|
// ServeVersion handles GET /api/version.
|
|
func (h *Handler) ServeVersion(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, VersionResponse{
|
|
Tag: h.BuildVersion,
|
|
Commit: h.BuildCommit,
|
|
BuildDate: h.BuildDate,
|
|
})
|
|
}
|
|
|
|
// ServeAdults handles GET /api/adults.
|
|
func (h *Handler) ServeAdults(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := h.AssembleAdults(r.Context())
|
|
if err != nil {
|
|
h.writeError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
// AssembleAdults loads all data and builds the adults view model.
|
|
// Shared between the JSON API route and the HTML handler.
|
|
func (h *Handler) AssembleAdults(ctx context.Context) (AdultsResponse, error) {
|
|
members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, true)
|
|
if err != nil {
|
|
return AdultsResponse{}, err
|
|
}
|
|
result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year())
|
|
return buildAdultsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")), nil
|
|
}
|
|
|
|
// ServeJuniors handles GET /api/juniors.
|
|
func (h *Handler) ServeJuniors(w http.ResponseWriter, r *http.Request) {
|
|
resp, err := h.AssembleJuniors(r.Context())
|
|
if err != nil {
|
|
h.writeError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, resp)
|
|
}
|
|
|
|
// AssembleJuniors loads all data and builds the juniors view model.
|
|
// Shared between the JSON API route and the HTML handler.
|
|
func (h *Handler) AssembleJuniors(ctx context.Context) (JuniorsResponse, error) {
|
|
members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, false)
|
|
if err != nil {
|
|
return JuniorsResponse{}, err
|
|
}
|
|
result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year())
|
|
return buildJuniorsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")), nil
|
|
}
|
|
|
|
// ServePayments handles GET /api/payments.
|
|
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)
|
|
}
|
|
|
|
// AssemblePayments loads transactions and builds the payments view model.
|
|
// Shared between the JSON API route and the HTML handler.
|
|
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
|
|
}
|
|
|
|
func (h *Handler) loadAll(ctx context.Context, adults bool) (
|
|
members []domreconcile.Member,
|
|
sortedMonths []string,
|
|
txns []domreconcile.Transaction,
|
|
exceptions map[domreconcile.ExceptionKey]domreconcile.Exception,
|
|
err error,
|
|
) {
|
|
if adults {
|
|
members, sortedMonths, err = h.Sources.LoadAdults(ctx)
|
|
} else {
|
|
members, sortedMonths, err = h.Sources.LoadJuniors(ctx)
|
|
}
|
|
if err != nil {
|
|
err = fmt.Errorf("load members: %w", err)
|
|
return
|
|
}
|
|
txns, err = h.Sources.LoadTransactions(ctx)
|
|
if err != nil {
|
|
err = fmt.Errorf("load transactions: %w", err)
|
|
return
|
|
}
|
|
exceptions, err = h.Sources.LoadExceptions(ctx)
|
|
if err != nil {
|
|
err = fmt.Errorf("load exceptions: %w", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (h *Handler) allMemberNames(ctx context.Context) []string {
|
|
var names []string
|
|
if adults, _, err := h.Sources.LoadAdults(ctx); err == nil {
|
|
for _, m := range adults {
|
|
names = append(names, m.Name)
|
|
}
|
|
}
|
|
if juniors, _, err := h.Sources.LoadJuniors(ctx); err == nil {
|
|
for _, m := range juniors {
|
|
names = append(names, m.Name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
func (h *Handler) writeError(w http.ResponseWriter, r *http.Request, err error) {
|
|
if h.Logger != nil {
|
|
h.Logger.Error("api error", "path", r.URL.Path, "err", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, v any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|