Begge fiksene er ferdig og verifisert med ekte produksjonsbuild (ren frontend-endring, ingen migrasjon, ingen backend-endring):

«Ny organisasjon»-knapp i dashboardets header (ved siden av «Medlemmer»/«Ny turnering») — lar en bruker som allerede eier én org opprette en til, uten begrensning (backend støttet det allerede).

Dato-sammendrag øverst på Program-skjermens øktliste — «X av Y runder har fått dato og klokkeslett» (grønn/uthevet når alle er satt).
This commit is contained in:
Erol Haagenrud 2026-07-21 09:18:07 +02:00
parent 3f888f1e10
commit f4bce6a3d2
2 changed files with 125 additions and 17 deletions

View file

@ -241,6 +241,7 @@ export function Dashboard() {
loadingTournaments={loadingTournaments}
onSelectOrg={setActiveOrgId}
onCreateTournament={handleCreateTournament}
onCreateOrg={handleCreateOrg}
/>
) : (
<CreateOrganizationState onCreate={handleCreateOrg} compact={hasMyTournaments} />
@ -393,6 +394,7 @@ function OrganizationView({
loadingTournaments,
onSelectOrg,
onCreateTournament,
onCreateOrg,
}: {
org: MyOrg
orgs: MyOrg[]
@ -400,6 +402,7 @@ function OrganizationView({
loadingTournaments: boolean
onSelectOrg: (id: string) => void
onCreateTournament: (name: string) => void
onCreateOrg: (name: string) => void
}) {
return (
<div className="flex flex-col gap-6">
@ -422,7 +425,7 @@ function OrganizationView({
</div>
)}
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<Link
href={`/organizations/${org.organization_id}/members?name=${encodeURIComponent(org.name)}`}
className="inline-flex h-11 items-center gap-1.5 rounded-2xl border border-border bg-card px-4 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
@ -430,6 +433,7 @@ function OrganizationView({
<Users aria-hidden="true" className="size-4" />
Medlemmer
</Link>
<NewOrganizationControl onCreate={onCreateOrg} />
<NewTournamentControl onCreate={onCreateTournament} />
</div>
</div>
@ -497,6 +501,75 @@ function OrganizationSwitcher({
)
}
// Ingen vei fantes tidligere til å opprette en ANDRE organisasjon når man
// allerede har én -- CreateOrganizationState (under) vises kun ved null
// org-er. Backend (POST /orgs) har ingen begrensning på antall org-er én
// bruker kan eie (ADR-021), så dette var en ren UI-mangel.
function NewOrganizationControl({ onCreate }: { onCreate: (name: string) => void }) {
const [open, setOpen] = useState(false)
const [name, setName] = useState("")
const valid = name.trim().length >= 2
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!valid) return
onCreate(name.trim())
setName("")
setOpen(false)
}
if (!open) {
return (
<Button
type="button"
variant="outline"
onClick={() => setOpen(true)}
className="inline-flex h-11 items-center gap-1.5 rounded-2xl border border-border bg-card px-4 text-sm font-semibold text-foreground transition-colors hover:bg-accent/50"
>
<Building2 aria-hidden="true" className="size-4" />
Ny organisasjon
</Button>
)
}
return (
<form
onSubmit={handleSubmit}
className="flex w-full items-center gap-2 rounded-2xl border border-border bg-card p-2 shadow-sm shadow-black/5 sm:w-auto"
>
<Input
autoFocus
placeholder="Navn på organisasjon"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setOpen(false)
}}
className="h-10 min-w-0 flex-1 rounded-xl text-base sm:w-56"
/>
<Button
type="submit"
disabled={!valid}
size="icon"
className="size-10 shrink-0 rounded-xl"
aria-label="Opprett organisasjon"
>
<Check aria-hidden="true" className="size-5" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setOpen(false)}
className="size-10 shrink-0 rounded-xl text-muted-foreground"
aria-label="Avbryt"
>
<X aria-hidden="true" className="size-5" />
</Button>
</form>
)
}
function NewTournamentControl({ onCreate }: { onCreate: (name: string) => void }) {
const [open, setOpen] = useState(false)
const [name, setName] = useState("")

View file

@ -321,6 +321,11 @@ export function TournamentProgram({
{sorted.length === 0 ? (
<EmptyState />
) : (
<>
<DateCoverageSummary
total={sorted.length}
withDate={sorted.filter((s) => s.scheduled_at !== null).length}
/>
<ol className="flex flex-col gap-3">
{sorted.map((session, index) => (
<SessionCard
@ -337,6 +342,7 @@ export function TournamentProgram({
/>
))}
</ol>
</>
)}
<div className="mt-6">
@ -844,6 +850,35 @@ function MetaBadge({ children }: { children: React.ReactNode }) {
)
}
// --- Date coverage summary ---------------------------------------------------
// Ingen sammendrag fantes tidligere for "har alle runder fått dato" -- måtte
// leses manuelt per øktkort. All nødvendig data finnes allerede i sessions.
function DateCoverageSummary({ total, withDate }: { total: number; withDate: number }) {
const allSet = withDate === total
return (
<div
className={cn(
"mb-3 flex items-center gap-2 rounded-2xl border px-4 py-3 text-sm font-semibold",
allSet
? "border-primary/30 bg-primary/5 text-primary"
: "border-border bg-muted/50 text-muted-foreground",
)}
>
{allSet ? (
<Check aria-hidden="true" className="size-4 shrink-0" />
) : (
<Clock aria-hidden="true" className="size-4 shrink-0" />
)}
<span>
{allSet
? `Alle ${total} runder har fått dato og klokkeslett.`
: `${withDate} av ${total} runder har fått dato og klokkeslett.`}
</span>
</div>
)
}
// --- Empty state -----------------------------------------------------------
function EmptyState() {