All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
- Extract AssembleAdults(ctx) from ServeAdults so HTML and JSON API share one reconcile path. - HTMLHandler gains *api.Handler; ServeAdults loads real data and renders adults.tmpl. - AdultsPageData view model + qrHref/qrHrefAll funcMap (URL-encode /qr params, YYYY-MM→MM/YYYY). - adults.tmpl: full reconcile table, per-cell status classes + cell-unpaid-current, Pay button hrefs, totals row, credits/debts/unmatched sections, filter controls, sheet links. - static/js/filters.js: NFD-normalize name filter + month-range column hiding; future months hidden by default. - TestAdultsPage asserts member name and cell text against fixture data. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
135 lines
3.8 KiB
Go
135 lines
3.8 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) {
|
|
ctx := r.Context()
|
|
members, sortedMonths, txns, exceptions, err := h.loadAll(ctx, false)
|
|
if err != nil {
|
|
h.writeError(w, r, err)
|
|
return
|
|
}
|
|
result := domreconcile.Reconcile(members, sortedMonths, txns, exceptions, time.Now().Year())
|
|
writeJSON(w, buildJuniorsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")))
|
|
}
|
|
|
|
// ServePayments handles GET /api/payments.
|
|
func (h *Handler) ServePayments(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
txns, err := h.Sources.LoadTransactions(ctx)
|
|
if err != nil {
|
|
h.writeError(w, r, fmt.Errorf("load transactions: %w", err))
|
|
return
|
|
}
|
|
writeJSON(w, buildPaymentsResponse(txns, h.allMemberNames(ctx)))
|
|
}
|
|
|
|
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)
|
|
}
|