diff --git a/frontend/src/routes/dashboard/settings/+layout.svelte b/frontend/src/routes/dashboard/settings/+layout.svelte index 612abb8..15ae4cd 100644 --- a/frontend/src/routes/dashboard/settings/+layout.svelte +++ b/frontend/src/routes/dashboard/settings/+layout.svelte @@ -1,9 +1,11 @@ @@ -149,28 +271,27 @@ {#if !canManage} - Fahrer verwaltet, wer das Team führt. Die Fahrer deines Teams siehst du + Fahrer und Konten verwaltet, wer das Team führt. Den Kader siehst du unter Fahrer. {:else}
-
+

- Der Kader des Teams. Wer bei einem Event dabei ist und mit welcher - Startnummer, steht am Event selbst. Ein verknüpftes Konto erlaubt der - Person, sich anzumelden. + Jedes Teammitglied ist ein Fahrer — ein Konto braucht es dafür nicht. + Wer sich anmelden können soll, bekommt eins.

- {#if roster.length === 0} + {#if rows.length === 0} - Noch keine Fahrer im Team. + Noch niemand im Team. {:else} @@ -180,51 +301,128 @@ Name - Teilnahmen - Konto + Konto + Rolle + Events Aktionen - {#each roster as r (r.id)} + {#each rows as row (row.rider?.id ?? row.userId)} - {fullName(r) || '— ohne Name —'} - - - {participants.byRider(r.id).length} + + + {row.name} + {#if !row.rider} + kein Fahrer + {/if} + + - {#if r.user} - - - - kann sich einloggen - - - {teams.userLabel(r.user)} - + {#if row.userId} + + {teams.userLabel(row.userId)} + {:else} + kein Konto + {/if} + + + + {#if row.role === 'owner'} + + + Owner + + {:else if row.role === 'admin'} + + + Admin + + {:else if row.role === 'mitglied'} + Mitglied {:else} {/if} + + + {row.rider ? participants.byRider(row.rider.id).length : '—'} + + - {#if r.user} - - {:else} - + {/if} + + {#if row.rider && !row.userId} + + {:else if row.rider && row.userId} + + {:else if row.userId} + + {/if} + + {#if row.rider} + + {/if} + + {#if row.userId && row.role !== 'owner'} + + {/if} + + {#if row.rider} + {/if} - - {/each} @@ -236,39 +434,77 @@
{/if} - - + + - {editing ? 'Fahrer bearbeiten' : 'Neuer Fahrer'} + {editing ? 'Fahrer bearbeiten' : 'Person anlegen'} + + {editing + ? 'Der Name gilt teamweit; Startnummern hängen am jeweiligen Event.' + : 'Kommt als Fahrer in den Kader. Ein Konto ist optional.'} + +
- - + +
- - + +
- {#if riderError} -

{riderError}

+ + {#if !editing} + + + {#if form.withAccount} +
+ + +
+
+ + +

+ Das Konto kommt zugleich ins Team. Gib das Passwort weiter; es lässt + sich unter „Profil" ändern. +

+
+ {/if} + {/if} + + {#if error} +

{error}

{/if}
+ - - + +
- - + + - Konto für {loginRider ? fullName(loginRider) : 'Fahrer'} + + Konto für {accountRider ? fullName(accountRider) : 'Fahrer'} + Mit einem Konto kann sich die Person selbst anmelden. @@ -276,69 +512,75 @@
- +
- {#if loginMode === 'link'} + {#if accountMode === 'neu'}
- - - - {linkUserId ? teams.userLabel(linkUserId) : 'Mitglied wählen…'} - - - {#each linkCandidates as id (id)} - {teams.userLabel(id)} - {/each} - - - {#if linkCandidates.length === 0} -

- Alle Teammitglieder sind bereits einem Fahrer zugeordnet. -

- {/if} + + +
+
+ +
{:else}
- - -
-
- - -

- Das Konto kommt zugleich ins Team. Gib das Passwort weiter; es lässt - sich unter „Profil" ändern. -

+ +
+ e.key === 'Enter' && searchAccount()} + /> + +
+ + {#if found} +
+ +
+

{found.name || found.email}

+

+ Wird ins Team aufgenommen und verknüpft. +

+
+
+ {/if} {/if} - {#if loginError} -

{loginError}

+ {#if accountError} +

{accountError}

{/if}
- - {#if loginMode === 'link'} - - {:else} - + {#if accountMode === 'neu'} + + {:else} + {/if}
diff --git a/frontend/src/routes/dashboard/settings/team/+page.svelte b/frontend/src/routes/dashboard/settings/team/+page.svelte index 63980c9..6efbd10 100644 --- a/frontend/src/routes/dashboard/settings/team/+page.svelte +++ b/frontend/src/routes/dashboard/settings/team/+page.svelte @@ -6,6 +6,10 @@ import { api } from '$lib/stores/pocketbase.svelte' import { getTeamContext } from '$lib/stores/teams.svelte' import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte' + import { getEventContext } from '$lib/stores/events.svelte' + import { getRiderContext } from '$lib/stores/riders.svelte' + import { getTrailContext } from '$lib/stores/trails.svelte' + import { getTrailMarkerContext } from '$lib/stores/trailMarkers.svelte' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' @@ -14,13 +18,24 @@ import * as Dialog from '@/components/ui/dialog' import Avatar from '$lib/components/Avatar.svelte' import Logo from '$lib/components/Logo.svelte' - import { Crown, Image, LogOut, Plus, Shield, Trash2, UserMinus, UserPlus, Users } from 'lucide-svelte' + import { + Bike, Calendar, Image, LogOut, Plus, Route, Trash2, Users, + } from 'lucide-svelte' import type { TeamsResponse, UsersResponse } from '$lib/types' const teams = getTeamContext() const flags = getTrailFlagContext() + const events = getEventContext() + const riders = getRiderContext() + const trails = getTrailContext() + const markers = getTrailMarkerContext() const me = $derived(api.authStore.record?.id ?? null) + const accounts = $derived(riders.scoped.filter((r) => r.user).length) + const openMarkers = $derived( + trails.scoped.reduce((sum, t) => sum + markers.openByTrail(t.id).length, 0), + ) + // Namen der Mitglieder nachladen — ein Team kennt nur deren IDs. $effect(() => { for (const t of teams.records) teams.loadMembers(t) @@ -83,6 +98,25 @@ } } + // Kennzahlen des aktiven Teams — was hier steht, gehört dem Team und + // wandert beim Wechsel mit. + const stats = $derived([ + { + label: 'Events', + value: events.scoped.length, + hint: `${events.upcoming.length} anstehend`, + icon: Calendar, + }, + { label: 'Fahrer', value: riders.scoped.length, hint: `${accounts} mit Konto`, icon: Bike }, + { + label: 'Trails', + value: trails.scoped.length, + hint: `${openMarkers} offene Meldungen`, + icon: Route, + }, + { label: 'Mitglieder', value: (teams.active?.users ?? []).length, icon: Users }, + ]) + // ---- Logo ---- let logoDialog = $state(false) let logoTeam = $state(null) @@ -112,91 +146,6 @@ } } - // ---- Mitglied hinzufügen ---- - // Zwei Wege: ein bestehendes Konto über seine E-Mail suchen (seit - // 1754501200 wieder möglich) oder ein neues anlegen. - let memberMode = $state<'suchen' | 'anlegen'>('suchen') - let searchEmail = $state('') - let searching = $state(false) - let found = $state(null) - - let memberDialog = $state(false) - let memberTeam = $state(null) - let memberForm = $state({ name: '', email: '', password: '' }) - let memberBusy = $state(false) - let memberError = $state(null) - - function openMember(t: TeamsResponse) { - memberTeam = t - memberMode = 'suchen' - searchEmail = '' - found = null - memberForm = { name: '', email: '', password: '' } - memberError = null - memberDialog = true - } - - async function searchMember() { - if (!searchEmail.trim()) return - - searching = true - memberError = null - found = null - try { - const user = await teams.findUserByEmail(searchEmail.trim()) - if (user) found = user - else memberError = `Kein Konto mit „${searchEmail}" gefunden.` - } catch (e: any) { - memberError = e.message ?? 'Die Suche ist fehlgeschlagen.' - } finally { - searching = false - } - } - - async function addFound() { - if (!memberTeam || !found) return - - if ((memberTeam.users ?? []).includes(found.id)) { - memberError = 'Diese Person ist bereits im Team.' - return - } - - memberBusy = true - memberError = null - try { - await teams.addMember(memberTeam.id, found.id) - memberDialog = false - } catch (e: any) { - memberError = e.message ?? 'Das Konto konnte nicht hinzugefügt werden.' - } finally { - memberBusy = false - } - } - - async function doCreateMember() { - if (!memberTeam) return - - if (!memberForm.email.trim()) { - memberError = 'Ohne E-Mail-Adresse gibt es keinen Anmeldenamen.' - return - } - if (memberForm.password.length < 8) { - memberError = 'Das Passwort braucht mindestens 8 Zeichen.' - return - } - - memberBusy = true - memberError = null - try { - await teams.createMember({ ...memberForm }, memberTeam.id) - memberDialog = false - } catch (e: any) { - memberError = e.message ?? 'Das Konto konnte nicht angelegt werden.' - } finally { - memberBusy = false - } - } - // ---- Aktionen ---- async function doLeave(t: TeamsResponse) { if (!confirm(`Team „${t.name}" wirklich verlassen?`)) return @@ -207,18 +156,29 @@ } } - async function doRemoveMember(t: TeamsResponse, userId: string) { - if (userId === t.owner) return alert('Der Owner kann nicht entfernt werden.') - if (!confirm('Mitglied wirklich entfernen?')) return - try { - await teams.removeMember(t.id, userId) - } catch (e: any) { - alert(`Fehler: ${e.message}`) - } - } -
+
+ {#if teams.active} +
+ {#each stats as stat (stat.label)} + {@const Icon = stat.icon} + + +
+

{stat.label}

+ +
+

{stat.value}

+ {#if stat.hint} +

{stat.hint}

+ {/if} +
+
+ {/each} +
+ {/if} +

Teams, in denen du bist. Das aktive Team bestimmt, welche Events, Fahrer und @@ -277,10 +237,6 @@ Logo - {/if} {#if !iAmOwner}

- -
    - {#each (t.users ?? []) as userId (userId)} - {@const isOwner = userId === t.owner} - {@const isAdm = (t.admins ?? []).includes(userId)} -
  • -
    - - {teams.userLabel(userId)} - {#if isOwner} - - - Owner - - {:else if isAdm} - - - Admin - - {/if} - {#if userId === me} - Du - {/if} -
    - {#if iAmOwner && !isOwner} -
    - {#if isAdm} - - {:else} - - {/if} - -
    - {/if} -
  • - {/each} -
+ + {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} + · verwaltet unter Fahrer {/each} @@ -389,96 +305,6 @@ - - - - - Mitglied hinzufügen - - Für „{memberTeam?.name}" — entweder ein bestehendes Konto oder ein neues. - - - -
- - -
- -
- {#if memberMode === 'suchen'} -
- -
- e.key === 'Enter' && searchMember()} - /> - -
-
- - {#if found} -
- -
-

{found.name || found.email}

-

Konto gefunden

-
-
- {/if} - {:else} -
- - -
-
- - -
-
- - -

- Gib es der Person weiter; sie kann es unter „Profil" ändern. -

-
- {/if} - - {#if memberError} -

{memberError}

- {/if} -
- - - - {#if memberMode === 'suchen'} - - {:else} - - {/if} - -
-
-