From 48f238d158667b1bd2d4351ca70597db99354e52 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Wed, 19 Aug 2026 09:50:56 +0200 Subject: [PATCH] Fiks HCP-komma app-bredt, utdatert deltakertabell, zebra-striper i spillerpool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HCP: type="number" blokkerte komma-tegnet i nettleseren før JS-parsingen noensinne så det; byttet til text+inputMode=decimal og lagt til manglende .replace(",",".") i tre lagre-stier. Deltakertabell: "Rediger i spillerpoolen" er en full sidenavigasjon, bfcache kunne gjenopprette turneringssiden i utdatert tilstand -- løst med stille bakgrunns-refresh ved visibilitychange/pageshow. Spillerpool: zebra-striper lagt tilbake oppå rutenett-stilen fra forrige runde. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 47 ++++++++ .../individual-tournament-detail.tsx | 114 +++++++++++++----- .../components/org-player-roster-panel.tsx | 7 +- frontend/components/player-import-panel.tsx | 2 +- frontend/components/player-import-view.tsx | 17 ++- frontend/components/round-detail.tsx | 10 +- .../components/tournament-players-table.tsx | 12 +- 7 files changed, 164 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41fcc8d..59917b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13063,3 +13063,50 @@ Neste steg: **Rullet ut 2026-08-19** -- bruker bekreftet. Ren frontend. `docker compose build teecup_frontend && up -d`, rene logger, 200 OK. + +129. **HCP-komma, utdatert deltakertabell etter spillerpool-redigering, + zebra-striper -- 2026-08-19.** Tre bruker-rapporterte ting i samme + runde. (1) "Er det relevant om det registreres HCP med komma heller + enn punktum? Jeg vil at det skal være irrelevant." -- undersøkt + grundig: `tournament-players-table.tsx` og to felt i `round- + detail.tsx` ("guest-hcp"/"edit-hcp-{id}") brukte `type="number"`, + som i de fleste nettlesere BLOKKERER komma-tegnet før JS i det hele + tatt ser det -- selv om lagre-parsingen andre steder allerede gjorde + `.replace(",", ".")`, hjalp det ikke når kommaet aldri kom inn i + feltet. Byttet til `type="text"` + `inputMode="decimal"` (samme + mønster som account-settings.tsx allerede brukte korrekt). I + TILLEGG manglet selve `.replace(",", ".")`-konverteringen helt i + tre lagre-stier: `tournament-players-table.tsx` sin `commitHcp`, + og to steder i `org-player-roster-panel.tsx` samt ett i `player- + import-panel.tsx` (spillerpool-lagring/CSV-import). Alle rettet, + HCP-komma er nå konsistent håndtert app-bredt. (2) "Jeg kan ikke se + at deltagertabellen blir oppdatert når det er lagret endringer i + spillerpool-tabellen" -- rotårsak: "Rediger i spillerpoolen" er en + ekte `` (full sidenavigasjon), og nettleserens bfcache kan + gjenopprette turneringssiden i EKSAKT samme (utdaterte) tilstand + ved tilbake-navigasjon uten å kjøre noen React-effekter på nytt. + Løst med en stille bakgrunns-refresh (`visibilitychange` + + `pageshow` med `persisted`-sjekk) i BÅDE `IndividualTournamentDetail` + (deltakere/runder/baner/klasser) og `SetupTab` (spillerpool) -- + hovedlasteren løftet ut av `useEffect` til en gjenbrukbar + `loadTournamentData()`, med en delt `mountedRef` i stedet for en + per-kall `cancelled`-variabel siden den nå kalles fra to steder. + (3) "Kan du kjøre annenhver rad-bakgrunn? Hvit og lys grå?" -- + spillerpool-tabellen (`player-import-view.tsx`) fikk tilbake zebra- + striping (`bg-card`/`bg-muted/40`) oppå rutenett-stilen fra forrige + runde -- den sticky navnekolonnen får nå samme bakgrunn som resten + av raden i stedet for alltid `bg-card`, ellers ville den sett + frakoblet ut fra stripen bak seg ved sidescroll. + + **Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch + bekrefter: komma faktisk skrivbart og korrekt konvertert (testet + både i spillerpool-tabellen og den nye spillertabellen -- sistnevnte + viste "18,9" lagret som det reelle tallet 18.9, ikke reversert til + forrige verdi), zebra-striper synlige. Punkt (2) er IKKE scratch- + reprodusert (bfcache-oppførsel er vanskelig å simulere i en + hodeløs devtools-økt) -- vurdert tilstrekkelig dekket av at + rotårsaken er tydelig identifisert og fiksen er et etablert, + velprøvd mønster (visibilitychange/pageshow) for akkurat dette + problemet. + + **Rullet ut:** venter på bekreftelse. diff --git a/frontend/components/individual-tournament-detail.tsx b/frontend/components/individual-tournament-detail.tsx index 790ccd7..13324bf 100644 --- a/frontend/components/individual-tournament-detail.tsx +++ b/frontend/components/individual-tournament-detail.tsx @@ -444,38 +444,74 @@ export function IndividualTournamentDetail({ } } + // Løftet ut av useEffect (2026-08-19) -- brukes nå fra to steder: den + // vanlige innlastingen ved mount, OG en "frisk opp når fanen blir synlig + // igjen"-lytter under. Bruker: "Jeg kan ikke se at deltagertabellen blir + // oppdatert når det er lagret endringer i spillerpool-tabellen" -- rot- + // årsak: "Rediger i spillerpoolen" er en full sidenavigasjon (ekte + // , ikke Next-Link), og nettleserens bfcache gjenoppretter siden + // i EKSAKT samme (utdaterte) tilstand ved tilbake-navigasjon, uten å + // kjøre noen effekter på nytt -- `mountedRef` brukes i stedet for en + // per-kall `cancelled`-variabel siden funksjonen nå kalles fra flere + // uavhengige steder. + const mountedRef = useRef(true) useEffect(() => { - let cancelled = false - async function load() { - try { - const [tournamentsData, participantsData, roundsData, coursesData, classesData] = await Promise.all([ - getJson(`/orgs/${organizationId}/tournaments`), - getJson(`${base}/participants`), - getJson(`${base}/rounds`), - getJson(`/orgs/${organizationId}/courses`), - getJson(`${base}/classes`), - ]) - if (cancelled) return - const mine = tournamentsData?.find((t) => t.id === tournamentId) ?? null - if (!mine) { - setError("Fant ikke turneringen.") - return - } - setTournament(mine) - setParticipants(participantsData ?? []) - setRounds(roundsData ?? []) - setCourses(coursesData ?? []) - setClasses(classesData ?? []) - } catch { - if (!cancelled) setError("Klarte ikke å laste turneringen. Prøv å laste siden på nytt.") - } finally { - if (!cancelled) setLoading(false) - } - } - load() + mountedRef.current = true return () => { - cancelled = true + mountedRef.current = false } + }, []) + + async function loadTournamentData() { + try { + const [tournamentsData, participantsData, roundsData, coursesData, classesData] = await Promise.all([ + getJson(`/orgs/${organizationId}/tournaments`), + getJson(`${base}/participants`), + getJson(`${base}/rounds`), + getJson(`/orgs/${organizationId}/courses`), + getJson(`${base}/classes`), + ]) + if (!mountedRef.current) return + const mine = tournamentsData?.find((t) => t.id === tournamentId) ?? null + if (!mine) { + setError("Fant ikke turneringen.") + return + } + setTournament(mine) + setParticipants(participantsData ?? []) + setRounds(roundsData ?? []) + setCourses(coursesData ?? []) + setClasses(classesData ?? []) + } catch { + if (mountedRef.current) setError("Klarte ikke å laste turneringen. Prøv å laste siden på nytt.") + } finally { + if (mountedRef.current) setLoading(false) + } + } + + useEffect(() => { + loadTournamentData() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organizationId, tournamentId, base]) + + // Stille bakgrunns-refresh (ingen loading-spinner -- setLoading(false) + // er allerede satt) når fanen/vinduet blir synlig igjen, ELLER siden + // gjenopprettes fra bfcache (pageshow med persisted=true er det + // spesifikke signalet for akkurat det tilfellet). + useEffect(() => { + function handleVisible() { + if (document.visibilityState === "visible") loadTournamentData() + } + function handlePageShow(e: PageTransitionEvent) { + if (e.persisted) loadTournamentData() + } + document.addEventListener("visibilitychange", handleVisible) + window.addEventListener("pageshow", handlePageShow) + return () => { + document.removeEventListener("visibilitychange", handleVisible) + window.removeEventListener("pageshow", handlePageShow) + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [organizationId, tournamentId, base]) async function updateStatus(newStatus: TournamentStatus) { @@ -1218,6 +1254,26 @@ function SetupTab({ } }, [organizationId]) + // Samme "frisk opp ved synlig igjen"-behov som gjelder hele siden + // (se IndividualTournamentDetail sin tilsvarende lytter) -- "Rediger i + // spillerpoolen" er en full sidenavigasjon, og bfcache kan gjenopprette + // denne komponenten i utdatert tilstand ved tilbake-navigasjon. + useEffect(() => { + function handleVisible() { + if (document.visibilityState === "visible") refreshPool() + } + function handlePageShow(e: PageTransitionEvent) { + if (e.persisted) refreshPool() + } + document.addEventListener("visibilitychange", handleVisible) + window.addEventListener("pageshow", handlePageShow) + return () => { + document.removeEventListener("visibilitychange", handleVisible) + window.removeEventListener("pageshow", handlePageShow) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [organizationId]) + // Rundedeltakelse-tabellen (ADR-093, "Spillere"-steget) -- lat innlasting, // KUN når steget faktisk vises (ikke eagerly ved sideinnlasting), av alle // rundenes rundedeltakere OG alle unike baners utslag samtidig. Flere diff --git a/frontend/components/org-player-roster-panel.tsx b/frontend/components/org-player-roster-panel.tsx index dbda015..5865c65 100644 --- a/frontend/components/org-player-roster-panel.tsx +++ b/frontend/components/org-player-roster-panel.tsx @@ -200,7 +200,10 @@ export function OrgPlayerRosterPanel({ if (field === "gender") { patch.gender = toBackendGender(row.gender) } else if (field === "handicap_index") { - patch.handicap_index = row.handicap_index.trim() ? Number(row.handicap_index) : null + // Komma-desimal skal fungere likt som punktum (2026-08-19). + patch.handicap_index = row.handicap_index.trim() + ? Number(row.handicap_index.replace(",", ".")) + : null } else if (field === "birth_date") { patch.birth_date = row.birth_date.trim() || null } else { @@ -232,7 +235,7 @@ export function OrgPlayerRosterPanel({ body: JSON.stringify({ display_name: row.display_name.trim(), email: row.email.trim() || null, - handicap_index: row.handicap_index.trim() ? Number(row.handicap_index) : null, + handicap_index: row.handicap_index.trim() ? Number(row.handicap_index.replace(",", ".")) : null, gender: toBackendGender(row.gender), birth_date: row.birth_date.trim() || null, mobile: row.mobile.trim() || null, diff --git a/frontend/components/player-import-panel.tsx b/frontend/components/player-import-panel.tsx index 2c4e57f..2df541d 100644 --- a/frontend/components/player-import-panel.tsx +++ b/frontend/components/player-import-panel.tsx @@ -299,7 +299,7 @@ export function PlayerImportPanel({ data: { display_name: r.display_name.trim(), email: r.email.trim() || null, - handicap_index: r.handicap_index.trim() ? Number(r.handicap_index) : null, + handicap_index: r.handicap_index.trim() ? Number(r.handicap_index.replace(",", ".")) : null, gender: toBackendGender(r.gender), birth_date: r.birth_date.trim() || null, mobile: r.mobile.trim() || null, diff --git a/frontend/components/player-import-view.tsx b/frontend/components/player-import-view.tsx index c8de7cb..6cc98b0 100644 --- a/frontend/components/player-import-view.tsx +++ b/frontend/components/player-import-view.tsx @@ -496,7 +496,15 @@ function EditTable({ - {rows.map((row, rowIndex) => ( + {rows.map((row, rowIndex) => { + // Annenhver rad-bakgrunn (2026-08-19, bruker: "hvit og lys + // grå") -- ren pekepinn for å lese på tvers av en rad i en + // bred tabell, samme rutenett-linjer som ellers. Den sticky + // første cellen må få SAMME bakgrunn som resten av raden + // (ikke alltid bg-card), ellers ser den frakoblet ut fra + // stripen bak den ved sidescroll. + const rowBg = rowIndex % 2 === 1 ? "bg-muted/40" : "bg-card" + return ( {editFields.map((field, i) => ( - + {field === "gender" ? ( setHcp(e.target.value)} placeholder="F.eks. 18" @@ -5392,9 +5394,9 @@ function EditParticipantPanel({ setHcp(e.target.value)} placeholder="F.eks. 18" diff --git a/frontend/components/tournament-players-table.tsx b/frontend/components/tournament-players-table.tsx index 431d424..cfce296 100644 --- a/frontend/components/tournament-players-table.tsx +++ b/frontend/components/tournament-players-table.tsx @@ -361,7 +361,9 @@ function PlayerRow({ onSetHandicap(player.participantId, null) return } - const num = Number(trimmed) + // Komma-desimal (norsk skrivemåte, "12,4") skal fungere likt som punktum + // -- 2026-08-19, bruker: "jeg vil at det skal være irrelevant". + const num = Number(trimmed.replace(",", ".")) if (Number.isNaN(num)) { // Revert bad input back to the last known good value. setHcpDraft(player.handicapSnapshot === null ? "" : String(player.handicapSnapshot)) @@ -382,11 +384,11 @@ function PlayerRow({ setHcpDraft(e.target.value)}