From 926a1a5275625b8dc0a1fd22a313bb3276d77560 Mon Sep 17 00:00:00 2001
From: Erol Haagenrud
Date: Sat, 25 Jul 2026 06:29:09 +0200
Subject: [PATCH] All three changes are implemented and scratch-verified (22/22
checks passed, test_isolation.sql still 12/12, frontend typechecks cleanly).
Summary of what's ready to ship:
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Enkeltbane-anlegg (Tjøme m.fl.): banenavnet droppes nå når anlegget bare har én bane — "Tjøme Golfklubb" i stedet for "Tjøme Golfklubb – Hovedbanen". Anlegg med flere baner (f.eks. Ålesund) beholder fortsatt kombinert navn. Gjelder både turnering-import og frittstående runder.
Navngi runder: nytt valgfritt name-felt på round (migrasjon 024_round_name.sql), settbart ved opprettelse og redigerbart/fjernbart senere via "Rediger runde". Vises på tvers av rundeliste, rundeside, scorekort og statistikk (faller tilbake til banenavn når ikke satt).
Land før hjemmeklubb: "Land" er nå en nedtrekksliste (kun "Norge" foreløpig, klargjort for flere), og "Hjemmeklubb" er en søkbar liste mot teeoffs ekte klubbregister (gjenbruker det eksisterende /rounds/official-search-endepunktet — ingen ny backend-kode). Gjelder både profil-fullføring og kontoinnstillinger.
---
024_round_name.sql | 6 +
app/routers/courses.py | 8 +-
app/routers/rounds.py | 31 +++-
frontend/components/account-settings.tsx | 172 +++++++++++++++++------
frontend/components/new-round.tsx | 21 +++
frontend/components/own-rounds.tsx | 2 +
frontend/components/round-card.tsx | 7 +-
frontend/components/round-detail.tsx | 28 +++-
frontend/components/round-scorecard.tsx | 4 +-
frontend/components/round-stats.tsx | 4 +-
10 files changed, 227 insertions(+), 56 deletions(-)
create mode 100644 024_round_name.sql
diff --git a/024_round_name.sql b/024_round_name.sql
new file mode 100644
index 0000000..5e0960d
--- /dev/null
+++ b/024_round_name.sql
@@ -0,0 +1,6 @@
+-- Frittstående rundeføring (ADR-033), oppfølging 2026-07-25: brukeren skal
+-- kunne navngi rundene sine ("Man må kunne navngi rundene man spiller.").
+-- Valgfritt -- mangler et navn, faller visningen tilbake til
+-- course_name_snapshot slik den alltid har gjort.
+
+ALTER TABLE round ADD COLUMN name text;
diff --git a/app/routers/courses.py b/app/routers/courses.py
index 11983ee..7a5bf77 100644
--- a/app/routers/courses.py
+++ b/app/routers/courses.py
@@ -432,7 +432,13 @@ async def import_official_course(
# Navn kombinerer anlegg + bane -- "Hovedbanen" alene er tvetydig (mange
# klubber navngir hovedbanen sin nøyaktig likt). Fant og fikset samme
# runde som idempotent-fiksen over, samme bakenforliggende brukerrapport.
- course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"
+ # UNNTAK (2026-07-25, brukerrapport om Tjøme Golfklubb): har anlegget
+ # bare ÉN bane totalt, er banenavnet ("Hovedbanen") overflødig
+ # informasjon -- kun anleggsnavnet brukes da.
+ if len(facility.get("courses", [])) == 1:
+ course_name = facility.get("name") or "Bane"
+ else:
+ course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"
async with org_connection(organization_id) as conn, translate_db_errors():
course_row = await conn.fetchrow(
diff --git a/app/routers/rounds.py b/app/routers/rounds.py
index 4ce1b26..3bf3ae7 100644
--- a/app/routers/rounds.py
+++ b/app/routers/rounds.py
@@ -337,7 +337,12 @@ async def _resolve_teeoff_course(facility_slug: str, teeoff_course_id: int) -> _
if h.get("par") is None or h.get("hcp_index") is None:
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen mangler par eller HCP-index på ett eller flere hull.")
- course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"
+ # Samme "kun ett banenavn -- hopp over duplikat anleggsnavn"-regel som
+ # `courses.py` sin `import_official_course` (2026-07-25).
+ if len(facility.get("courses", [])) == 1:
+ course_name = facility.get("name") or "Bane"
+ else:
+ course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"
resolved = _ResolvedCourse(
holes=sorted((h["hole_number"], h["par"], h["hcp_index"]) for h in raw_holes),
course_name=course_name,
@@ -394,6 +399,9 @@ class RoundCreate(BaseModel):
teeoff_course_id: int | None = None
personal_course_id: str | None = None
tee_name: str
+ # Valgfritt eget navn på runden (2026-07-25) -- mangler det, faller
+ # visningen tilbake til course_name_snapshot, som alltid er satt.
+ name: str | None = Field(default=None, max_length=200)
played_at: date
start_hole: int = Field(default=1, ge=1, le=18)
holes_planned: Literal[9, 18] = 18
@@ -419,6 +427,7 @@ class RoundParticipantOut(BaseModel):
class RoundOut(BaseModel):
id: str
course_source: str
+ name: str | None
course_name_snapshot: str
tee_name_snapshot: str
played_at: date
@@ -440,7 +449,7 @@ class RoundOut(BaseModel):
async def _load_round_out(conn, round_id: str) -> RoundOut:
round_row = await conn.fetchrow(
"""
- SELECT id::text AS id, course_source, course_name_snapshot, tee_name_snapshot,
+ SELECT id::text AS id, course_source, name, course_name_snapshot, tee_name_snapshot,
played_at, start_hole, holes_planned, started_at, completed_at
FROM round WHERE id = $1
""",
@@ -474,6 +483,7 @@ async def _load_round_out(conn, round_id: str) -> RoundOut:
return RoundOut(
id=round_row["id"],
course_source=round_row["course_source"],
+ name=round_row["name"],
course_name_snapshot=round_row["course_name_snapshot"],
tee_name_snapshot=round_row["tee_name_snapshot"],
played_at=round_row["played_at"],
@@ -577,8 +587,8 @@ async def create_round(
INSERT INTO round
(owner_user_id, course_source, teeoff_facility_slug, teeoff_course_id,
personal_course_id, course_name_snapshot, tee_name_snapshot, played_at,
- start_hole, holes_planned, started_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
+ start_hole, holes_planned, started_at, name)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING id::text AS id
""",
user.user_id,
@@ -592,6 +602,7 @@ async def create_round(
body.start_hole,
body.holes_planned,
body.started_at,
+ body.name.strip() if body.name and body.name.strip() else None,
)
round_id = round_row["id"]
@@ -640,6 +651,12 @@ class RoundUpdate(BaseModel):
teeoff_course_id: int | None = None
personal_course_id: str | None = None
tee_name: str | None = None
+ # Rent metadata-felt -- kan endres/fjernes uansett fullført-status,
+ # samme begrunnelse som start_hole/started_at/completed_at under. Sendes
+ # feltet med tom streng, tolkes det som "fjern navnet" (NULL); mangler
+ # feltet i kallet, røres eksisterende navn ikke (samme mønster som de
+ # andre feltene her -- kun `None`/utelatt betyr "ikke rør").
+ name: str | None = Field(default=None, max_length=200)
holes_planned: Literal[9, 18] | None = None
start_hole: int | None = Field(default=None, ge=1, le=18)
# Utslagstidspunkt og "Ferdig"-tidspunkt -- begge kan justeres i
@@ -735,6 +752,12 @@ async def update_round(
await conn.execute("UPDATE round SET holes_planned = $2 WHERE id = $1", round_id, body.holes_planned)
if body.start_hole is not None:
await conn.execute("UPDATE round SET start_hole = $2 WHERE id = $1", round_id, body.start_hole)
+ if body.name is not None:
+ await conn.execute(
+ "UPDATE round SET name = $2 WHERE id = $1",
+ round_id,
+ body.name.strip() if body.name.strip() else None,
+ )
if body.started_at is not None:
await conn.execute("UPDATE round SET started_at = $2 WHERE id = $1", round_id, body.started_at)
if body.completed_at is not None:
diff --git a/frontend/components/account-settings.tsx b/frontend/components/account-settings.tsx
index 18e0880..bc95ef0 100644
--- a/frontend/components/account-settings.tsx
+++ b/frontend/components/account-settings.tsx
@@ -42,6 +42,123 @@ const BAG_CLUBS = [
] as const
const MAX_BAG_CLUBS = 14
+// Land+hjemmeklubb (2026-07-25): "Land" er klargjøring for fremtidig
+// flerspråklighet -- kun norske klubber finnes i teeoff ennå, så listen har
+// bevisst kun ett valg foreløpig. Flere land legges til her når teeoff (eller
+// en fremtidig tilsvarende kilde) faktisk har data for dem -- INGEN
+// backend-endring trengs da, kun denne listen utvides.
+const COUNTRIES = ["Norge"] as const
+
+function CountryField({ id, value, onChange }: { id: string; value: string; onChange: (v: string) => void }) {
+ return (
+