From 45835cd45c610bc346abbd99ffa8a33252c30a0f Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Mon, 7 Sep 2026 22:11:13 +0200 Subject: [PATCH] refactor: Team und Kader an einem Ort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frueher lag beides in den Einstellungen und ein blasses Verzeichnis zusaetzlich unter "Fahrer". Wer einen Fahrer anlegen wollte, landete erst im Verzeichnis, dann in den Einstellungen. Jetzt gibt es einen Ort, und die Verwaltungsknoepfe stehen an den Zeilen, zu denen sie gehoeren. Der Einladungslink sitzt im Kopf der Seite statt in einer eigenen Section; die ausgegebenen Links stehen mit im Dialog, weil sie sonst mit der Section auch das Kopieren und das Zuruecknehmen verloren haetten. Die Adminrolle wandert aus der Tabellenzeile in den Personendialog. Ein Knopf, der ohne Rueckfrage Rechte vergibt, gehoert nicht neben die Icons fuers Entfernen und Loeschen. Der Bearbeiten-Knopf steht dafuer an jeder Zeile, auch an Konten ohne Fahrer — sonst waere deren Rolle nirgends mehr erreichbar. Die Liste "Meine Teams" entfaellt: Wechseln und Anlegen stehen im Submenue am Menuepunkt Team. Verlassen gilt nur dem aktiven Team und steht deshalb jetzt im Kopf. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr --- CLAUDE.md | 28 +- NAMING.md | 31 + TODO.md | 118 --- frontend/src/lib/stores/pocketbase.svelte.ts | 14 +- frontend/src/lib/stores/teams.svelte.ts | 13 - frontend/src/routes/dashboard/+page.svelte | 2 +- .../routes/dashboard/events/[id]/+page.svelte | 12 +- .../src/routes/dashboard/riders/+page.svelte | 134 --- .../routes/dashboard/settings/+layout.svelte | 20 +- .../dashboard/settings/riders/+page.svelte | 587 ----------- .../dashboard/settings/team/+page.svelte | 352 ------- .../src/routes/dashboard/team/+page.svelte | 953 ++++++++++++++++++ frontend/static/pedaler.svg | 95 ++ 13 files changed, 1125 insertions(+), 1234 deletions(-) create mode 100644 NAMING.md delete mode 100644 frontend/src/routes/dashboard/riders/+page.svelte delete mode 100644 frontend/src/routes/dashboard/settings/riders/+page.svelte delete mode 100644 frontend/src/routes/dashboard/settings/team/+page.svelte create mode 100644 frontend/src/routes/dashboard/team/+page.svelte create mode 100644 frontend/static/pedaler.svg diff --git a/CLAUDE.md b/CLAUDE.md index 6139626..4f7a888 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,20 @@ docker compose down Container und müssen mit `docker compose cp` ins Repo geholt werden; siehe `backend/README.md`. - **Type-safe client**: The PocketBase client is typed using auto-generated types in `frontend/src/lib/types.d.ts` -- **Collections**: users, teams, events, runs, riders, times +- **Collections**: users, teams, team_invites, events, runs, riders, times, + trails, trail_versions, trail_flags, trail_markers, trail_comments, + event_participants, event_series +- **Regeln**: Jede Zugriffsregel, die `@request.auth.id` erwähnt, beginnt mit + `@request.auth.id != "" && (…)`. Ohne diesen Vorspann ist eine *leere* + Relation gleich dem leeren `@request.auth.id` einer anonymen Anfrage — ein + Team ohne Admins stünde damit offen im Netz. Siehe + `backend/pb_migrations/1754501700_rules_require_login.js`. +- **Hooks**: `backend/pb_hooks/` enthält serverseitiges JS für das, was keine + Collection-Regel abbilden kann — derzeit die öffentlichen Routen der + Einladungslinks. Jeder Route-Handler läuft in einer eigenen JS-Laufzeit; + gemeinsamer Code gehört in ein Modul und wird per + ``require(`${__hooks}/…`)`` im Handler geholt. Details in + `backend/README.md`. - **Authentication**: Handled through `src/lib/stores/pocketbase.svelte.ts` with the `AuthStore` class - **File handling**: Use `getFileURL(record, file, options)` helper for PocketBase file URLs @@ -136,7 +149,18 @@ tippy.js, which the action initializes, updates and destroys. ### File Structure Notes -- Routes are in `src/routes/` following SvelteKit conventions +- Routes are in `src/routes/` following SvelteKit conventions. **Routennamen + immer auf Englisch** (`/invite/[token]`, nicht `/einladung/[token]`) — + Oberflächentexte sind deutsch, Pfade nicht. +- Der Menüpunkt **Team** (`/dashboard/team`) führt Team und Kader an einer + Stelle: Kennzahlen, Teamdaten, Einladungslinks und die Fahrerliste samt der + Verwaltungsknöpfe der Teamleitung. Unter **Einstellungen** steht nur noch, + was einen selbst betrifft (Profil, Flag-Typen). +- **Ein Fahrer, ein Name, eine Richtung**: Ein Konto entsteht immer am Fahrer + (`riders.createLogin`), es gibt kein Verknüpfen bestehender Konten und kein + Lösen. Angezeigt wird ausschließlich der Fahrername — der Kontoname wäre + eine zweite Wahrheit, die niemand geradeziehen könnte, weil + `users.updateRule` nur den Kontoinhaber selbst ändern lässt. - Reusable components in `src/lib/components/` - Stores use `.svelte.ts` extension for Svelte 5 runes - Static assets in `static/` diff --git a/NAMING.md b/NAMING.md new file mode 100644 index 0000000..faf414f --- /dev/null +++ b/NAMING.md @@ -0,0 +1,31 @@ +Die "Wanderer"-Schiene + + Shredder: Ziemlich actiongeladen, 100% MTB-Jargon und hat genau wie "Wanderer" dieses aktive, ausführende Element. + + Kurbler: Die direktere, etwas rotzigere deutsche Alternative. Hat einen leichten Retro-Charme. + + Dropper: Angelehnt an die Dropper-Post (absenkbare Sattelstütze) oder das "Reindroppen" in den Trail. + +Fokus auf Trail & Community + + TrailTribe: Betont den Vereins- und Gruppenaspekt hervorragend. + + DirtBase: Perfekt für die Traildatenbank – kurz, prägnant und technisch sauber. + + FlowHub: Verbindet das ultimative MTB-Gefühl ("Flow") mit dem modernen Plattform-Gedanken für Eventveranstalter. + +Fokus auf Zeitnahme & Action + + SendSync: "Send it!" gepaart mit der schnellen Datensynchronisation einer Webapp. + + PacePeak: Klingt sportlich, kompetitiv und extrem passend für Rennveranstalter und Bestzeitenjäger. + + TrailTicker: Etwas verspielter, bringt aber das Thema Zeitmessung und Live-Updates sofort auf den Punkt. + +Kurz & Knackig (Startup-Vibe) + + Berm: Englisch für die Anliegerkurve. Kurz, international und unter Mountainbikern ein fester Begriff. + + Roost: Der aufgeworfene Dreck in der Kurve. Hat Energie und ist sehr spezifisch für den Offroad-Sport. + + Stollen: Ein sehr deutsches, geerdetes Wort (Stollenreifen), das MTB-Flair mit technischer Präzision verbindet. diff --git a/TODO.md b/TODO.md index 467d0c5..e69de29 100644 --- a/TODO.md +++ b/TODO.md @@ -1,118 +0,0 @@ -# Offene Punkte - -Stand: 2026-09-02, abgeglichen mit der produktiven Instanz. Die fachlichen -Entscheidungen sind gefallen und gebaut; was hier steht, passiert außerhalb des -Repos oder wartet auf den nächsten Deploy. - -## Vor dem nächsten Deploy - -### `PUBLIC_CARTO_API_KEY` in Coolify setzen - -Die Kartenkacheln kommen von CARTO. Der Schlüssel muss den Browser erreichen, -deshalb das `PUBLIC_`-Präfix — ohne das reicht SvelteKit ihn nicht weiter. - -Gelesen wird er über `$env/dynamic/public`, das heißt: Die Variable muss in der -**Laufzeitumgebung** des `node build`-Prozesses stehen, nicht nur zur Buildzeit. -In Coolify als normale Umgebungsvariable eintragen genügt. Fehlt sie, laufen die -Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar. - -### Migrationen: sechs gelaufen, fünf offen - -Abgeglichen mit `api.stammtisch-hersbruck.de` (nur gelesen): Der Backend-Deploy -hat alles bis `1754500900` mitgenommen, ab `1754501000` steht es noch aus. - -| Datei | Was sie tut | Stand | -|---|---|---| -| `1754500500_riders_user_and_admin_rules` | `riders.user`, Fahrerpflege nur Teamleitung, `users.viewRule`/`createRule` | gelaufen | -| `1754500600_events_dates_instead_of_status` | `events.status` raus, `starts`/`ends` rein | gelaufen | -| `1754500700_trail_status_meta` | `trails.status_changed`, `status_by` | gelaufen | -| `1754500800_trail_markers_custom` | `trail_markers.label`/`icon`, `flag` optional | gelaufen | -| `1754500900_users_superadmin` | `users.superadmin` samt Schutz vor Selbstbeförderung | gelaufen | -| `1754501000_teams_logo` | `teams.logo` | **offen** | -| `1754501100_trails_status_report` | Mitglieder dürfen den Trail-Zustand melden | **offen** | -| `1754501200_users_list_for_invites` | `users.listRule` gelockert (Kontosuche) | **offen** | -| `1754501300_event_participants` | neue Collection, **verschiebt Daten**, entfernt `riders.event`/`number` | **offen** | -| `1754501400_events_timing_participation` | `events.timing`/`participation` | **offen** | -| `1754501500_event_series` | `event_series`, `events.series` | **offen** | - -Bis zum nächsten Deploy laufen deshalb ins Leere: Team-Logo, Zustandsmeldung -durch Mitglieder, Kontosuche per E-Mail, Teilnehmerlisten, die Event-Schalter -und die Serien. Die Oberfläche dafür ist da und wird beim Speichern eine -Fehlermeldung von PocketBase zeigen. - -**Zwei Dinge sind beim Nachziehen nicht zurückzunehmen:** - -1. `1754501300` legt für jeden Fahrer mit Event einen Teilnehmer an und entfernt - danach `riders.event` und `riders.number`. Eine Startnummer an einem Fahrer - **ohne** Event hat danach keinen Platz mehr und geht verloren — sie war - ohnehin nirgends sichtbar, weil alle Listen nach Event filterten. -2. Bestehende Events werden auf `timing = true` und `participation = - geschlossen` gesetzt. Genau so wurden sie bisher benutzt; wer einen - Stammtisch daraus machen will, schaltet die Zeitnahme im Dialog ab. - -Ebenfalls bewusst so: Nach `1754501200` kann jeder Eingeloggte Konten -auflisten, damit sich bestehende Konten per E-Mail ins Team holen lassen. -E-Mail-Adressen bleiben über `emailVisibility` verborgen. - -### Typen einmal neu erzeugen - -`frontend/src/lib/types.d.ts` wurde bei allen Schema-Änderungen von Hand -gepflegt. Grund: `pocketbase-typegen` braucht native sqlite3-Bindings, die in -der Arbeitsumgebung nicht gebaut werden durften. - -```bash -cd frontend && npm run generate-pocketbase-types -``` - -Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis -des Generators. - -### Regeln einmal durchspielen - -Die Zugriffsregeln der neuen Collections konnten nirgends laufen — weder Docker -noch eine lokale PocketBase-Instanz standen zur Verfügung. Sie sind nach dem -Muster der vorhandenen Collections geschrieben, aber ungetestet. Nach dem Deploy -einmal ausprobieren: - -- Ein einfaches Teammitglied trägt sich bei einem **offenen** Event selbst ein. -- Dasselbe bei einem Event **mit Bestätigung** — es muss als „Angefragt" - landen, nicht als „Dabei". -- Bei einem **geschlossenen** Event darf es gar nicht gehen. -- Ein Mitglied meldet einen Trail-Zustand (muss gehen) und versucht, den Namen - des Trails zu ändern (darf nicht gehen). - -Der Selbsteintrag setzt außerdem voraus, dass das Konto der Person mit einem -Fahrer verknüpft ist (Einstellungen → Fahrer & Konten). Ohne Verknüpfung -erscheint der Knopf gar nicht erst. - -### Logo des Stammtischs zuordnen - -`teams.logo` gibt es ab `1754501000`. Solange nichts hinterlegt ist, zeigt die -App weiterhin das Wappen aus `static/` — für den Stammtisch Hersbruck sieht es -also unverändert aus. Wer es als Datei am Team haben will, lädt es nach dem -Deploy unter Einstellungen → Team → Logo hoch. - -## Kleinigkeiten - -- Wer Regeln direkt im PocketBase-Admin ändert, hinterlässt keine Spur im Repo. - Die Migrationen bleiben die Quelle: Sie setzen dieselben Werte noch einmal und - überschreiben eine abweichende Handänderung stillschweigend. -- Die Rolle **Superadmin** ist gesetzt, gibt in der App aber noch keine - besonderen Rechte. Sie ist bewusst nur in PocketBase vergebbar: Die - `updateRule` der users-Collection schließt `superadmin` vom Selbstsetzen aus. - Sobald klar ist, was sie dürfen soll, kommen die Regeln dazu. -- Flag-Typen sind **teamspezifisch**. Ein neues Team bekommt beim Anlegen den - hartkodierten Standardsatz aus `trailFlags.svelte.ts` und kann ihn frei - ändern. -- „Jedes Teammitglied ist ein Fahrer" hält die Oberfläche ein, das Backend - erzwingt es nicht: Ein Konto kann im Team sein, ohne an einem Fahrer zu - hängen. Solche Fälle stehen unter „Fahrer & Konten" mit dem Vermerk „kein - Fahrer" und lassen sich per Knopf übernehmen. -- „Als Fahrer übernehmen" rät den Namen aus dem Kontonamen und setzt ihn als - Vornamen. „Max Mustermann" auf zwei Felder aufzuteilen wäre geraten und läge - bei Doppelnamen daneben; der Name lässt sich danach korrigieren. Wenn das zu - grob ist, müsste der Knopf stattdessen nach Vor- und Nachname fragen. -- Serien erzeugen bis zu zwölf Termine je Klick und überspringen, was schon - angelegt ist. Ein Termin, der ausfällt, wird als Event gelöscht oder - verschoben — die Serie bleibt davon unberührt. -- `NAMING.md` und `frontend/static/pedaler.svg` liegen bewusst nur lokal. diff --git a/frontend/src/lib/stores/pocketbase.svelte.ts b/frontend/src/lib/stores/pocketbase.svelte.ts index 413423b..af8f23f 100644 --- a/frontend/src/lib/stores/pocketbase.svelte.ts +++ b/frontend/src/lib/stores/pocketbase.svelte.ts @@ -27,19 +27,19 @@ class AuthStore { api.authStore.loadFromCookie(cookie) } + /** + * Anmelden. Ohne Argumente gelten die Felder des Stores, die das + * Login-Formular füllt; mit Argumenten die übergebenen — die + * Einladungsseite meldet damit das eben angelegte Konto an, ohne den + * Umweg über die Formularfelder. + */ login = async (user = auth.username, password = auth.password) => { this.loading = true this.error = '' - - if (this.error) { - this.loading = false - return - } - try { - await api.collection('users').authWithPassword(auth.username, auth.password, this.params) + await api.collection('users').authWithPassword(user, password, this.params) this.user = api.authStore.record as AuthRecord | null diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts index 498748e..4807784 100644 --- a/frontend/src/lib/stores/teams.svelte.ts +++ b/frontend/src/lib/stores/teams.svelte.ts @@ -246,19 +246,6 @@ export class TeamStore { return user } - /** User per E-Mail suchen (für Einladungs-UI) */ - async findUserByEmail(email: string): Promise { - try { - const res = await api.collection('users').getFirstListItem( - `email = "${email.replaceAll('"', '')}"`, - { requestKey: null }, - ) - return res - } catch { - return null - } - } - destroy() { if (this.unsubscribe) { this.unsubscribe() diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 1598cea..2ee044d 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -42,7 +42,7 @@ Team {teams.active.name} · {stats.totalEvents} Events, {stats.totalRiders} Fahrer

