diff --git a/TODO.md b/TODO.md index cb9931e..0bd550e 100644 --- a/TODO.md +++ b/TODO.md @@ -36,6 +36,39 @@ Ein **bestehendes** fremdes Konto in ein Team zu holen braucht eine offenere `listRule` (z. B. `@request.auth.id != ""`). Das erlaubt allerdings, alle Konten der Instanz aufzulisten — deshalb bewusst nicht ohne Rücksprache. +### Ausfahrten: geplant, noch nicht gebaut + +Ausfahrten werden **nicht** zu einem zweiten Konzept neben Events, sondern +Events bekommen Schalter. Ein Bikepark-Ausflug mit gestoppten Läufen ist beides +zugleich; eine Trennung wäre willkürlich und würde Datum, Rechte und Oberfläche +doppelt führen. + +Geplant, in dieser Reihenfolge: + +1. **`events.timing`** (bool). Runs und Zeitnahme erscheinen nur, wenn gesetzt. + Der wöchentliche Stammtisch braucht sie nicht. +2. **Teilnahme** als eigene Collection `event_participants` (Event, Person, + Status, Zeitpunkt) — nicht als Liste am Event, sonst hängen Status und + Zeitpunkt nirgends, und Gäste ohne Konto passen nicht hinein. + + `events.participation` steuert, wie man hineinkommt: + + | Wert | Bedeutung | + |---|---| + | `offen` | Jedes Teammitglied trägt sich selbst ein | + | `bestaetigung` | Anmeldung möglich, Ersteller oder Team-Admin bestätigt | + | `geschlossen` | Nur Ersteller oder Team-Admin trägt Teilnehmer ein | + +3. **Serien** (`event_series`): Regel, Zeitraum und Vorlage; daraus entstehen + echte Einzeltermine. Bewusst nicht ein Datensatz mit Wiederholungsregel, aus + dem Termine nur gerechnet werden — sobald „wer kommt am 12.?" oder „diese + Woche fällt aus" beantwortet werden soll, braucht jeder Termin einen eigenen + Datensatz. + +Offen: Sind Teilnehmer **Nutzerkonten** oder **Fahrer**? Für den Stammtisch +wären es Konten, für eine gemeinsame Ausfahrt eher Fahrer (die es auch ohne +Konto gibt). Beides zugleich geht, macht die Collection aber breiter. + ## Vor dem nächsten Deploy ### `PUBLIC_CARTO_API_KEY` in Coolify setzen @@ -50,13 +83,14 @@ Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar. ### Migrationen greifen beim nächsten Start -Fünf neue Migrationen liegen in `backend/pb_migrations/`: +Sechs neue Migrationen liegen in `backend/pb_migrations/`: - `1754500500_riders_user_and_admin_rules.js` - `1754500600_events_dates_instead_of_status.js` - `1754500700_trail_status_meta.js` - `1754500800_trail_markers_custom.js` - `1754500900_users_superadmin.js` +- `1754501000_teams_logo.js` Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln aufbaut. Am deutlichsten beim Anlegen eines Mitglieds oder eines Fahrer-Logins: @@ -85,6 +119,13 @@ cd frontend && npm run generate-pocketbase-types Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis des Generators. +### 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 - Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js index 6848b17..7681761 100644 --- a/backend/pb_migrations/1754400000_init_schema.js +++ b/backend/pb_migrations/1754400000_init_schema.js @@ -334,6 +334,27 @@ migrate((app) => { "system": false, "type": "relation" }, + { + "help": "", + "hidden": false, + "id": "file_team_logo", + "maxSelect": 1, + "maxSize": 5242880, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "logo", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, { "hidden": false, "id": "autodate2990389176", diff --git a/backend/pb_migrations/1754501000_teams_logo.js b/backend/pb_migrations/1754501000_teams_logo.js new file mode 100644 index 0000000..465eb19 --- /dev/null +++ b/backend/pb_migrations/1754501000_teams_logo.js @@ -0,0 +1,38 @@ +/// + +// Jedes Team bringt sein eigenes Logo mit. +// +// Bisher steckte das Wappen fest in der Anwendung (static/schildl.svg und +// static/radl.svg). Für den Stammtisch Hersbruck stimmt das, für jedes +// weitere Team wäre es fremdes Vereinswappen. Ohne hinterlegte Datei bleibt +// die bisherige Darstellung als Rückfalloption bestehen. +// +// 5 MB, weil die vorhandenen Wappen als Inkscape-SVG je rund 2,5 MB wiegen. + +migrate((app) => { + const teams = app.findCollectionByNameOrId('teams') + + if (!teams.fields.getByName('logo')) { + teams.fields.add(new Field({ + id: 'file_team_logo', + name: 'logo', + type: 'file', + maxSelect: 1, + maxSize: 5242880, + mimeTypes: [ + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/gif', + 'image/webp', + ], + })) + } + + app.save(teams) +}, (app) => { + const teams = app.findCollectionByNameOrId('teams') + + teams.fields.removeByName('logo') + app.save(teams) +}) diff --git a/frontend/src/lib/components/Logo.svelte b/frontend/src/lib/components/Logo.svelte index bf740d7..b58ef14 100644 --- a/frontend/src/lib/components/Logo.svelte +++ b/frontend/src/lib/components/Logo.svelte @@ -1,10 +1,28 @@ diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts index 4bb9790..498748e 100644 --- a/frontend/src/lib/stores/teams.svelte.ts +++ b/frontend/src/lib/stores/teams.svelte.ts @@ -112,6 +112,20 @@ export class TeamStore { return await api.collection('teams').update(id, { name }) } + /** + * Logo setzen oder entfernen. Mit Datei als FormData — ein leeres Feld im + * FormData würde ein vorhandenes Bild löschen, deshalb der zweite Weg. + */ + async setLogo(id: string, file: File | null) { + if (!file) { + return await api.collection('teams').update(id, { logo: null }) + } + + const data = new FormData() + data.append('logo', file) + return await api.collection('teams').update(id, data) + } + /** Mitglied hinzufügen (Owner/Admin only) */ async addMember(teamId: string, userId: string) { const team = this.records.find((t) => t.id === teamId) diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts index f5f5bd9..6e488be 100644 --- a/frontend/src/lib/types.d.ts +++ b/frontend/src/lib/types.d.ts @@ -86,6 +86,7 @@ export type TeamsRecord = { admins?: RecordIdString[] created?: IsoDateString id: string + logo?: string name: string owner: RecordIdString updated?: IsoDateString diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 0e3adb4..2662b5a 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -101,15 +101,28 @@ {#if auth.user} -
-
+
+ + + +
- +
-
+
{@render children?.()}
diff --git a/frontend/src/routes/dashboard/settings/team/+page.svelte b/frontend/src/routes/dashboard/settings/team/+page.svelte index ed3b320..8dd40ec 100644 --- a/frontend/src/routes/dashboard/settings/team/+page.svelte +++ b/frontend/src/routes/dashboard/settings/team/+page.svelte @@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label' import * as Dialog from '@/components/ui/dialog' import Avatar from '$lib/components/Avatar.svelte' - import { Crown, LogOut, Plus, Shield, Trash2, UserMinus, UserPlus, Users } from 'lucide-svelte' + import Logo from '$lib/components/Logo.svelte' + import { Crown, Image, LogOut, Plus, Shield, Trash2, UserMinus, UserPlus, Users } from 'lucide-svelte' import type { TeamsResponse } from '$lib/types' const teams = getTeamContext() @@ -82,6 +83,35 @@ } } + // ---- Logo ---- + let logoDialog = $state(false) + let logoTeam = $state(null) + let logoFile = $state(null) + let logoBusy = $state(false) + let logoError = $state(null) + + function openLogo(t: TeamsResponse) { + logoTeam = t + logoFile = null + logoError = null + logoDialog = true + } + + async function saveLogo(file: File | null) { + if (!logoTeam) return + + logoBusy = true + logoError = null + try { + await teams.setLogo(logoTeam.id, file) + logoDialog = false + } catch (e: any) { + logoError = e.message ?? 'Das Logo konnte nicht gespeichert werden.' + } finally { + logoBusy = false + } + } + // ---- Mitglied anlegen ---- // Bestehende Konten lassen sich nicht suchen: users.listRule gibt nur den // eigenen Datensatz frei, und das soll auch so bleiben. Ein neues Konto @@ -175,16 +205,19 @@
-
- - {t.name} - {#if t.id === teams.activeId} - Aktiv - {/if} - - - {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} - +
+ +
+ + {t.name} + {#if t.id === teams.activeId} + Aktiv + {/if} + + + {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} + +
{#if t.id !== teams.activeId} @@ -196,6 +229,10 @@ + + {#if logoTeam?.logo} + + {/if} + + + +