From e3adddafda6fa8d54142d645e5000800e61ed6ee Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Wed, 2 Sep 2026 16:18:47 +0200 Subject: [PATCH] refactor: Fahrer und Konten in einem Reiter, Team wird zur Uebersicht Dieselben Menschen wurden an zwei Stellen gepflegt: Mitglieder unter Team, Fahrer unter Fahrer - und die Zuordnung dazwischen von Hand. Jedes Teammitglied ist aber ein Fahrer; nur hat nicht jeder Fahrer ein Konto. "Fahrer & Konten" fuehrt beides in einer Liste: Kader, daneben wer sich anmelden kann und mit welcher Rolle. Angelegt wird eine Person als Fahrer, das Konto ist eine Checkbox im selben Dialog - mit E-Mail, sobald sie gesetzt ist. Rollen, Entfernen aus dem Team und die Kontosuche wandern ebenfalls hierher. Konten im Team ohne Fahrer bekommen eine eigene Zeile samt "Als Fahrer uebernehmen". Sie verschwaenden sonst aus der Ansicht, obwohl sie Mitglied sind. "Team" bleibt als Uebersicht: Kennzahlen des aktiven Teams, Name, Logo, Wechseln, Verlassen, Loeschen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o --- .../routes/dashboard/settings/+layout.svelte | 10 +- .../dashboard/settings/riders/+page.svelte | 614 ++++++++++++------ .../dashboard/settings/team/+page.svelte | 292 ++------- 3 files changed, 493 insertions(+), 423 deletions(-) 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} - -
-
-