Shotgun-start: frontend (individuell + Cup-format)
Løpende/Shotgun-veksler i rundens gruppeoppsett og øktoppsett, per- gruppe/per-match starthull-felt i shotgun-modus. Fant og rettet en reell bug underveis: begge skjermene hadde en hardkodet sequence-sort som stille overstyrte backendens starthull-sortering for shotgun. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
f1575c9b42
commit
9f81797324
3 changed files with 297 additions and 70 deletions
|
|
@ -30,6 +30,10 @@ type ApiRoundGroup = {
|
|||
sequence: number
|
||||
tee_time: string | null
|
||||
tee_time_override: string | null
|
||||
// Kun meningsfullt når runden er 'shotgun' (migrasjon 088, ADR-096) --
|
||||
// `null` for en 'consecutive'-runde, som fortsatt bruker rundens EGET
|
||||
// delte start_hole (uendret).
|
||||
start_hole: number | null
|
||||
participants: ApiGroupParticipant[]
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +46,7 @@ type ApiRoundInfo = {
|
|||
scheduled_at: string | null
|
||||
tee_interval_minutes: number | null
|
||||
start_hole: number
|
||||
start_mode: string
|
||||
}
|
||||
|
||||
function formatTeeTime(iso: string | null) {
|
||||
|
|
@ -83,6 +88,10 @@ export function RoundGroupsPanel({
|
|||
const [roundInfo, setRoundInfo] = useState<ApiRoundInfo | null>(null)
|
||||
const [scheduledAtInput, setScheduledAtInput] = useState("")
|
||||
const [intervalInput, setIntervalInput] = useState("")
|
||||
// Shotgun-start (migrasjon 088, ADR-096) -- bruker: "Det må komme frem
|
||||
// om det er Shotgun eller løpende start... Sorteringen av startlisten
|
||||
// gjøres etter utslagsform." 'consecutive' = dagens oppførsel uendret.
|
||||
const [startModeInput, setStartModeInput] = useState<"consecutive" | "shotgun">("consecutive")
|
||||
|
||||
const [groups, setGroups] = useState<ApiRoundGroup[]>([])
|
||||
const [ungrouped, setUngrouped] = useState<ApiGroupParticipant[]>([])
|
||||
|
|
@ -103,6 +112,7 @@ export function RoundGroupsPanel({
|
|||
setRoundInfo(thisRound)
|
||||
setScheduledAtInput(toDatetimeLocalValue(thisRound?.scheduled_at ?? null))
|
||||
setIntervalInput(thisRound?.tee_interval_minutes?.toString() ?? "")
|
||||
setStartModeInput(thisRound?.start_mode === "shotgun" ? "shotgun" : "consecutive")
|
||||
setGroups(groupsData.groups)
|
||||
setUngrouped(groupsData.ungrouped)
|
||||
} catch {
|
||||
|
|
@ -134,6 +144,7 @@ export function RoundGroupsPanel({
|
|||
body: JSON.stringify({
|
||||
scheduled_at: scheduledAtIso,
|
||||
tee_interval_minutes: intervalInput.trim() ? Number(intervalInput) : null,
|
||||
start_mode: startModeInput,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
|
|
@ -169,7 +180,15 @@ export function RoundGroupsPanel({
|
|||
|
||||
function addEmptyGroup() {
|
||||
const nextSequence = (groups.reduce((m, g) => Math.max(m, g.sequence), 0) || 0) + 1
|
||||
setGroups((prev) => [...prev, { id: null, sequence: nextSequence, tee_time: null, tee_time_override: null, participants: [] }])
|
||||
setGroups((prev) => [
|
||||
...prev,
|
||||
{ id: null, sequence: nextSequence, tee_time: null, tee_time_override: null, start_hole: null, participants: [] },
|
||||
])
|
||||
}
|
||||
|
||||
function setGroupStartHole(sequence: number, value: string) {
|
||||
const startHole = value.trim() ? Number(value) : null
|
||||
setGroups((prev) => prev.map((g) => (g.sequence === sequence ? { ...g, start_hole: startHole } : g)))
|
||||
}
|
||||
|
||||
function removeGroup(sequence: number) {
|
||||
|
|
@ -220,6 +239,7 @@ export function RoundGroupsPanel({
|
|||
groups: groups.map((g) => ({
|
||||
sequence: g.sequence,
|
||||
tee_time_override: null,
|
||||
start_hole: g.start_hole,
|
||||
round_participant_ids: g.participants.map((p) => p.round_participant_id),
|
||||
})),
|
||||
}),
|
||||
|
|
@ -297,41 +317,99 @@ export function RoundGroupsPanel({
|
|||
) : (
|
||||
<>
|
||||
<section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:p-5">
|
||||
<h2 className="text-base font-bold text-foreground">Første utslag og intervall</h2>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Alle grupper starter fra samme hull (hull {roundInfo?.start_hole ?? 1}, satt for hele runden),
|
||||
med staggerte klokkeslett ut fra intervallet under. Gruppe 1 slår ut først, gruppe 2 dette
|
||||
antall minutter senere, og så videre.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<label className="flex flex-1 flex-col gap-1.5">
|
||||
<span className="text-sm font-bold text-foreground">Første utslag</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduledAtInput}
|
||||
onChange={(e) => setScheduledAtInput(e.target.value)}
|
||||
className={CONTROL}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-1 flex-col gap-1.5">
|
||||
<span className="text-sm font-bold text-foreground">Intervall (minutter)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={intervalInput}
|
||||
onChange={(e) => setIntervalInput(e.target.value)}
|
||||
placeholder="f.eks. 10"
|
||||
className={CONTROL}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveSchedule()}
|
||||
className="inline-flex h-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background px-5 text-sm font-bold text-foreground transition-colors hover:bg-accent/50"
|
||||
>
|
||||
Lagre tidspunkt
|
||||
</button>
|
||||
<h2 className="text-base font-bold text-foreground">Utslagsform</h2>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Utslagsform"
|
||||
className="inline-flex w-fit rounded-xl border border-border bg-background p-1"
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ value: "consecutive" as const, label: "Løpende start" },
|
||||
{ value: "shotgun" as const, label: "Shotgun" },
|
||||
]
|
||||
).map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={startModeInput === opt.value}
|
||||
onClick={() => setStartModeInput(opt.value)}
|
||||
className={cn(
|
||||
"h-10 rounded-lg px-4 text-sm font-bold transition-colors",
|
||||
startModeInput === opt.value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{startModeInput === "consecutive" ? (
|
||||
<>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Alle grupper starter fra samme hull (hull {roundInfo?.start_hole ?? 1}, satt for hele runden),
|
||||
med staggerte klokkeslett ut fra intervallet under. Gruppe 1 slår ut først, gruppe 2 dette
|
||||
antall minutter senere, og så videre.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<label className="flex flex-1 flex-col gap-1.5">
|
||||
<span className="text-sm font-bold text-foreground">Første utslag</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduledAtInput}
|
||||
onChange={(e) => setScheduledAtInput(e.target.value)}
|
||||
className={CONTROL}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-1 flex-col gap-1.5">
|
||||
<span className="text-sm font-bold text-foreground">Intervall (minutter)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={intervalInput}
|
||||
onChange={(e) => setIntervalInput(e.target.value)}
|
||||
placeholder="f.eks. 10"
|
||||
className={CONTROL}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveSchedule()}
|
||||
className="inline-flex h-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background px-5 text-sm font-bold text-foreground transition-colors hover:bg-accent/50"
|
||||
>
|
||||
Lagre tidspunkt
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Alle grupper går ut SAMTIDIG, hver fra sitt eget starthull -- sett starthull per
|
||||
gruppe nedenfor. Intervallet gjelder ikke i denne modusen.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:gap-4">
|
||||
<label className="flex flex-1 flex-col gap-1.5">
|
||||
<span className="text-sm font-bold text-foreground">Felles utslagstidspunkt</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={scheduledAtInput}
|
||||
onChange={(e) => setScheduledAtInput(e.target.value)}
|
||||
className={CONTROL}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveSchedule()}
|
||||
className="inline-flex h-10 shrink-0 items-center justify-center rounded-xl border border-border bg-background px-5 text-sm font-bold text-foreground transition-colors hover:bg-accent/50"
|
||||
>
|
||||
Lagre tidspunkt
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:p-5">
|
||||
|
|
@ -379,11 +457,23 @@ export function RoundGroupsPanel({
|
|||
)}
|
||||
{groups
|
||||
.slice()
|
||||
.sort((a, b) => a.sequence - b.sequence)
|
||||
.sort((a, b) =>
|
||||
// Shotgun (migrasjon 088, ADR-096): vis sortert etter
|
||||
// STARTHULL, ikke sequence -- speiler backend sin
|
||||
// tilsvarende sortering (_fetch_round_groups), men må
|
||||
// også gjelde HER siden lokale, ulagrede endringer
|
||||
// (f.eks. et nettopp justert starthull) ellers ikke
|
||||
// ville flyttet gruppen i visningen før neste lagring/
|
||||
// reload.
|
||||
startModeInput === "shotgun"
|
||||
? (a.start_hole === null ? 1 : 0) - (b.start_hole === null ? 1 : 0) ||
|
||||
(a.start_hole ?? 0) - (b.start_hole ?? 0)
|
||||
: a.sequence - b.sequence,
|
||||
)
|
||||
.map((g) => (
|
||||
<div key={g.sequence} className="rounded-xl border border-border bg-background p-3 sm:p-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="flex size-8 items-center justify-center rounded-full bg-info/15 text-sm font-extrabold tabular-nums text-info">
|
||||
{g.sequence}
|
||||
</span>
|
||||
|
|
@ -391,6 +481,20 @@ export function RoundGroupsPanel({
|
|||
<Clock aria-hidden="true" className="size-4 text-muted-foreground" />
|
||||
{formatTeeTime(g.tee_time) ?? "Ingen klokkeslett satt"}
|
||||
</span>
|
||||
{startModeInput === "shotgun" && (
|
||||
<label className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-bold text-muted-foreground">Starthull</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={18}
|
||||
value={g.start_hole ?? ""}
|
||||
onChange={(e) => setGroupStartHole(g.sequence, e.target.value)}
|
||||
placeholder="Hull"
|
||||
className={cn(CONTROL, "h-9 w-20 px-2 text-xs")}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import type React from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, Clock, EyeOff, Lock, Mail, PartyPopper, Plus, Trophy, Users, X } from "lucide-react"
|
||||
import { ArrowLeft, Clock, EyeOff, Flag, Lock, Mail, PartyPopper, Plus, Trophy, Users, X } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
|
@ -61,6 +61,10 @@ type ApiSession = {
|
|||
course_id: string
|
||||
points_per_match: number
|
||||
scoring_mode: ScoringMode
|
||||
// Shotgun-start (migrasjon 088, ADR-096) -- 'shotgun': matchene under
|
||||
// legges til med eget starthull hver (se addMatch), ikke felles
|
||||
// klokkeslett-staggering.
|
||||
start_mode: "consecutive" | "shotgun"
|
||||
locked_team_ids: string[]
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +89,9 @@ type ApiMatch = {
|
|||
points_side_b: number | null
|
||||
leading_side: "a" | "b" | null
|
||||
tee_time: string | null
|
||||
// Kun meningsfullt (og satt av organisator ved opprettelse) når økten
|
||||
// er 'shotgun' (migrasjon 088, ADR-096).
|
||||
start_hole: number | null
|
||||
participants: ApiParticipant[]
|
||||
}
|
||||
|
||||
|
|
@ -190,7 +197,20 @@ export function SessionBlindDraw({
|
|||
}
|
||||
}, [organizationId, tournamentId, sessionId])
|
||||
|
||||
const sortedMatches = useMemo(() => [...matches].sort((a, b) => a.sequence - b.sequence), [matches])
|
||||
// Shotgun (migrasjon 088, ADR-096): vis sortert etter STARTHULL, ikke
|
||||
// sequence -- speiler backend sin tilsvarende sortering (fetch_matches
|
||||
// i matches.py), men må også gjelde HER siden lokale, nettopp lagt-til
|
||||
// matcher ellers ikke ville vist i riktig rekkefølge før neste reload.
|
||||
const sortedMatches = useMemo(
|
||||
() =>
|
||||
[...matches].sort((a, b) =>
|
||||
session?.start_mode === "shotgun"
|
||||
? (a.start_hole === null ? 1 : 0) - (b.start_hole === null ? 1 : 0) ||
|
||||
(a.start_hole ?? 0) - (b.start_hole ?? 0)
|
||||
: a.sequence - b.sequence,
|
||||
),
|
||||
[matches, session?.start_mode],
|
||||
)
|
||||
const nextSequence = useMemo(
|
||||
() => (matches.length === 0 ? 1 : Math.max(...matches.map((m) => m.sequence)) + 1),
|
||||
[matches],
|
||||
|
|
@ -203,7 +223,7 @@ export function SessionBlindDraw({
|
|||
if (res.ok) setMatches(await res.json())
|
||||
}
|
||||
|
||||
async function addMatch() {
|
||||
async function addMatch(startHole?: number) {
|
||||
if (!teams) return
|
||||
setError(null)
|
||||
try {
|
||||
|
|
@ -211,7 +231,12 @@ export function SessionBlindDraw({
|
|||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ sequence: nextSequence, team_a_id: teams[0].id, team_b_id: teams[1].id }),
|
||||
body: JSON.stringify({
|
||||
sequence: nextSequence,
|
||||
team_a_id: teams[0].id,
|
||||
team_b_id: teams[1].id,
|
||||
start_hole: startHole ?? null,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) throw new Error(`create match: ${res.status}`)
|
||||
const created: ApiMatch = await res.json()
|
||||
|
|
@ -413,6 +438,7 @@ export function SessionBlindDraw({
|
|||
<RevealedView
|
||||
teams={teams}
|
||||
matches={sortedMatches}
|
||||
startMode={session.start_mode}
|
||||
scorecardHrefFor={(matchId) =>
|
||||
`/tournaments/${tournamentId}/sessions/${sessionId}/matches/${matchId}?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`
|
||||
}
|
||||
|
|
@ -430,6 +456,7 @@ export function SessionBlindDraw({
|
|||
matches={sortedMatches}
|
||||
perSide={maxSlotsPerSide(session.format)}
|
||||
format={session.format}
|
||||
startMode={session.start_mode}
|
||||
locked={session.locked_team_ids.includes(team.id)}
|
||||
usedPlayerIds={usedPlayerIds}
|
||||
onAddParticipant={addParticipant}
|
||||
|
|
@ -523,6 +550,7 @@ function TeamColumn({
|
|||
matches,
|
||||
perSide,
|
||||
format,
|
||||
startMode,
|
||||
locked,
|
||||
usedPlayerIds,
|
||||
onAddParticipant,
|
||||
|
|
@ -542,6 +570,9 @@ function TeamColumn({
|
|||
matches: ApiMatch[]
|
||||
perSide: number
|
||||
format: Format
|
||||
// Shotgun-start (migrasjon 088, ADR-096) -- styrer om "Legg til match"
|
||||
// spør om starthull (shotgun) eller ikke (consecutive, som før).
|
||||
startMode: "consecutive" | "shotgun"
|
||||
locked: boolean
|
||||
usedPlayerIds: (teamId: string, exceptMatchId: string) => Set<string>
|
||||
onAddParticipant: (
|
||||
|
|
@ -552,7 +583,7 @@ function TeamColumn({
|
|||
lineupOrder?: number | null,
|
||||
) => void
|
||||
onRemoveParticipant: (matchId: string, participantId: string) => void
|
||||
onAddMatch: () => void
|
||||
onAddMatch: (startHole?: number) => void
|
||||
complete: boolean
|
||||
confirming: boolean
|
||||
onRequestLock: () => void
|
||||
|
|
@ -560,6 +591,7 @@ function TeamColumn({
|
|||
onConfirmLock: () => void
|
||||
}) {
|
||||
const color = team.color ?? "#64748b"
|
||||
const [newMatchStartHole, setNewMatchStartHole] = useState("")
|
||||
|
||||
return (
|
||||
<section
|
||||
|
|
@ -595,10 +627,17 @@ function TeamColumn({
|
|||
<span className="flex size-8 items-center justify-center rounded-xl bg-primary text-sm font-extrabold tabular-nums text-primary-foreground">
|
||||
{match.sequence}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Clock aria-hidden="true" className="size-4" />
|
||||
{match.tee_time ? `Utslag ${formatTime(match.tee_time)}` : "Tid ikke satt"}
|
||||
</span>
|
||||
{startMode === "shotgun" ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Flag aria-hidden="true" className="size-4" />
|
||||
{match.start_hole ? `Starthull ${match.start_hole}` : "Starthull ikke satt"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Clock aria-hidden="true" className="size-4" />
|
||||
{match.tee_time ? `Utslag ${formatTime(match.tee_time)}` : "Tid ikke satt"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
|
|
@ -658,10 +697,31 @@ function TeamColumn({
|
|||
})}
|
||||
</div>
|
||||
|
||||
{!locked && startMode === "shotgun" && (
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex flex-1 items-center gap-2">
|
||||
<span className="text-xs font-bold text-muted-foreground">Starthull</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={18}
|
||||
value={newMatchStartHole}
|
||||
onChange={(e) => setNewMatchStartHole(e.target.value)}
|
||||
placeholder="Hull"
|
||||
className="h-10 w-full rounded-xl border border-border bg-background px-3 text-sm font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!locked && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onAddMatch}
|
||||
onClick={() => {
|
||||
const hole = newMatchStartHole.trim() ? Number(newMatchStartHole) : undefined
|
||||
onAddMatch(hole)
|
||||
setNewMatchStartHole("")
|
||||
}}
|
||||
className="flex items-center justify-center gap-2 rounded-2xl border border-dashed border-border bg-background/50 px-4 py-3 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||
>
|
||||
<Plus aria-hidden="true" className="size-4" />
|
||||
|
|
@ -827,10 +887,12 @@ function AddSlotForm({
|
|||
function RevealedView({
|
||||
teams,
|
||||
matches,
|
||||
startMode,
|
||||
scorecardHrefFor,
|
||||
}: {
|
||||
teams: [ApiTeam, ApiTeam]
|
||||
matches: ApiMatch[]
|
||||
startMode: "consecutive" | "shotgun"
|
||||
scorecardHrefFor: (matchId: string) => string
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -865,6 +927,11 @@ function RevealedView({
|
|||
{decided && <PartyPopper aria-hidden="true" className="size-3.5" />}
|
||||
{match.status_text}
|
||||
</span>
|
||||
) : startMode === "shotgun" ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Flag aria-hidden="true" className="size-4" />
|
||||
{match.start_hole ? `Starthull ${match.start_hole}` : "Starthull ikke satt"}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<Clock aria-hidden="true" className="size-4" />
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@ type ApiSession = {
|
|||
scheduled_at: string | null
|
||||
tee_interval_minutes: number | null
|
||||
start_hole: number
|
||||
// Shotgun-start (migrasjon 088, ADR-096) -- 'consecutive' (standard) er
|
||||
// dagens oppførsel uendret. 'shotgun': alle matcher i økten går ut
|
||||
// SAMTIDIG, fra hvert sitt starthull (match.start_hole, satt i
|
||||
// trekning-skjermen) i stedet for staggert klokkeslett.
|
||||
start_mode: "consecutive" | "shotgun"
|
||||
locked_team_ids: string[]
|
||||
revealed: boolean
|
||||
}
|
||||
|
|
@ -718,14 +723,25 @@ function SessionCard({
|
|||
</div>
|
||||
|
||||
<Link href={href} className="flex flex-col gap-1.5 border-t border-border pt-3">
|
||||
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5",
|
||||
session.start_mode === "shotgun" ? "bg-primary/15 text-primary" : "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{session.start_mode === "shotgun" ? "Shotgun" : "Løpende start"}
|
||||
</span>
|
||||
</div>
|
||||
{session.scheduled_at ? (
|
||||
<div className="flex items-start gap-2 text-sm text-foreground">
|
||||
<CalendarClock aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-primary" />
|
||||
<span className="text-pretty">
|
||||
Første utslag {formatDateTime(session.scheduled_at)}
|
||||
{session.tee_interval_minutes
|
||||
? `, ${session.tee_interval_minutes} min mellom hver`
|
||||
: ""}
|
||||
{session.start_mode === "shotgun"
|
||||
? `Felles utslag ${formatDateTime(session.scheduled_at)}`
|
||||
: `Første utslag ${formatDateTime(session.scheduled_at)}${
|
||||
session.tee_interval_minutes ? `, ${session.tee_interval_minutes} min mellom hver` : ""
|
||||
}`}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -734,7 +750,7 @@ function SessionCard({
|
|||
<span>Tidspunkt ikke satt</span>
|
||||
</div>
|
||||
)}
|
||||
{session.start_hole !== 1 && (
|
||||
{session.start_mode === "consecutive" && session.start_hole !== 1 && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Flag aria-hidden="true" className="size-4 shrink-0" />
|
||||
<span>Starter på hull {session.start_hole}</span>
|
||||
|
|
@ -823,6 +839,12 @@ function EditSessionForm({
|
|||
const [teeInterval, setTeeInterval] = useState(
|
||||
session.tee_interval_minutes ? String(session.tee_interval_minutes) : "",
|
||||
)
|
||||
// Shotgun-start (migrasjon 088, ADR-096) -- bruker: "Det må komme frem
|
||||
// om det er Shotgun eller løpende start... Sorteringen av startlisten
|
||||
// gjøres etter utslagsform." 'shotgun': starthull settes per MATCH i
|
||||
// trekning-skjermen i stedet (økten sitt eget start_hole blir da
|
||||
// urelevant, skjules under).
|
||||
const [startMode, setStartMode] = useState<"consecutive" | "shotgun">(session.start_mode)
|
||||
const [courseId, setCourseId] = useState(session.course_id)
|
||||
const [courseName, setCourseName] = useState(course?.name ?? "")
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
|
@ -841,8 +863,10 @@ function EditSessionForm({
|
|||
name: name.trim() || null,
|
||||
points_per_match: pointsValue,
|
||||
start_hole: startHoleValue,
|
||||
start_mode: startMode,
|
||||
scheduled_at: hasTime ? scheduledAt : null,
|
||||
tee_interval_minutes: intervalValue && !Number.isNaN(intervalValue) ? intervalValue : null,
|
||||
tee_interval_minutes:
|
||||
startMode === "consecutive" && intervalValue && !Number.isNaN(intervalValue) ? intervalValue : null,
|
||||
course_id: courseId,
|
||||
})
|
||||
setSubmitting(false)
|
||||
|
|
@ -895,6 +919,36 @@ function EditSessionForm({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm font-semibold">Utslagsform</Label>
|
||||
<div role="radiogroup" aria-label="Utslagsform" className="inline-flex w-fit rounded-2xl border border-border bg-background p-1">
|
||||
{(
|
||||
[
|
||||
{ value: "consecutive" as const, label: "Løpende start" },
|
||||
{ value: "shotgun" as const, label: "Shotgun" },
|
||||
]
|
||||
).map((opt) => (
|
||||
<Button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={startMode === opt.value}
|
||||
variant={startMode === opt.value ? "default" : "ghost"}
|
||||
onClick={() => setStartMode(opt.value)}
|
||||
className="h-10 rounded-xl px-4 text-sm font-bold"
|
||||
>
|
||||
{opt.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{startMode === "shotgun" && (
|
||||
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
||||
Alle matcher i økten går ut samtidig -- starthull settes per match i trekning-skjermen
|
||||
i stedet for her.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={`edit-points-${uid}`} className="text-sm font-semibold">
|
||||
|
|
@ -911,26 +965,28 @@ function EditSessionForm({
|
|||
className="h-12 rounded-2xl text-base"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={`edit-starthole-${uid}`} className="text-sm font-semibold">
|
||||
Starthull
|
||||
</Label>
|
||||
<Input
|
||||
id={`edit-starthole-${uid}`}
|
||||
inputMode="numeric"
|
||||
type="number"
|
||||
min="1"
|
||||
max="18"
|
||||
value={startHole}
|
||||
onChange={(e) => setStartHole(e.target.value)}
|
||||
className="h-12 rounded-2xl text-base"
|
||||
/>
|
||||
</div>
|
||||
{startMode === "consecutive" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={`edit-starthole-${uid}`} className="text-sm font-semibold">
|
||||
Starthull
|
||||
</Label>
|
||||
<Input
|
||||
id={`edit-starthole-${uid}`}
|
||||
inputMode="numeric"
|
||||
type="number"
|
||||
min="1"
|
||||
max="18"
|
||||
value={startHole}
|
||||
onChange={(e) => setStartHole(e.target.value)}
|
||||
className="h-12 rounded-2xl text-base"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={`edit-time-${uid}`} className="text-sm font-semibold">
|
||||
Startdato og -klokkeslett{" "}
|
||||
{startMode === "shotgun" ? "Felles utslagstidspunkt" : "Startdato og -klokkeslett"}{" "}
|
||||
<span className="font-normal text-muted-foreground">(valgfritt)</span>
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -942,7 +998,7 @@ function EditSessionForm({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{hasTime && (
|
||||
{hasTime && startMode === "consecutive" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={`edit-interval-${uid}`} className="text-sm font-semibold">
|
||||
Minutter mellom hver flight
|
||||
|
|
|
|||
Loading…
Reference in a new issue