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
+ EventsAktionen
- {#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}
-
{/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}
+
- (riderDialog = false)}>Abbrechen
- {editing ? 'Speichern' : 'Anlegen'}
+ (personDialog = false)}>Abbrechen
+
+ {busy ? 'Wird gespeichert …' : editing ? 'Speichern' : 'Anlegen'}
+
-
-
+
+
- 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 @@
- (loginDialog = false)}>Abbrechen
- {#if loginMode === 'link'}
- Verknüpfen
- {:else}
-
- {loginBusy ? 'Wird angelegt …' : 'Konto anlegen'}
+ (accountDialog = false)}>Abbrechen
+ {#if accountMode === 'neu'}
+
+ {accountBusy ? 'Wird angelegt …' : 'Konto anlegen'}
+ {:else}
+ Verknüpfen
{/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
- openMember(t)}>
-
- Mitglied
-
{/if}
{#if !iAmOwner}
doLeave(t)}>
@@ -295,49 +251,9 @@