Compare commits
6 Commits
feat/go-m6
...
505d635c66
| Author | SHA1 | Date | |
|---|---|---|---|
| 505d635c66 | |||
| 9f0e4b0ac3 | |||
| 2cbd98df1a | |||
| e618e906ef | |||
| 96e574e6c7 | |||
| 6a8aa37198 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-05-08 11:25 CEST — feat(go): M6.3 — Go-native /juniors page
|
||||
|
||||
- Extracted `AssembleJuniors(ctx)` from the JSON handler so HTML and JSON share one view-model path (mirrors `AssembleAdults`).
|
||||
- Added `JuniorsPageData` to `render.go`; wired `HTMLHandler.ServeJuniors` to call `AssembleJuniors` and render the new template.
|
||||
- Replaced the 4-line "Coming in M6.3" placeholder in `juniors.tmpl` with the full template: member table, name/month-range filters, totals row, Credits + Debts sections, Pay / Pay All links to `/qr`. No Unmatched section (parity with Python).
|
||||
- J/A attendance breakdown and `"?"` sentinel rendered via `MonthCell.Text` produced by `buildJuniorMemberRow` — no extra template logic.
|
||||
- `make parity` reports 3/3 routes OK; all Go tests pass.
|
||||
|
||||
## 2026-05-08 11:11 CEST — fix: period selector showed only Dec 2025+ on adults
|
||||
|
||||
- Restored the `2025-09 → 2025-10` adult merge in `scripts/attendance.py` and `go/internal/services/membership/sources.go` (commented-out by `1257f0d`); the `2025-12 → 2026-01` mapping stays disabled per product decision (Dec and Jan are billed separately for adults).
|
||||
- Dropped the `defaultFrom = maxMonthIdx − 4` JS auto-default in `templates/adults.html` and `templates/juniors.html` (introduced by `7774301`); the From-selector now starts at the oldest available month so all non-future periods render on first load. Future-month removal is preserved.
|
||||
- `go/internal/services/membership/sources_test.go`: `TestLoadAdults` / `TestLoadAdults_Fee` now assert that Sep dates land in the merged `2025-10` bucket.
|
||||
- **Independent of these code changes**: the live adults attendance Google Sheet header had been pruned to start at `02.12.2025` (Sep/Oct/Nov 2025 columns deleted); restoring those columns from Sheets version history is required to actually see those periods on the dashboard.
|
||||
|
||||
## 2026-05-08 10:15 CEST — fix(go): adults template — use lifted CSS classes for visual parity
|
||||
|
||||
- Use existing `.balance-pos` / `.balance-neg` (drop invented `balance-cell` / `balance-negative`); Pay-All button now lives inside the balance cell with `position: relative` (matches Python; no separate trailing column).
|
||||
|
||||
@@ -4,7 +4,7 @@ Companion to [2026-05-03-2349-go-backend-rewrite.md](2026-05-03-2349-go-backend-
|
||||
|
||||
**Current milestone:** M6 — Go-native HTML frontend
|
||||
**Started:** 2026-05-04
|
||||
**Last updated:** 2026-05-08 (M6.2)
|
||||
**Last updated:** 2026-05-08 (M6.2 merged)
|
||||
|
||||
## How to use
|
||||
|
||||
@@ -111,7 +111,7 @@ Goal: byte-equal JSON between Python and Go for every route. This is the parity
|
||||
Goal: feature-equivalent UX on the Go side, designed cleanly. Not a Jinja port.
|
||||
|
||||
- [x] **M6.1** Template skeleton: base layout, nav (Adults/Juniors/Payments/Sync/Flush), terminal-green-on-black theme; `embed.FS` for `templates/` + `static/` — `78e5059`
|
||||
- [x] **M6.2** `/adults` page: table, name filter input, month range filter, totals row, credits/debts/unmatched sections, Pay buttons that link to `/qr`
|
||||
- [x] **M6.2** `/adults` page: table, name filter input, month range filter, totals row, credits/debts/unmatched sections, Pay buttons that link to `/qr` — `c85748b`
|
||||
- [ ] **M6.3** `/juniors` page: same structure + per-month J/A attendance breakdown + `"?"` sentinel rendering
|
||||
- [ ] **M6.4** `/payments` page: grouped-by-person ledger view
|
||||
- [ ] **M6.5** Modal JS module (`static/js/member-detail.js`): fetches `/api/adults` (or juniors), renders status/exceptions/transactions on row click; keyboard nav (Esc, ↑/↓)
|
||||
|
||||
142
docs/plans/2026-05-08-1052-period-selector-missing-old-months.md
Normal file
142
docs/plans/2026-05-08-1052-period-selector-missing-old-months.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Period selector missing older months — diagnosis
|
||||
|
||||
## Context
|
||||
|
||||
User reports the "From / To" period selector on the **adults** dashboard now
|
||||
shows **Dec 2025** as the oldest available period. The older production
|
||||
deployment shows Sep+Oct 2025, Nov 2025, Dec 2025+Jan 2026 (merged labels) —
|
||||
i.e. data going back to September 2025. The user wants to know what went
|
||||
sideways. Confirmed: the dropdown options on both Python and Go genuinely
|
||||
start at Dec 2025, not just the default selection — Sep/Oct/Nov 2025 are not
|
||||
in the list at all.
|
||||
|
||||
## Root cause — the live adults sheet header is missing those columns
|
||||
|
||||
The fresh cache files at `tmp/go/attendance_regular_cache.json` (raw rows from
|
||||
Google Sheets, modifiedTime `2026-05-06T22:30:02Z`, cached `2026-05-08T00:26`)
|
||||
contain the actual header row for the adults tab (gid=0):
|
||||
|
||||
```text
|
||||
['FUJ tréninky úterý 20:30-22:00', '', '', '02.12.2025', '09.12.2025',
|
||||
'16.12.2025', '06.01.2026', '13.01.2026', '20.01.2026', '27.01.2026',
|
||||
'03.02.2026', '10.02.2026', '17.02.2026', '24.02.2026', '03.03.2026',
|
||||
'10.03.2026', '17.03.2026', '23.03.2026', '31.03.2026', '13.04.2026',
|
||||
'20.04.2026', '27.04.2026', '04.05.2026', '', '', '']
|
||||
```
|
||||
|
||||
**The first date column in the live adults sheet is `02.12.2025` (Dec 2 2025).**
|
||||
There are no September, October, or November 2025 columns in the header at
|
||||
all. Both backends parse this faithfully (no slicing, no cutoff anywhere) and
|
||||
correctly produce `sortedMonths = ["2025-12", "2026-01", …, "2026-05"]`.
|
||||
|
||||
The juniors sheet (different tab, `JUNIOR_SHEET_GID`) is **fine** — its header
|
||||
still contains `['', 'tier', '', '15.09.2025', '13.10.2025', '20.10.2025',
|
||||
'03.11.2025', '24.11.2025', '10.11.2025', '17.11.2025', '01.12.2025', …]`. So
|
||||
the juniors page still shows Sep+Oct / Nov / Dec+Jan correctly.
|
||||
|
||||
So this is a **data issue in the adults attendance Google Sheet**: at some
|
||||
point between when production's cache was last warmed (showing Sep–Nov) and
|
||||
2026-05-06, somebody (or some action) removed the columns for September,
|
||||
October, and November 2025 from the adults tab header.
|
||||
|
||||
The code is doing exactly what it should. There is no parser regression.
|
||||
|
||||
## What to do
|
||||
|
||||
### 1. Restore the missing date columns in the adults attendance sheet
|
||||
|
||||
The fix lives in Google Sheets, not in the codebase. Options, in order of
|
||||
preference:
|
||||
|
||||
- **(a) Use Sheets version history.** File → Version history → See version
|
||||
history; find a version from before the columns were dropped (anything
|
||||
before about Mar 2026 should still have them). Copy the Sep/Oct/Nov 2025
|
||||
date column headers and the `TRUE/FALSE` cells underneath them back into
|
||||
the current sheet. Only restore the 11 missing date columns; do not
|
||||
full-revert (you'd lose every change since then).
|
||||
- **(b) Pull from the production server's cache.** The production deployment
|
||||
evidently still has the older cache, since its dashboard renders those
|
||||
months. SSH there, copy `tmp/attendance_regular_cache.json`, and you can
|
||||
reconstruct the per-member Sep/Oct/Nov attendance counts from the
|
||||
`data[*][2]` map (keys `"2025-09"`, `"2025-10"`, `"2025-11"`). Re-enter
|
||||
those into the sheet manually as date columns + `TRUE` cells — tedious but
|
||||
deterministic.
|
||||
- **(c) Accept the loss.** If the older columns aren't recoverable, the
|
||||
dashboard correctly reflects what the sheet contains; nothing more to do.
|
||||
|
||||
Which to pick depends on whether those months still need to be billed /
|
||||
reconciled.
|
||||
|
||||
### 2. Restore `ADULT_MERGED_MONTHS` (user confirmed this was unintentional)
|
||||
|
||||
Independent of the sheet issue: commit `1257f0d` (Mar 9 2026) commented out
|
||||
the adult merge mappings. Once the Sep/Oct/Nov columns are back in the sheet,
|
||||
the dashboard would still show them as separate periods instead of the
|
||||
production-style "Sep+Oct 2025" and "Dec 2025+Jan 2026" merged labels.
|
||||
|
||||
User confirmed this was unintentional. Two files to update:
|
||||
|
||||
- [scripts/attendance.py:32-35](scripts/attendance.py#L32-L35) — uncomment
|
||||
the two mappings:
|
||||
|
||||
```python
|
||||
ADULT_MERGED_MONTHS = {
|
||||
"2025-12": "2026-01", # keys are merged into values
|
||||
"2025-09": "2025-10",
|
||||
}
|
||||
```
|
||||
|
||||
- [go/internal/services/membership/sources.go:30](go/internal/services/membership/sources.go#L30) — mirror the same:
|
||||
|
||||
```go
|
||||
var AdultMergedMonths = map[string]string{
|
||||
"2025-12": "2026-01",
|
||||
"2025-09": "2025-10",
|
||||
}
|
||||
```
|
||||
|
||||
After this change, hit `POST /flush-cache` on each backend so the in-process
|
||||
post-processed adults cache is rebuilt with the new mapping.
|
||||
|
||||
### 3. (Optional, separate) Fix the JS auto-default that hides older months
|
||||
|
||||
This is **not** the cause of the user's current symptom (that's the sheet
|
||||
issue), but it will become a UX issue once Sep/Oct/Nov columns are restored:
|
||||
the Python frontend's `defaultFrom = Math.max(0, maxMonthIdx - 4)` will still
|
||||
default the From-selector to ~5 months before the latest column on every page
|
||||
load, hiding restored older months until the user manually picks them.
|
||||
|
||||
- [templates/adults.html:1047](templates/adults.html#L1047) — `var defaultFrom = Math.max(0, maxMonthIdx - 4);`
|
||||
- [templates/juniors.html:1028](templates/juniors.html#L1028) — same line.
|
||||
|
||||
Drop those four lines (`defaultFrom`, `fromSelect.value = defaultFrom`,
|
||||
`toSelect.value = maxMonthIdx`, `applyMonthFilter()`) so the page loads with
|
||||
all non-future months visible — matching the Go side, which only calls
|
||||
`hideFutureMonths()` and leaves From at its first option.
|
||||
|
||||
Recommend bundling this with step 2 since they touch related UI.
|
||||
|
||||
## Verification
|
||||
|
||||
1. **After step 1** — `POST /flush-cache` on Python and Go backends; reload
|
||||
`/adults` on each. Confirm the dropdown now lists Sep/Oct/Nov 2025.
|
||||
2. **After step 2** — reload `/adults`. Confirm the dropdown shows
|
||||
"Sep+Oct 2025" as a single period and "Dec 2025+Jan 2026" as a single
|
||||
period. (Still requires the sheet columns to exist.)
|
||||
3. **After step 3** — reload `/adults` and `/juniors` on Python. Confirm the
|
||||
table renders all non-future months on first load (Sep 2025 through the
|
||||
current month) instead of starting at Dec 2025.
|
||||
4. **Parity check** — `make parity` should report zero diffs between Python
|
||||
and Go on `/api/adults` and `/api/juniors`.
|
||||
|
||||
## Critical files referenced
|
||||
|
||||
- `tmp/go/attendance_regular_cache.json` — current adults sheet rows
|
||||
(evidence: header starts at `02.12.2025`).
|
||||
- `tmp/go/attendance_juniors_cache.json` — current juniors sheet rows
|
||||
(header still has `15.09.2025`).
|
||||
- [scripts/attendance.py](scripts/attendance.py) — `ADULT_MERGED_MONTHS`
|
||||
empty after `1257f0d`; `parse_dates` / `group_by_month` faithful.
|
||||
- [go/internal/services/membership/sources.go](go/internal/services/membership/sources.go) — Go counterpart, same shape.
|
||||
- [templates/adults.html](templates/adults.html), [templates/juniors.html](templates/juniors.html) — JS onload `defaultFrom = -4` issue (step 3).
|
||||
- [go/internal/web/static/js/filters.js](go/internal/web/static/js/filters.js) — Go filter UI (already correct, no changes).
|
||||
67
docs/plans/2026-05-08-1123-go-m6-3-juniors-page.md
Normal file
67
docs/plans/2026-05-08-1123-go-m6-3-juniors-page.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# M6.3 — Go-native `/juniors` page
|
||||
|
||||
## Context
|
||||
|
||||
The Go rewrite is in milestone M6 (Go-native HTML frontend). M6.2 shipped the `/adults` page in commit `c85748b`; the [progress tracker](../../srv/personal/fuj-management/docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md) line 115 names M6.3 as:
|
||||
|
||||
> `/juniors` page: same structure + per-month J/A attendance breakdown + `"?"` sentinel rendering
|
||||
|
||||
The Go side already has `buildJuniorsResponse` (M5.2) that produces the full JSON view model, and the JuniorsResponse uses the same `MemberRow`/`MonthCell`/`TotalCell` types as `AdultsResponse`. Critically, `buildJuniorMemberRow` ([go/internal/web/api/build_juniors.go:142](../../srv/personal/fuj-management/go/internal/web/api/build_juniors.go#L142)) already bakes the J/A breakdown (e.g. `"3/500 CZK (4:2J,2A)"`) and the `"?"` sentinel into `MonthCell.Text` — no template-level junior-specific logic is required for the M6.3 table.
|
||||
|
||||
Today `juniors.tmpl` is a 4-line "Coming in M6.3" placeholder and `HTMLHandler.ServeJuniors` ([html_handler.go:35](../../srv/personal/fuj-management/go/internal/web/html_handler.go#L35)) just renders that placeholder. Goal of M6.3: produce a working `/juniors` page that mirrors the structure of `/adults` and reaches feature parity with the Python `/juniors` route.
|
||||
|
||||
## Approach
|
||||
|
||||
Mirror the M6.2 pattern exactly:
|
||||
|
||||
1. **Extract `AssembleJuniors(ctx)`** from the JSON handler so HTML + JSON share one view-model assembly path (the parity contract).
|
||||
2. **Add `JuniorsPageData`** wrapper type next to `AdultsPageData` in `render.go`.
|
||||
3. **Replace `juniors.tmpl`** with an adults-shaped layout. Because `JuniorsResponse` shares the same field names used by `adults.tmpl` (`Months`, `RawMonths`, `Results`, `Totals`, `Credits`, `Debts`, `AttendanceURL`, `PaymentsURL`, `BankAccount`, `CurrentMonth`), the template body is essentially copy/paste with one parity tweak (see "Decisions" below).
|
||||
4. **Wire `HTMLHandler.ServeJuniors`** to call `AssembleJuniors` and render the new template, parallel to `ServeAdults`.
|
||||
|
||||
No new CSS, no new JS — `filters.js` and the `cell-*` classes (lifted in M6.1) already cover juniors.
|
||||
|
||||
## Files to modify
|
||||
|
||||
| File | Change |
|
||||
| --- | --- |
|
||||
| [go/internal/web/api/handler.go](../../srv/personal/fuj-management/go/internal/web/api/handler.go) | Add `AssembleJuniors(ctx) (JuniorsResponse, error)`; refactor existing `ServeJuniors` to call it (mirrors `AssembleAdults` at line 47). |
|
||||
| [go/internal/web/render.go](../../srv/personal/fuj-management/go/internal/web/render.go) | Add `JuniorsPageData` struct (PageData + `Data api.JuniorsResponse` + `Error string`). |
|
||||
| [go/internal/web/html_handler.go](../../srv/personal/fuj-management/go/internal/web/html_handler.go) | Replace stub `ServeJuniors` with the same shape as `ServeAdults` (lines 20–33). |
|
||||
| [go/internal/web/templates/juniors.tmpl](../../srv/personal/fuj-management/go/internal/web/templates/juniors.tmpl) | Replace placeholder with a structurally identical copy of [adults.tmpl](../../srv/personal/fuj-management/go/internal/web/templates/adults.tmpl), with the title swapped to "Juniors Dashboard" and the Unmatched section removed (parity with Python). |
|
||||
| [docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md](../../srv/personal/fuj-management/docs/plans/2026-05-03-2349-go-backend-rewrite-progress.md) | Tick `M6.3` once merged, append merge SHA. |
|
||||
| [CHANGELOG.md](../../srv/personal/fuj-management/CHANGELOG.md) | New top-most entry per CLAUDE.md format. |
|
||||
|
||||
## Decisions captured
|
||||
|
||||
- **No "Unmatched Transactions" section on `/juniors`.** Python's [templates/juniors.html](../../srv/personal/fuj-management/templates/juniors.html) renders only Credits + Debts (no Unmatched block). Although `JuniorsResponse.Unmatched` is populated, parity says we don't render it on juniors. The data stays available for the modal in M6.5.
|
||||
- **Reuse `qrHref` / `qrHrefAll` template funcs** from [render.go:35–57](../../srv/personal/fuj-management/go/internal/web/render.go#L35-L57). They are name-only (not adult-specific) and already on `tmplFuncs`.
|
||||
- **Reuse the existing `cell-empty / cell-unpaid / cell-unpaid-current / cell-ok / cell-overridden` cell-class branching** from `adults.tmpl` lines 60–67 verbatim. The "?" sentinel is rendered as plain `{{$cell.Text}}` text — no template branch needed because `buildJuniorMemberRow` produces `"?"` or `"? CZK"` already.
|
||||
- **No template helper extraction** (e.g. shared `_table_section.tmpl`). The two templates will be near-duplicates, but extracting a shared partial now is premature — wait until M6.4 or later when patterns settle. M6.2 didn't extract one either.
|
||||
- **Branch**: `feat/go-m6-3-juniors-page`, MR via `tea pr create` per CLAUDE.md.
|
||||
|
||||
## Verification
|
||||
|
||||
End-to-end smoke (parallel-running both backends as in M5/M6.2):
|
||||
|
||||
1. `cd go && make run` (or whatever target boots the Go server on :8080).
|
||||
2. Browser-load `http://localhost:8080/juniors`. Confirm:
|
||||
- Member rows render with cells containing fee + attendance breakdown like `0/500 CZK (3:2J,1A)`.
|
||||
- At least one `"?"` cell visible (any junior with exactly one session in some month).
|
||||
- Filter input narrows rows by name; From/To month selects hide columns via `data-month-idx`.
|
||||
- `Pay` button visible on past-month unpaid cells; clicking links to `/qr?...`. (The QR endpoint itself lands in M6.6 — only the link target is in scope here.)
|
||||
- `Pay All` visible on rows with `Balance < 0`.
|
||||
- TOTAL row sums correctly per month.
|
||||
- Credits + Debts sections render when present; no Unmatched section.
|
||||
3. JSON parity: `make parity` against the M3 fixture corpus must still report zero non-allowlisted diffs for `/api/juniors` (the `AssembleJuniors` extraction must not change wire output).
|
||||
4. Compare side-by-side against Python `/juniors` on :5001 for the same fixture; the table cells should match cell-for-cell modulo the known M5 allowlist.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Modal (`[i]` info button + `#memberModal`) — that is M6.5.
|
||||
- QR endpoint, `/sync-bank`, `/flush-cache`, `/version` pages — M6.6.
|
||||
- Embed.FS deploy verification — M6.7.
|
||||
|
||||
## Plan-file relocation
|
||||
|
||||
Per [CLAUDE.md](../../srv/personal/fuj-management/CLAUDE.md) "Plans" section, plan files belong in `docs/plans/YYYY-MM-DD-HHMM-<slug>.md` inside the repo. Plan mode forced this draft into `~/.claude/plans/`; first step after ExitPlanMode is to copy this file to `docs/plans/2026-05-08-HHMM-go-m6-3-juniors-page.md` (resolving the timestamp at that moment) and commit it on the feature branch alongside the implementation.
|
||||
@@ -27,7 +27,9 @@ const (
|
||||
|
||||
// AdultMergedMonths mirrors ADULT_MERGED_MONTHS in scripts/attendance.py.
|
||||
// Source month → target month (source attendance accumulated into target).
|
||||
var AdultMergedMonths = map[string]string{}
|
||||
var AdultMergedMonths = map[string]string{
|
||||
"2025-09": "2025-10",
|
||||
}
|
||||
|
||||
// JuniorMergedMonths mirrors JUNIOR_MERGED_MONTHS in scripts/attendance.py.
|
||||
var JuniorMergedMonths = map[string]string{
|
||||
|
||||
@@ -46,8 +46,8 @@ func TestLoadAdults(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// adultMergedMonths is empty so 2025-09 stays as-is
|
||||
if len(months) != 1 || months[0] != "2025-09" {
|
||||
// AdultMergedMonths sends 2025-09 → 2025-10
|
||||
if len(months) != 1 || months[0] != "2025-10" {
|
||||
t.Errorf("unexpected months: %v", months)
|
||||
}
|
||||
if len(members) != 2 {
|
||||
@@ -55,7 +55,7 @@ func TestLoadAdults(t *testing.T) {
|
||||
}
|
||||
byName := map[string]int{}
|
||||
for _, m := range members {
|
||||
byName[m.Name] = m.Fees["2025-09"].Attendance
|
||||
byName[m.Name] = m.Fees["2025-10"].Attendance
|
||||
}
|
||||
if byName["Alice"] != 2 {
|
||||
t.Errorf("Alice: want 2 sessions, got %d", byName["Alice"])
|
||||
@@ -73,9 +73,9 @@ func TestLoadAdults_Fee(t *testing.T) {
|
||||
}
|
||||
byName := map[string]int{}
|
||||
for _, m := range members {
|
||||
byName[m.Name] = m.Fees["2025-09"].Expected
|
||||
byName[m.Name] = m.Fees["2025-10"].Expected
|
||||
}
|
||||
// 2 sessions in 2025-09 → AdultFeeMonthlyRate["2025-09"] = 750
|
||||
// 2 sessions land in merged 2025-10 → AdultFeeMonthlyRate["2025-10"] = 750
|
||||
if byName["Alice"] != 750 {
|
||||
t.Errorf("Alice fee: want 750, got %d", byName["Alice"])
|
||||
}
|
||||
|
||||
@@ -55,14 +55,23 @@ func (h *Handler) AssembleAdults(ctx context.Context) (AdultsResponse, error) {
|
||||
|
||||
// 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)
|
||||
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())
|
||||
writeJSON(w, buildJuniorsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")))
|
||||
return buildJuniorsResponse(members, sortedMonths, result, txns, h.Config, time.Now().Format("2006-01")), nil
|
||||
}
|
||||
|
||||
// ServePayments handles GET /api/payments.
|
||||
|
||||
@@ -33,7 +33,18 @@ func (h *HTMLHandler) ServeAdults(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *HTMLHandler) ServeJuniors(w http.ResponseWriter, r *http.Request) {
|
||||
h.renderer.Render(w, "juniors", PageData{Active: "juniors", Build: h.build})
|
||||
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) {
|
||||
|
||||
@@ -23,6 +23,13 @@ type AdultsPageData struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
// JuniorsPageData is the view model for the /juniors HTML page.
|
||||
type JuniorsPageData struct {
|
||||
PageData
|
||||
Data api.JuniorsResponse
|
||||
Error string
|
||||
}
|
||||
|
||||
// Renderer parses and executes HTML templates from the embedded FS.
|
||||
type Renderer struct {
|
||||
tmpls map[string]*template.Template
|
||||
|
||||
@@ -1,5 +1,121 @@
|
||||
{{define "title"}}Juniors{{end}}
|
||||
{{define "content"}}
|
||||
<h1>Juniors Dashboard</h1>
|
||||
<p class="description">Coming in M6.3</p>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="description">Error loading data: {{.Error}}</div>
|
||||
{{else}}
|
||||
|
||||
<div class="description">
|
||||
Balances calculated by matching Google Sheet payments against attendance fees.<br>
|
||||
Source: <a href="{{.Data.AttendanceURL}}" target="_blank" rel="noopener">Attendance Sheet</a> |
|
||||
<a href="{{.Data.PaymentsURL}}" target="_blank" rel="noopener">Payments Ledger</a>
|
||||
</div>
|
||||
|
||||
<div class="filter-container" id="filterContainer" data-current-month="{{.Data.CurrentMonth}}">
|
||||
<div class="filter-item">
|
||||
<label class="filter-label" for="nameFilter">Member</label>
|
||||
<input id="nameFilter" class="filter-input" type="text" placeholder="Filter by name…">
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label" for="fromMonth">From</label>
|
||||
<select id="fromMonth" class="filter-select">
|
||||
<option value="">All</option>
|
||||
{{range $i, $m := .Data.Months}}
|
||||
<option value="{{$i}}">{{$m}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label class="filter-label" for="toMonth">To</label>
|
||||
<select id="toMonth" class="filter-select">
|
||||
<option value="">All</option>
|
||||
{{range $i, $m := .Data.Months}}
|
||||
<option value="{{$i}}">{{$m}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<button id="applyFilter" class="filter-select" type="button" style="cursor: pointer;">Apply</button>
|
||||
<button id="clearFilter" class="filter-select" type="button" style="cursor: pointer;">All</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .Data.Results}}
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Member</th>
|
||||
{{range $i, $m := .Data.Months}}
|
||||
<th data-month-idx="{{$i}}" data-raw-month="{{index $.Data.RawMonths $i}}">{{$m}}</th>
|
||||
{{end}}
|
||||
<th>Balance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $row := .Data.Results}}
|
||||
<tr class="member-row" data-name="{{$row.Name}}">
|
||||
<td class="member-name">{{$row.Name}}</td>
|
||||
{{range $i, $cell := $row.Months}}
|
||||
<td data-month-idx="{{$i}}" title="{{$cell.Tooltip}}"
|
||||
class="{{if eq $cell.Status "empty"}}cell-empty{{else if and (or (eq $cell.Status "unpaid") (eq $cell.Status "partial")) (ge $cell.RawMonth $.Data.CurrentMonth)}}cell-unpaid-current{{else if or (eq $cell.Status "unpaid") (eq $cell.Status "partial")}}cell-unpaid{{else if eq $cell.Status "ok"}}cell-ok{{end}}{{if $cell.Overridden}} cell-overridden{{end}}">
|
||||
{{$cell.Text}}
|
||||
{{if and (or (eq $cell.Status "unpaid") (eq $cell.Status "partial")) (lt $cell.RawMonth $.Data.CurrentMonth)}}
|
||||
<a class="pay-btn" href="{{qrHref $.Data.BankAccount $cell.Amount $row.Name $cell.RawMonth}}">Pay</a>
|
||||
{{end}}
|
||||
</td>
|
||||
{{end}}
|
||||
<td class="{{if lt $row.Balance 0}}balance-neg{{else if gt $row.Balance 0}}balance-pos{{end}}" style="position: relative;">
|
||||
{{$row.Balance}}
|
||||
{{if gt $row.PayableAmount 0}}
|
||||
<a class="pay-btn" href="{{qrHrefAll $.Data.BankAccount $row.PayableAmount $row.Name $row.RawUnpaidPeriods}}">Pay All</a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr class="totals-row" style="font-weight: bold; background-color: #111; border-top: 2px solid #333;">
|
||||
<td style="text-align: left; padding: 6px 8px;">TOTAL</td>
|
||||
{{range $i, $t := .Data.Totals}}
|
||||
<td data-month-idx="{{$i}}" data-raw-month="{{index $.Data.RawMonths $i}}" class="{{if eq $t.Status "ok"}}cell-ok{{else if eq $t.Status "unpaid"}}cell-unpaid{{else if eq $t.Status "surplus"}}cell-overridden{{end}}" style="padding-top: 4px; padding-bottom: 4px;">
|
||||
<span style="font-size: 0.6em; font-weight: normal; color: #666; text-transform: lowercase; display: block; margin-bottom: 2px;">received / expected</span>
|
||||
{{$t.Text}}
|
||||
</td>
|
||||
{{end}}
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="description">No members found.</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Data.Credits}}
|
||||
<h2>Credits (Advance Payments / Surplus)</h2>
|
||||
<div class="list-container">
|
||||
{{range .Data.Credits}}
|
||||
<div class="list-item">
|
||||
<span class="list-item-name">{{.Name}}</span>
|
||||
<span class="list-item-val">{{.Amount}} CZK</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Data.Debts}}
|
||||
<h2>Debts (Missing Payments)</h2>
|
||||
<div class="list-container">
|
||||
{{range .Data.Debts}}
|
||||
<div class="list-item">
|
||||
<span class="list-item-name">{{.Name}}</span>
|
||||
<span class="list-item-val" style="color: #ff3333;">{{.Amount}} CZK</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
|
||||
<script src="/static/js/filters.js" defer></script>
|
||||
{{end}}
|
||||
|
||||
@@ -30,8 +30,7 @@ JUNIOR_MONTHLY_RATE = {
|
||||
"2026-03": 250 # reduced fee for March 2026
|
||||
}
|
||||
ADULT_MERGED_MONTHS = {
|
||||
#"2025-12": "2026-01", # keys are merged into values
|
||||
#"2025-09": "2025-10"
|
||||
"2025-09": "2025-10", # keys are merged into values
|
||||
}
|
||||
|
||||
JUNIOR_MERGED_MONTHS = {
|
||||
|
||||
@@ -1044,8 +1044,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
var defaultFrom = Math.max(0, maxMonthIdx - 4);
|
||||
fromSelect.value = defaultFrom;
|
||||
toSelect.value = maxMonthIdx;
|
||||
applyMonthFilter();
|
||||
})();
|
||||
|
||||
@@ -1025,8 +1025,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
var defaultFrom = Math.max(0, maxMonthIdx - 4);
|
||||
fromSelect.value = defaultFrom;
|
||||
toSelect.value = maxMonthIdx;
|
||||
applyMonthFilter();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user