{:else} -

Kein Team aktiv. Team verwalten.

+

Kein Team aktiv. Team verwalten.

{/if} diff --git a/frontend/src/routes/dashboard/events/[id]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/+page.svelte index 973492f..8b14b66 100644 --- a/frontend/src/routes/dashboard/events/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation' import { getEventContext } from '$lib/stores/events.svelte' import { getRunContext } from '$lib/stores/runs.svelte' - import { fullName, getRiderContext } from '$lib/stores/riders.svelte' + import { riderName, getRiderContext } from '$lib/stores/riders.svelte' import { getTimeContext } from '$lib/stores/times.svelte' import { getTeamContext } from '$lib/stores/teams.svelte' import { @@ -283,7 +283,7 @@ {#if canManage && available.length === 0 && riders.scoped.length === 0}

Das Team hat noch keine Fahrer. - Jetzt anlegen + Jetzt anlegen

{/if} @@ -322,8 +322,8 @@ - {#if rider && fullName(rider)} - {fullName(rider)} + {#if rider && riderName(rider)} + {riderName(rider)} {:else} — ohne Name — {/if} @@ -507,12 +507,12 @@ {form.rider - ? fullName(riders.getById(form.rider) ?? ({} as any)) || 'Fahrer wählen…' + ? riderName(riders.getById(form.rider) ?? ({} as any)) || 'Fahrer wählen…' : 'Fahrer wählen…'} {#each available as r (r.id)} - {fullName(r) || '— ohne Name —'} + {riderName(r) || '— ohne Name —'} {/each} diff --git a/frontend/src/routes/dashboard/riders/+page.svelte b/frontend/src/routes/dashboard/riders/+page.svelte deleted file mode 100644 index 7e159f7..0000000 --- a/frontend/src/routes/dashboard/riders/+page.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - -Fahrer - -
-
-
-

Fahrer

-

- {riders.scoped.length} im Team {teams.active?.name ?? ''} -

-
- {#if riders.canManage} - - {/if} -
- -
- - -
- - {#if riders.scoped.length === 0} - - - -

Noch keine Fahrer

-

- {#if riders.canManage} - Leg sie in den Einstellungen an. - {:else} - Die Teamleitung legt Fahrer in den Einstellungen an. - {/if} -

- - - {:else if roster.length === 0} - - Keine Treffer. - - {:else} - - - - - - Name - Events - Zeiten - Konto - - - - {#each roster as r (r.id)} - {@const path = riderPath(r.id)} - - - {#if path} - {fullName(r) || '— ohne Name —'} - {:else} - {fullName(r) || '— ohne Name —'} - {/if} - - - {participants.byRider(r.id).length} - - - {times.getByRider(r.id).length} - - - {#if r.user} - - - {teams.userLabel(r.user)} - - {:else} - - {/if} - - - {/each} - - - - - {/if} -
diff --git a/frontend/src/routes/dashboard/settings/+layout.svelte b/frontend/src/routes/dashboard/settings/+layout.svelte index 15ae4cd..8c39ecb 100644 --- a/frontend/src/routes/dashboard/settings/+layout.svelte +++ b/frontend/src/routes/dashboard/settings/+layout.svelte @@ -2,27 +2,19 @@ /** * Rahmen der Einstellungen: eine Überschrift, darunter Reiter je Bereich. * - * „Team" ist die Übersicht des Teams, „Fahrer & Konten" führt die - * Menschen: Jedes Mitglied ist ein Fahrer, ein Konto hat aber nicht jeder. - * Beides an einer Stelle, weil es sonst zwei Wahrheiten über dieselbe - * Person gäbe. Der Reiter erscheint nur für die Teamleitung. + * Hier steht nur noch, was einen selbst betrifft — das eigene Profil und + * die Flag-Typen. Team und Kader sind unter „Team" zu Hause; sie sind + * keine Einstellung, sondern der Gegenstand der Anwendung. */ import { page } from '$app/state' - import { getTeamContext } from '$lib/stores/teams.svelte' - import { Flag, User, Users, Bike } from 'lucide-svelte' + import { Flag, User } from 'lucide-svelte' let { children } = $props() - const teams = getTeamContext() - - const tabs = $derived([ + const tabs = [ { href: '/dashboard/settings', label: 'Profil', icon: User }, - { href: '/dashboard/settings/team', label: 'Team', icon: Users }, - ...(teams.isAdmin() - ? [{ href: '/dashboard/settings/riders', label: 'Fahrer & Konten', icon: Bike }] - : []), { href: '/dashboard/settings/flags', label: 'Flag-Typen', icon: Flag }, - ]) + ] // Das Profil liegt auf der Wurzel; ohne den Sonderfall wäre sein Reiter // auf jedem Unterpfad mit aktiv. diff --git a/frontend/src/routes/dashboard/settings/riders/+page.svelte b/frontend/src/routes/dashboard/settings/riders/+page.svelte deleted file mode 100644 index 3504d4a..0000000 --- a/frontend/src/routes/dashboard/settings/riders/+page.svelte +++ /dev/null @@ -1,587 +0,0 @@ - - -{#if !canManage} - - - Fahrer und Konten verwaltet, wer das Team führt. Den Kader siehst du - unter Fahrer. - - -{:else} -
-
-

- Jedes Teammitglied ist ein Fahrer — ein Konto braucht es dafür nicht. - Wer sich anmelden können soll, bekommt eins. -

- -
- - {#if rows.length === 0} - - - Noch niemand im Team. - - - {:else} - - - - - - Name - Konto - Rolle - Events - Aktionen - - - - {#each rows as row (row.rider?.id ?? row.userId)} - - - - - {row.name} - {#if !row.rider} - kein Fahrer - {/if} - - - - - {#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 iAmOwner && row.userId && row.role !== 'owner'} - - {/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} - - - - - {/if} -
-{/if} - - - - - - {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 !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 {accountRider ? fullName(accountRider) : 'Fahrer'} - - - Mit einem Konto kann sich die Person selbst anmelden. - - - -
- - -
- -
- {#if accountMode === 'neu'} -
- - -
-
- - -
- {:else} -
- -
- e.key === 'Enter' && searchAccount()} - /> - -
-
- - {#if found} -
- -
-

{found.name || found.email}

-

- Wird ins Team aufgenommen und verknüpft. -

-
-
- {/if} - {/if} - - {#if accountError} -

{accountError}

- {/if} -
- - - - {#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 deleted file mode 100644 index 6efbd10..0000000 --- a/frontend/src/routes/dashboard/settings/team/+page.svelte +++ /dev/null @@ -1,352 +0,0 @@ - - -
- {#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 - Trails du siehst. -

- -
- - {#if teams.records.length === 0} - - - -

Noch in keinem Team

-

Erstelle dein erstes Team.

- -
-
- {:else} - {#each teams.records as t (t.id)} - {@const iAmOwner = t.owner === me} - {@const iAmAdmin = iAmOwner || (t.admins ?? []).includes(me ?? '')} - - -
-
- -
- - {t.name} - {#if t.id === teams.activeId} - Aktiv - {/if} - - - {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} - -
-
-
- {#if t.id !== teams.activeId} - - {/if} - {#if iAmAdmin} - - - {/if} - {#if !iAmOwner} - - {:else} - - {/if} -
-
-
- - {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} - · verwaltet unter Fahrer - -
- {/each} - {/if} -
- - - - - - Neues Team - Du wirst automatisch Owner. - -
-
- - -
- {#if createError} -

{createError}

- {/if} -
- - - - -
-
- - - - - - Team umbenennen - -
-
- - -
- {#if renameError} -

{renameError}

- {/if} -
- - - - -
-
- - - - - - Logo für „{logoTeam?.name}" - - Erscheint in der Kopfzeile und blass im Seitenhintergrund. Ohne eigenes - Logo steht dort das Wappen des Stammtischs. - - - -
-
- -
- - (logoFile = e.currentTarget.files?.[0] ?? null)} - /> -

PNG, SVG oder WebP, bis 5 MB.

-
-
- - {#if logoError} -

{logoError}

- {/if} -
- - - - {#if logoTeam?.logo} - - {/if} - - -
-
diff --git a/frontend/src/routes/dashboard/team/+page.svelte b/frontend/src/routes/dashboard/team/+page.svelte new file mode 100644 index 0000000..30e949e --- /dev/null +++ b/frontend/src/routes/dashboard/team/+page.svelte @@ -0,0 +1,953 @@ + + +Team + +
+
+ +
+

{team?.name ?? 'Team'}

+

+ {(team?.users ?? []).length} Mitglied{(team?.users ?? []).length === 1 ? '' : 'er'} + · {riders.scoped.length} im Kader +

+
+ + + {#if team} +
+ {#if canManage} + + + + {/if} + + {#if iAmOwner} + + {:else} + + {/if} +
+ {/if} +
+ + {#if !team} + + + +

Noch in keinem Team

+

Erstelle dein erstes Team.

+ +
+
+ {:else} +
+ {#each stats as stat (stat.label)} + {@const Icon = stat.icon} + + +
+

{stat.label}

+ +
+

{stat.value}

+ {#if stat.hint} +

{stat.hint}

+ {/if} +
+
+ {/each} +
+ + + + +
+
+ + +
+ {#if canManage} + + {/if} +
+
+ + + {#if rows.length === 0} +
+ +

Noch niemand im Team

+

+ {#if canManage} + Leg die erste Person an oder verschick einen Einladungslink. + {:else} + Die Teamleitung legt den Kader an. + {/if} +

+
+ {:else if filtered.length === 0} +
Keine Treffer.
+ {:else} + + + + Name + Konto + Rolle + Events + Zeiten + {#if canManage} + Aktionen + {/if} + + + + {#each filtered as row (row.rider?.id ?? row.userId)} + {@const path = row.rider ? riderPath(row.rider.id) : null} + + + + + {#if path} + {row.name} + {:else} + {row.name} + {/if} + {#if !row.rider} + kein Fahrer + {/if} + + + + + + {#if row.userId} + + + kann sich anmelden + + {: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 : '—'} + + + + {row.rider ? times.getByRider(row.rider.id).length : '—'} + + + {#if canManage} + + {#if row.rider && !row.userId} + + {/if} + + + + {#if row.userId && row.role !== 'owner'} + + {/if} + + {#if row.rider} + + {/if} + + {/if} + + {/each} + + + {/if} +
+
+ {/if} + +
+ + + + + + {isEdit ? 'Person bearbeiten' : 'Person anlegen'} + + {isEdit + ? 'Der Name gilt teamweit; Startnummern hängen am jeweiligen Event.' + : 'Kommt als Fahrer in den Kader. Ein Konto ist optional.'} + + + +
+ {#if editing || !isEdit} +
+ + +
+ {/if} + + + {#if canSetRole} + +
+ + +

+ Admins verwalten Kader, Einladungslinks und Teamdaten. Löschen und + Rollen bleiben beim Owner. +

+
+ {:else if isEdit && editingRole === 'owner'} + +

+ Owner des Teams — diese Rolle lässt sich hier nicht ändern. +

+ {/if} + + {#if !isEdit} + + + {#if form.withAccount} +
+ + +
+
+ + +

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

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

{error}

+ {/if} +
+ + + + + +
+
+ + + + + + + Konto für {accountRider ? riderName(accountRider) : 'Fahrer'} + + + Mit einem Konto kann sich die Person selbst anmelden. Gib das Passwort + weiter; ändern lässt es sich unter „Einstellungen". + + + +
+
+ + +
+
+ + +
+ + {#if accountError} +

{accountError}

+ {/if} +
+ + + + + +
+
+ + + + + + Einladungslinks + + Wer den Link öffnet, legt sich selbst ein Konto an und steht danach im + Team {team?.name ?? ''} und im Kader. Gib ihn nur weiter, wem du das + zutraust — der Link ist das Passwort. + + + +
+ {#if invites.scoped.length > 0} +
+ {#each invites.scoped as invite (invite.id)} +
+ e.currentTarget.select()} + /> + + {inviteHint(invite)} + + + +
+ {/each} +
+ + {/if} + +
+ + +

0 = läuft nicht ab.

+
+
+ + +

0 = beliebig oft.

+
+ + {#if inviteError} +

{inviteError}

+ {/if} +
+ + + + + +
+
+ + + + + + + + + Team umbenennen + +
+
+ + +
+ {#if renameError} +

{renameError}

+ {/if} +
+ + + + +
+
+ + + + + + Logo für „{logoTeam?.name}" + + Erscheint in der Kopfzeile und blass im Seitenhintergrund. Ohne eigenes + Logo steht dort das Wappen des Stammtischs. + + + +
+
+ +
+ + (logoFile = e.currentTarget.files?.[0] ?? null)} + /> +

PNG, SVG oder WebP, bis 5 MB.

+
+
+ + {#if logoError} +

{logoError}

+ {/if} +
+ + + + {#if logoTeam?.logo} + + {/if} + + +
+
diff --git a/frontend/static/pedaler.svg b/frontend/static/pedaler.svg new file mode 100644 index 0000000..8e9c4e4 --- /dev/null +++ b/frontend/static/pedaler.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +