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:
parent
3f888f1e10
commit
f4bce6a3d2
2 changed files with 125 additions and 17 deletions
|
|
@ -241,6 +241,7 @@ export function Dashboard() {
|
||||||
loadingTournaments={loadingTournaments}
|
loadingTournaments={loadingTournaments}
|
||||||
onSelectOrg={setActiveOrgId}
|
onSelectOrg={setActiveOrgId}
|
||||||
onCreateTournament={handleCreateTournament}
|
onCreateTournament={handleCreateTournament}
|
||||||
|
onCreateOrg={handleCreateOrg}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CreateOrganizationState onCreate={handleCreateOrg} compact={hasMyTournaments} />
|
<CreateOrganizationState onCreate={handleCreateOrg} compact={hasMyTournaments} />
|
||||||
|
|
@ -393,6 +394,7 @@ function OrganizationView({
|
||||||
loadingTournaments,
|
loadingTournaments,
|
||||||
onSelectOrg,
|
onSelectOrg,
|
||||||
onCreateTournament,
|
onCreateTournament,
|
||||||
|
onCreateOrg,
|
||||||
}: {
|
}: {
|
||||||
org: MyOrg
|
org: MyOrg
|
||||||
orgs: MyOrg[]
|
orgs: MyOrg[]
|
||||||
|
|
@ -400,6 +402,7 @@ function OrganizationView({
|
||||||
loadingTournaments: boolean
|
loadingTournaments: boolean
|
||||||
onSelectOrg: (id: string) => void
|
onSelectOrg: (id: string) => void
|
||||||
onCreateTournament: (name: string) => void
|
onCreateTournament: (name: string) => void
|
||||||
|
onCreateOrg: (name: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
|
@ -422,7 +425,7 @@ function OrganizationView({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
href={`/organizations/${org.organization_id}/members?name=${encodeURIComponent(org.name)}`}
|
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"
|
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" />
|
<Users aria-hidden="true" className="size-4" />
|
||||||
Medlemmer
|
Medlemmer
|
||||||
</Link>
|
</Link>
|
||||||
|
<NewOrganizationControl onCreate={onCreateOrg} />
|
||||||
<NewTournamentControl onCreate={onCreateTournament} />
|
<NewTournamentControl onCreate={onCreateTournament} />
|
||||||
</div>
|
</div>
|
||||||
</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 }) {
|
function NewTournamentControl({ onCreate }: { onCreate: (name: string) => void }) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [name, setName] = useState("")
|
const [name, setName] = useState("")
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,11 @@ export function TournamentProgram({
|
||||||
{sorted.length === 0 ? (
|
{sorted.length === 0 ? (
|
||||||
<EmptyState />
|
<EmptyState />
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<DateCoverageSummary
|
||||||
|
total={sorted.length}
|
||||||
|
withDate={sorted.filter((s) => s.scheduled_at !== null).length}
|
||||||
|
/>
|
||||||
<ol className="flex flex-col gap-3">
|
<ol className="flex flex-col gap-3">
|
||||||
{sorted.map((session, index) => (
|
{sorted.map((session, index) => (
|
||||||
<SessionCard
|
<SessionCard
|
||||||
|
|
@ -337,6 +342,7 @@ export function TournamentProgram({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6">
|
<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 -----------------------------------------------------------
|
// --- Empty state -----------------------------------------------------------
|
||||||
|
|
||||||
function EmptyState() {
|
function EmptyState() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue