Files
fuj-management/go/internal/web/html_handler.go
Jan Novak 69af4c1e3b
All checks were successful
Deploy to K8s / deploy (push) Successful in 24s
feat: multi-account Fio sync + switch QR default to 2502035405/2010
Add second Fio account (CZ0820100000002502035405 / 2502035405/2010).
Both accounts are fetched on every sync run and combined before dedup,
so the payments sheet accumulates transactions from either account.
QR codes now default to the new account.

Go:
- config.go: hardcoded Accounts/LoadedAccount slice replaces scalar
  BankAccount + FioAPIToken; Config.BankAccount renamed QRAccount;
  per-account tokens via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD
- banksync.SyncToSheets: accepts []fio.Client, loops to combine txns
- cmd/fuj/main.go: buildFioClients helper; both sync call sites updated
- html_handler + build_adults/juniors: use Config.QRAccount
- New TestSyncToSheets_MultiAccount covers cross-account dedup

Python:
- config.py: ACCOUNTS list + LOADED_ACCOUNTS (tokens from env)
- fio_utils.py: fetch_transactions_for (per-account) +
  fetch_transactions_all (loops all accounts)
- sync_fio_to_sheets.py: uses fetch_transactions_all

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 21:42:47 +02:00

144 lines
3.9 KiB
Go

package web
import (
"bytes"
"fmt"
"fuj-management/go/internal/web/api"
"net/http"
"runtime/debug"
)
// HTMLHandler serves the Go-native HTML frontend.
type HTMLHandler struct {
renderer *Renderer
build BuildInfo
apiHandler *api.Handler
actions ActionHandlers
}
// NewHTMLHandler constructs an HTMLHandler.
func NewHTMLHandler(r *Renderer, b BuildInfo, ah *api.Handler, actions ActionHandlers) *HTMLHandler {
return &HTMLHandler{renderer: r, build: b, apiHandler: ah, actions: actions}
}
func (h *HTMLHandler) ServeAdults(w http.ResponseWriter, r *http.Request) {
data, err := h.apiHandler.AssembleAdults(r.Context())
if err != nil {
h.renderer.Render(w, "adults", AdultsPageData{
PageData: PageData{Active: "adults", Build: h.build},
Error: err.Error(),
})
return
}
h.renderer.Render(w, "adults", AdultsPageData{
PageData: PageData{Active: "adults", Build: h.build},
Data: data,
})
}
func (h *HTMLHandler) ServeJuniors(w http.ResponseWriter, r *http.Request) {
data, err := h.apiHandler.AssembleJuniors(r.Context())
if err != nil {
h.renderer.Render(w, "juniors", JuniorsPageData{
PageData: PageData{Active: "juniors", Build: h.build},
Error: err.Error(),
})
return
}
h.renderer.Render(w, "juniors", JuniorsPageData{
PageData: PageData{Active: "juniors", Build: h.build},
Data: data,
})
}
func (h *HTMLHandler) ServePayments(w http.ResponseWriter, r *http.Request) {
data, err := h.apiHandler.AssemblePayments(r.Context())
if err != nil {
h.renderer.Render(w, "payments", PaymentsPageData{
PageData: PageData{Active: "payments", Build: h.build},
Error: err.Error(),
})
return
}
h.renderer.Render(w, "payments", PaymentsPageData{
PageData: PageData{Active: "payments", Build: h.build},
Data: data,
})
}
// ServeSync handles GET /sync-bank: runs sync+infer+flush then renders the result.
func (h *HTMLHandler) ServeSync(w http.ResponseWriter, r *http.Request) {
pd := PageData{Active: "sync", Build: h.build}
if h.actions.BankSync == nil {
h.renderer.Render(w, "sync", SyncPageData{
PageData: pd,
Output: "Bank sync is not configured.",
Success: false,
})
return
}
var buf bytes.Buffer
success := true
if err := h.actions.BankSync(r.Context(), &buf); err != nil {
fmt.Fprintf(&buf, "\nError: %s\n\nStack trace:\n%s", err.Error(), debug.Stack())
success = false
}
fmt.Fprintln(&buf, "\n=== Flush Cache ===")
n, err := h.apiHandler.FlushCache()
if err != nil {
fmt.Fprintf(&buf, "flush error: %s\n", err.Error())
success = false
} else {
fmt.Fprintf(&buf, "%d cache file(s) deleted.\n", n)
}
h.renderer.Render(w, "sync", SyncPageData{
PageData: pd,
Output: buf.String(),
Success: success,
})
}
// ServeFlushCacheGET handles GET /flush-cache: renders the confirmation form.
func (h *HTMLHandler) ServeFlushCacheGET(w http.ResponseWriter, r *http.Request) {
h.renderer.Render(w, "flush_cache", FlushPageData{
PageData: PageData{Active: "flush", Build: h.build},
})
}
// ServeFlushCachePOST handles POST /flush-cache: flushes and re-renders with count.
func (h *HTMLHandler) ServeFlushCachePOST(w http.ResponseWriter, r *http.Request) {
n, _ := h.apiHandler.FlushCache()
h.renderer.Render(w, "flush_cache", FlushPageData{
PageData: PageData{Active: "flush", Build: h.build},
Flushed: true,
Deleted: n,
})
}
// ServeQR handles GET /qr: generates and returns a Czech QR Platba PNG.
func (h *HTMLHandler) ServeQR(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
account := q.Get("account")
amount := q.Get("amount")
message := q.Get("message")
if account == "" {
account = h.apiHandler.Config.QRAccount
}
if amount == "" {
amount = "0"
}
payload := BuildSPD(account, amount, message, h.apiHandler.Config.QRAccount)
png, err := RenderQRCode(payload)
if err != nil {
http.Error(w, "qr encode: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write(png)
}