diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md
index cdfcbea..3b85461 100644
--- a/ARCHITECTURE_DECISIONS.md
+++ b/ARCHITECTURE_DECISIONS.md
@@ -8751,6 +8751,75 @@ bør vurderes nevnt i CLAUDE.md om det skjer igjen.
---
+## ADR-085: Rediger organisasjonens hele spillerpool, når som helst (2026-08-18)
+
+Bruker meldte "lettere krise": ingen måte å redigere spillere i org-
+turneringer utenom den tre-felts inline-formen fra 2026-07-19/20
+(`EditPlayerForm` i `tournament-detail.tsx`: kun navn/HCP/kjønn). Ba
+eksplisitt om at tabellen fra spillerimporten (ADR-082) skal være
+tilgjengelig for redigering NÅR SOM HELST, ikke bare under en fersk
+CSV-import inni én bestemt turnering.
+
+**Undersøkt før bygging:** `PlayerImportPanel` fantes kun som en modal
+inni to turnering-rostringsskjermer, alltid startet ved `step="upload"`
+-- ingen rute, ingen vei til å åpne den med eksisterende spillere
+forhåndsutfylt. **Kritisk funn i `POST .../players/bulk`** (ADR-082 sitt
+lagre-endepunkt): bruker `COALESCE` per felt -- fyller KUN tomme felt på
+en eksisterende match, og rører ALDRI `display_name`/`email`/`paid` i
+det hele tatt (bevisst, for CSV-import). Dette endepunktet ville derfor
+IKKE fungert som lagre-vei for en ekte "rett dette feltet"-skjerm --
+en korrigering av et allerede utfylt navn eller HCP ville stille feilet
+(returnert 200, men ingenting endret). `PATCH /orgs/{id}/players/{id}`
+gjør derimot ekte per-felt-overskriving, men kun ett spiller om gangen,
+ingen bulk-variant.
+
+**Løsning: gjenbruk selve tabell-UI-et (`PlayerImportView`) uendret i
+sin visuelle utforming, ny orkestrering rundt den.** Ny
+`Mode.kind === "roster"` (ingen turneringskontekst -- skjuler lag-/
+klasse-kolonnene, endrer tittel/avslutningstekst). Ny
+`org-player-roster-panel.tsx`: laster radene fra `GET /orgs/{id}/
+players` (i stedet for en opplastet CSV), differ hvert felt mot
+originalverdien ved lagring, og sender KUN de faktisk endrede feltene
+som `PATCH` per eksisterende spiller (ekte overskriving) -- helt nye
+rader (lagt til i tabellen denne økten) oppretter med vanlig `POST`,
+én om gangen. Ingen backend-endring, ingen migrasjon -- ren
+klient-side orkestrering mot eksisterende endepunkter.
+
+**Bevisst utenfor omfang:** ekte sletting av en spiller fra
+organisasjonen -- "Slett"-knappen i tabellen fjerner kun raden fra
+DENNE redigeringsøkten (samme klient-side-only-oppførsel som i
+import-flyten), IKKE spilleren fra databasen. Ingen slette-endepunkt
+finnes. Gjort eksplisitt i UI-teksten (`player-import-view.tsx` sin
+`EditTable`-beskrivelse, roster-modus) for å unngå at "Slett" feilaktig
+oppfattes som en reell sletting.
+
+**Ny rute:** `/organizations/[id]/players` (`org-players.tsx` +
+`app/organizations/[id]/players/page.tsx`, samme skall-mønster som
+`org-members.tsx`/`members/page.tsx`), lenket fra en ny kort-lenke på
+Medlemmer-siden (`org-members.tsx`, ved siden av Order of Merit).
+
+**Ingen ny V0-runde denne gangen** -- selve tabellen (`EditTable`) er
+100% UENDRET gjenbruk av den allerede V0-designede komponenten fra
+ADR-082, kun ny data-inn/data-ut-orkestrering og et par tekstlinjer
+avhengig av modus. Vurdert IKKE å bryte "nye UI-flater går via V0"-
+regelen, siden ingen ny visuell design faktisk lages her.
+
+**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch-runde
+(database+API+next dev) med tre eksisterende spillere -- bekreftet at
+tabellen laster reelle verdier (ikke bare navn), at HCP-korrigering på
+en spiller MED allerede satt verdi (12.4→10.5) og navneendring på en
+spiller UTEN e-post (dermed ingen bulk-matching mulig uansett) begge
+faktisk persisterte i databasen ved reload, og at en helt ny rad
+korrekt opprettet en ny spiller. Lys+mørk bekreftet. Måtte nedgradere
+scratch-brukeren fra `owner` til `member` underveis -- org-eiere/
+-admins krever obligatorisk 2FA-oppsett ved innlogging (urelatert
+funn, ikke en bug, bare et scratch-oppsett-hinder). Scratch-stacken
+revet ned.
+
+**Rullet ut:** venter på bekreftelse (ren frontend, ingen migrasjon).
+
+---
+
## Utviklingsplan (rekkefølge)
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f363be..3c818f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12333,3 +12333,39 @@ Neste steg:
**Ingen migrasjon** -- ren frontend-endring
(`frontend/components/round-detail.tsx`).
+
+109. **Rediger organisasjonens hele spillerpool, når som helst (ADR-085)
+ — 2026-08-18.** "Lettere krise": bruker kunne ikke redigere spillere
+ i org-turneringer utenom en gammel tre-felts inline-form (navn/HCP/
+ kjønn). Ba om at spillerimport-tabellen (ADR-082) skal være
+ tilgjengelig for redigering til enhver tid.
+
+ **Kritisk funn:** `POST .../players/bulk` (import-lagringen) bruker
+ `COALESCE` per felt -- fyller KUN tomme felt på en eksisterende
+ match, rører ALDRI `display_name`/`email`/`paid`. Ubrukelig som
+ lagre-vei for ekte redigering (en korrigering av et allerede utfylt
+ navn/HCP ville stille feilet). Løsning: gjenbruker selve
+ `PlayerImportView`-tabellen UENDRET via en ny `mode.kind===
+ "roster"`, men ny orkestrering (`org-player-roster-panel.tsx`) som
+ laster fra `GET /orgs/{id}/players` og lagrer med ekte `PATCH`
+ per endret felt (kun det som faktisk er rørt) -- nye rader
+ oppretter med vanlig `POST`. Ingen backend-endring, ingen
+ migrasjon. Ny rute `/organizations/[id]/players`, lenket fra
+ Medlemmer-siden.
+
+ Ingen ny V0-runde -- selve tabellen er 100% uendret gjenbruk av en
+ allerede V0-designet komponent, kun ny data-orkestrering.
+
+ **Bevisst utenfor omfang:** ekte sletting av en spiller -- "Slett"
+ fjerner kun raden fra redigeringsøkten, ikke fra databasen (ingen
+ slette-endepunkt finnes), gjort eksplisitt i UI-teksten.
+
+ **Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Scratch-
+ runde med tre eksisterende spillere -- bekreftet at en HCP-
+ korrigering (12.4→10.5) OG en navneendring på en allerede utfylt
+ spiller begge faktisk persisterte ved reload (nøyaktig det bulk-
+ endepunktet ikke kunne), og at en ny rad korrekt opprettet en ny
+ spiller. Lys+mørk bekreftet. Scratch-stacken revet ned.
+
+ **Rullet ut:** venter på bekreftelse (ren frontend, ingen
+ migrasjon).
diff --git a/frontend/app/organizations/[id]/players/page.tsx b/frontend/app/organizations/[id]/players/page.tsx
new file mode 100644
index 0000000..d8025d7
--- /dev/null
+++ b/frontend/app/organizations/[id]/players/page.tsx
@@ -0,0 +1,13 @@
+import { OrgPlayers } from "@/components/org-players"
+
+export default async function OrgPlayersPage({
+ params,
+ searchParams,
+}: {
+ params: Promise<{ id: string }>
+ searchParams: Promise<{ name?: string }>
+}) {
+ const { id } = await params
+ const { name } = await searchParams
+ return
+}
diff --git a/frontend/components/org-members.tsx b/frontend/components/org-members.tsx
index 701f598..98ecd63 100644
--- a/frontend/components/org-members.tsx
+++ b/frontend/components/org-members.tsx
@@ -8,7 +8,7 @@
import type React from "react"
import { useEffect, useState } from "react"
import Link from "next/link"
-import { ArrowLeft, Mail, Trash2, Trophy, UserPlus, Users } from "lucide-react"
+import { ArrowLeft, Mail, Trash2, Trophy, UserPlus, Users, UsersRound } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -182,8 +182,21 @@ export function OrgMembers({ organizationId, orgName }: { organizationId: string
{canInvite && }
+
+
+
+
+ Spillerpool
+ Se og rediger alle spillere organisasjonen har registrert
+
+
+
+
diff --git a/frontend/components/org-player-roster-panel.tsx b/frontend/components/org-player-roster-panel.tsx
new file mode 100644
index 0000000..59726fa
--- /dev/null
+++ b/frontend/components/org-player-roster-panel.tsx
@@ -0,0 +1,270 @@
+"use client"
+
+// Rediger HELE organisasjonens spillerpool, når som helst -- ikke bare de
+// spillerne som tilfeldigvis dukker opp i en turnerings deltakerliste
+// (2026-08-18, "lettere krise": brukeren kunne ikke redigere spillere i
+// org-turneringer, ba eksplisitt om at tabellen fra spillerimporten (ADR-082)
+// skal være tilgjengelig for redigering til enhver tid, ikke bare under en
+// fersk CSV-import).
+//
+// Gjenbruker `PlayerImportView` (V0-eksportert, se player-import-view.tsx)
+// UENDRET som selve tabell-UI-et via en ny `mode.kind === "roster"` --
+// ORKESTRERINGEN her er ny og annerledes enn player-import-panel.tsx sin:
+// radene starter FYLT UT fra `GET /orgs/{id}/players` (ikke en opplastet
+// CSV), og "Lagre" gjør ekte PATCH-per-endret-felt mot eksisterende
+// spillere (IKKE `/players/bulk`, som kun fyller TOMME felt og aldri
+// overskriver et allerede satt navn/verdi -- feil oppførsel for en ekte
+// "rett dette feltet"-redigeringsskjerm, se app/routers/players.py sin
+// egen COALESCE-forklaring). Nye rader (lagt til i tabellen, uten kjent
+// spiller-id) opprettes med vanlig POST, én om gangen.
+
+import { useEffect, useState } from "react"
+import { PlayerImportView, type DraftRow, type FieldKey } from "@/components/player-import-view"
+
+const EMPTY_ROW: Omit = {
+ display_name: "",
+ email: "",
+ handicap_index: "",
+ gender: "",
+ birth_date: "",
+ mobile: "",
+ club: "",
+ nickname: "",
+ country: "",
+ club_member_number: "",
+ first_name: "",
+ last_name: "",
+ comment: "",
+ team_name: "",
+ class_name: "",
+}
+
+// Speiler player-import-panel.tsx sin gjeng-konvertering, men begge veier
+// -- radene her starter fra EKTE backend-data (m/f/x), ikke rå CSV-tekst.
+function fromBackendGender(value: string | null): string {
+ if (value === "f") return "female"
+ if (value === "m") return "male"
+ if (value === "x") return "other"
+ return ""
+}
+function toBackendGender(value: string): string | null {
+ if (value === "female") return "f"
+ if (value === "male") return "m"
+ if (value === "other") return "x"
+ return null
+}
+
+type ApiPlayer = {
+ id: string
+ display_name: string
+ handicap_index: number | null
+ gender: string | null
+ mobile: string | null
+ email: string | null
+ birth_date: string | null
+ nickname: string | null
+ country: string | null
+ club: string | null
+ club_member_number: string | null
+ first_name: string | null
+ last_name: string | null
+ paid: boolean
+ comment: string | null
+}
+
+// De feltene et rad-objekt kan avvike fra originalen på -- team_name/
+// class_name finnes ikke i roster-modus (ingen turneringskontekst), og
+// display_name/email/gender/birth_date/handicap_index håndteres med egen
+// konvertering ved diff, ikke rå strenglikhet.
+const PLAYER_FIELDS: FieldKey[] = [
+ "display_name",
+ "email",
+ "handicap_index",
+ "gender",
+ "birth_date",
+ "mobile",
+ "club",
+ "nickname",
+ "country",
+ "club_member_number",
+ "first_name",
+ "last_name",
+ "comment",
+]
+
+function playerToRow(p: ApiPlayer): DraftRow {
+ return {
+ key: p.id,
+ ...EMPTY_ROW,
+ display_name: p.display_name,
+ email: p.email ?? "",
+ handicap_index: p.handicap_index !== null ? String(p.handicap_index) : "",
+ gender: fromBackendGender(p.gender),
+ birth_date: p.birth_date ?? "",
+ mobile: p.mobile ?? "",
+ club: p.club ?? "",
+ nickname: p.nickname ?? "",
+ country: p.country ?? "",
+ club_member_number: p.club_member_number ?? "",
+ first_name: p.first_name ?? "",
+ last_name: p.last_name ?? "",
+ comment: p.comment ?? "",
+ }
+}
+
+export function OrgPlayerRosterPanel({
+ organizationId,
+ onClose,
+}: {
+ organizationId: string
+ onClose: () => void
+}) {
+ const [step, setStep] = useState<"loading" | "edit" | "saving" | "done">("loading")
+ const [rows, setRows] = useState([])
+ // Nøkkelen for en rad som ALLEREDE var en ekte spiller er selve
+ // spiller-id-en (satt i playerToRow over) -- originalById lar handleSave
+ // vite hvilke felt som faktisk endret seg, slik at PATCH-en kun sender
+ // det som er rørt (samme prinsipp som backend sin `exclude_unset`).
+ const [originalById, setOriginalById] = useState>({})
+ const [error, setError] = useState(null)
+ const [saveWarnings, setSaveWarnings] = useState([])
+
+ useEffect(() => {
+ let cancelled = false
+ fetch(`/orgs/${organizationId}/players`, { credentials: "include" })
+ .then((res) => (res.ok ? res.json() : Promise.reject(new Error(`players: ${res.status}`))))
+ .then((players: ApiPlayer[]) => {
+ if (cancelled) return
+ const mapped = players.map(playerToRow)
+ setRows(mapped)
+ setOriginalById(Object.fromEntries(mapped.map((r) => [r.key, r])))
+ setStep("edit")
+ })
+ .catch(() => {
+ if (!cancelled) setError("Klarte ikke å hente spillerne. Prøv å laste siden på nytt.")
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [organizationId])
+
+ function updateCell(key: string, field: FieldKey, value: string) {
+ setRows((prev) => prev.map((r) => (r.key === key ? { ...r, [field]: value } : r)))
+ }
+
+ function addBlankRow() {
+ setRows((prev) => [...prev, { key: `new-${Date.now()}`, ...EMPTY_ROW }])
+ }
+
+ function removeRow(key: string) {
+ // Fjerner KUN raden fra denne redigeringsøkten -- sletter ikke
+ // spilleren (ingen slette-endepunkt finnes ennå, se kommentar i
+ // player-import-view.tsx sin EditTable-beskrivelse for roster-modus).
+ setRows((prev) => prev.filter((r) => r.key !== key))
+ }
+
+ async function handleSave() {
+ setStep("saving")
+ setError(null)
+ setSaveWarnings([])
+ const warnings: string[] = []
+ try {
+ for (const row of rows) {
+ const isExisting = row.key in originalById
+ if (isExisting) {
+ const original = originalById[row.key]
+ const patch: Record = {}
+ for (const field of PLAYER_FIELDS) {
+ if (row[field] === original[field]) continue
+ 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
+ } else if (field === "birth_date") {
+ patch.birth_date = row.birth_date.trim() || null
+ } else {
+ patch[field] = row[field].trim() || null
+ }
+ }
+ // display_name er required (min_length=1) på backend -- aldri
+ // send en tom streng, selv om raden ble tømt ved et uhell.
+ if ("display_name" in patch && !String(patch.display_name ?? "").trim()) {
+ delete patch.display_name
+ }
+ if (Object.keys(patch).length === 0) continue
+ const res = await fetch(`/orgs/${organizationId}/players/${row.key}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(patch),
+ })
+ if (!res.ok) {
+ warnings.push(`${row.display_name || "(uten navn)"}: klarte ikke å lagre endringen.`)
+ }
+ } else if (row.display_name.trim()) {
+ // Ny rad (lagt til i tabellen denne økten) -- ekte oppretting,
+ // én om gangen (samme mønster som player-import-panel.tsx).
+ const res = await fetch(`/orgs/${organizationId}/players`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ 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,
+ gender: toBackendGender(row.gender),
+ birth_date: row.birth_date.trim() || null,
+ mobile: row.mobile.trim() || null,
+ club: row.club.trim() || null,
+ nickname: row.nickname.trim() || null,
+ country: row.country.trim() || null,
+ club_member_number: row.club_member_number.trim() || null,
+ first_name: row.first_name.trim() || null,
+ last_name: row.last_name.trim() || null,
+ comment: row.comment.trim() || null,
+ }),
+ })
+ if (!res.ok) {
+ warnings.push(`${row.display_name}: klarte ikke å opprette spilleren.`)
+ }
+ }
+ }
+ setSaveWarnings(warnings)
+ setStep("done")
+ } catch {
+ setError("Lagringen feilet. Prøv igjen.")
+ setStep("edit")
+ }
+ }
+
+ if (step === "loading") {
+ return (
+
+
+
+ )
+ }
+
+ return (
+ {}}
+ onColumnMappingChange={() => {}}
+ onConfirmMapping={() => {}}
+ onCellChange={updateCell}
+ onDeleteRow={removeRow}
+ onAddBlankRow={addBlankRow}
+ onSave={() => void handleSave()}
+ onClose={onClose}
+ />
+ )
+}
diff --git a/frontend/components/org-players.tsx b/frontend/components/org-players.tsx
new file mode 100644
index 0000000..44b7814
--- /dev/null
+++ b/frontend/components/org-players.tsx
@@ -0,0 +1,60 @@
+"use client"
+
+// Organisasjonens spillerpool -- egen side (2026-08-18), lenket fra
+// Medlemmer-siden (org-members.tsx). Se org-player-roster-panel.tsx for
+// selve redigeringslogikken; denne filen er kun sideskallet (samme mønster
+// som org-members.tsx: sticky header med tilbake-lenke + tittel).
+
+import { useState } from "react"
+import Link from "next/link"
+import { ArrowLeft, Pencil } from "lucide-react"
+import { OrgPlayerRosterPanel } from "@/components/org-player-roster-panel"
+
+export function OrgPlayers({ organizationId, orgName }: { organizationId: string; orgName: string }) {
+ const [editing, setEditing] = useState(false)
+
+ return (
+
+
+
+
+
+
+
+
+ Spillerpool
+
+
{orgName}
+
+
+
+
+
+
+ Alle spillere organisasjonen har registrert, på tvers av turneringer. Rediger navn, HCP,
+ kontaktinfo og andre felt direkte i tabellen.
+
+
+
+
+
+ {editing && setEditing(false)} />}
+
+ )
+}
diff --git a/frontend/components/player-import-view.tsx b/frontend/components/player-import-view.tsx
index cd286cc..5bee26d 100644
--- a/frontend/components/player-import-view.tsx
+++ b/frontend/components/player-import-view.tsx
@@ -44,6 +44,11 @@ export type DraftRow = { key: string } & Record
export type Mode =
| { kind: "team"; teams: { id: string; name: string }[] }
| { kind: "individual"; classes: { id: string; name: string }[] }
+ // Organisasjonens hele spillerpool, utenfor enhver turneringskontekst
+ // (2026-08-18, se org-player-roster-panel.tsx) -- ingen lag/klasse-
+ // kolonner (de gir ikke mening uten en turnering), og radene starter
+ // FYLT UT fra eksisterende spillere i stedet for en opplastet CSV.
+ | { kind: "roster" }
export type PlayerImportStep = "upload" | "map" | "edit" | "saving" | "done"
@@ -168,9 +173,14 @@ export function PlayerImportView({
// In the edit step, only render columns that some row actually uses (i.e.
// fields that were mapped), keeping mode-specific columns filtered too.
- const activeFields = selectableFields.filter((field) =>
- Object.values(columnMapping).includes(field),
- )
+ // Roster mode never goes through the upload/map steps (rows start pre-
+ // filled from existing players, see org-player-roster-panel.tsx), so
+ // `columnMapping` is always empty there -- show every selectable field
+ // instead of falling back to just the name column.
+ const activeFields =
+ mode.kind === "roster"
+ ? selectableFields
+ : selectableFields.filter((field) => Object.values(columnMapping).includes(field))
// Guarantee at least the name column so a blank table is still usable.
const editFields = activeFields.length > 0 ? activeFields : ["display_name" as FieldKey]
@@ -181,21 +191,23 @@ export function PlayerImportView({
{/* Header: title left, large close button right */}
- Rediger cellene direkte før du lagrer. Du kan legge til eller slette rader.
+ {mode.kind === "roster"
+ ? "Rediger cellene direkte, og trykk Lagre. \"Slett\" fjerner raden herfra, men sletter IKKE spilleren fra organisasjonen -- det er ikke bygget ennå."
+ : "Rediger cellene direkte før du lagrer. Du kan legge til eller slette rader."}