project documentary
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# Datenmodell
|
||||
|
||||
## Überblick
|
||||
|
||||
Die Anwendung speichert fachliche Daten in Firestore und Song-Anhänge in Firebase Storage. Firestore-Dokumente werden über AngularFire als Live-Observables gelesen; Dokument-IDs werden in den Services als `id` in die Objekte übernommen.
|
||||
|
||||
Zentrale Collections:
|
||||
|
||||
- `users`
|
||||
- `songs`
|
||||
- `shows`
|
||||
- `guest`
|
||||
- `global`
|
||||
|
||||
Subcollections:
|
||||
|
||||
- `songs/{songId}/files`
|
||||
- `shows/{showId}/songs`
|
||||
|
||||
## `users/{uid}`
|
||||
|
||||
Benutzerprofile ergänzen Firebase Auth um Anwendungsdaten:
|
||||
|
||||
- `id`: Firestore-Dokument-ID, entspricht der Firebase-Auth-UID.
|
||||
- `name`: Anzeigename.
|
||||
- `role`: Semikolon-getrennte Rollenliste.
|
||||
- `chordMode`: bevorzugte Akkorddarstellung.
|
||||
- `songUsage`: Map von `songId` auf Nutzungszähler.
|
||||
|
||||
`songUsage` wird beim Hinzufügen oder Entfernen von Show-Songs inkrementell aktualisiert. Die Admin-Migration `rebuildSongUsage` kann die Werte aus Shows neu berechnen.
|
||||
|
||||
## `songs/{songId}`
|
||||
|
||||
Ein Song enthält Stammdaten, musikalische Angaben, rechtliche Angaben und Bearbeitungshistorie:
|
||||
|
||||
- `number`, `title`, `type`, `status`
|
||||
- `key`, `tempo`, `text`, `flags`, `final`, `comment`
|
||||
- `legalType`, `legalOwner`, `legalOwnerId`
|
||||
- `artist`, `label`, `termsOfUse`, `origin`
|
||||
- `edits`: Liste aus Bearbeitername und Timestamp
|
||||
|
||||
Typen:
|
||||
|
||||
- `type`: `Praise`, `Worship` oder `Misc`.
|
||||
- `status`: `draft`, `set` oder `final`.
|
||||
- `legalOwner`: `CCLI` oder `other`.
|
||||
- `legalType`: `open` oder `allowed`.
|
||||
|
||||
Beim Aktualisieren eines Songs ergänzt `SongService` einen Eintrag in `edits`.
|
||||
|
||||
## `songs/{songId}/files/{fileId}`
|
||||
|
||||
Die Subcollection enthält Metadaten zu Dateien in Firebase Storage:
|
||||
|
||||
- `name`: Dateiname.
|
||||
- `path`: Storage-Verzeichnis des Songs.
|
||||
- `createdAt`: Erstellzeitpunkt.
|
||||
|
||||
`UploadService` lädt Dateien nach Firebase Storage hoch und schreibt anschließend das Metadaten-Dokument. `FileService` erzeugt Download-URLs und entfernt Dateien aus Storage und Firestore.
|
||||
|
||||
## `shows/{showId}`
|
||||
|
||||
Shows beschreiben Veranstaltungen und den Präsentationszustand:
|
||||
|
||||
- `showType`: Veranstaltungstyp.
|
||||
- `date`: Firestore-Timestamp.
|
||||
- `owner`: Benutzer-ID des Besitzers.
|
||||
- `songIds`: denormalisierter Index der enthaltenen Ursprungssongs.
|
||||
- `public`: Kennzeichen für öffentliche Show-Typen.
|
||||
- `reportedType`: `null`, `pending`, `reported` oder `not-required`.
|
||||
- `published`: Veröffentlichung in Listen.
|
||||
- `archived`: Archivstatus.
|
||||
- `order`: Reihenfolge der Show-Song-Dokument-IDs.
|
||||
- `shareId`: Verweis auf ein Gastfreigabe-Dokument.
|
||||
|
||||
Präsentationsfelder:
|
||||
|
||||
- `presentationSongId`
|
||||
- `presentationDynamicCaption`
|
||||
- `presentationDynamicText`
|
||||
- `presentationSection`
|
||||
- `presentationZoom`
|
||||
- `presentationBackground`
|
||||
|
||||
`presentationBackground` erlaubt `none`, `blue`, `green`, `leder`, `praise` und `bible`.
|
||||
|
||||
`ShowService` unterscheidet öffentliche und private Show-Typen. Beim Anlegen werden `owner`, leere `order`, leere `songIds` und `public` aus dem Show-Typ berechnet.
|
||||
|
||||
## `shows/{showId}/songs/{showSongId}`
|
||||
|
||||
Show-Songs sind Snapshots von Songs innerhalb einer Show. Sie erweitern das Song-Modell um showbezogene Felder:
|
||||
|
||||
- `songId`: ID des Ursprungssongs.
|
||||
- `key`: aktuelle Tonart in der Show.
|
||||
- `keyOriginal`: ursprüngliche Tonart.
|
||||
- `chordMode`: Akkorddarstellung des Benutzers zum Zeitpunkt des Hinzufügens.
|
||||
- `addedLive`: Kennzeichen für live hinzugefügte Songs.
|
||||
|
||||
Beim Hinzufügen kopiert `ShowSongService` die aktuellen Songdaten in die Subcollection, aktualisiert `users/{uid}.songUsage` und fügt die Ursprungssong-ID zu `shows/{showId}.songIds` hinzu. Beim Entfernen wird die Reihenfolge angepasst, der Nutzungszähler reduziert und `songIds` entfernt, wenn kein weiterer Show-Song denselben Ursprungssong nutzt.
|
||||
|
||||
## `guest/{guestId}`
|
||||
|
||||
Gastfreigaben sind öffentlich lesbare Snapshots einer Show:
|
||||
|
||||
- `showType`
|
||||
- `date`
|
||||
- `updatedAt`
|
||||
- `songs`: Liste der freigegebenen Songs
|
||||
|
||||
`GuestShowService` erstellt oder aktualisiert das Gastdokument und schreibt die erzeugte `shareId` zurück an die Show. Die öffentliche URL hat das Format `/guest/{shareId}`.
|
||||
|
||||
## `global/config`
|
||||
|
||||
Globale Konfiguration:
|
||||
|
||||
- `ccliLicenseId`: CCLI-Lizenznummer.
|
||||
|
||||
`ConfigService` liest das Dokument gecacht.
|
||||
|
||||
## `global/static`
|
||||
|
||||
Laufzeitstatus der Anwendung:
|
||||
|
||||
- `currentShow`: ID der aktuell ausgewählten Show für die Präsentation.
|
||||
|
||||
`GlobalSettingsService` liest und schreibt dieses Dokument. Die Präsentationsauswahl setzt `currentShow`; Remote- und Monitoransicht verwenden den Wert zur Synchronisierung.
|
||||
|
||||
## Denormalisierung und Migrationen
|
||||
|
||||
Zwei Felder sind bewusst denormalisiert:
|
||||
|
||||
- `users/{uid}.songUsage` für Nutzungszähler pro Benutzer.
|
||||
- `shows/{showId}.songIds` für schnelle Abfragen und Anzeigeinformationen zu verwendeten Songs.
|
||||
|
||||
Für beide Datenbestände existieren Admin-Migrationen im README. Sie sollten nach strukturellen Korrekturen oder historischen Datenproblemen manuell ausgeführt werden.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Entwicklungsworkflow
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Das Projekt ist eine Angular-Anwendung mit Node.js-Abhängigkeiten aus `package.json`. Der CI-Workflow verwendet `node:20-bullseye`; lokale Entwicklung sollte daher eine kompatible Node-Version verwenden.
|
||||
|
||||
Installiere Abhängigkeiten mit:
|
||||
|
||||
```bash
|
||||
npm ci -f
|
||||
```
|
||||
|
||||
Die Option `-f` entspricht dem Gitea-CI-Workflow.
|
||||
|
||||
## Lokale Konfiguration
|
||||
|
||||
Für lokale Builds muss `src/environments/firebase.ts` existieren. Die Datei wird nicht versioniert und enthält die Firebase-Projektwerte.
|
||||
|
||||
Minimalstruktur:
|
||||
|
||||
```ts
|
||||
export const firebase = {
|
||||
apiKey: '...',
|
||||
authDomain: '...',
|
||||
databaseURL: 'https://worshipgenerator.firebaseio.com',
|
||||
projectId: '...',
|
||||
storageBucket: '...',
|
||||
messagingSenderId: '...',
|
||||
appId: '...',
|
||||
};
|
||||
```
|
||||
|
||||
Ohne diese Datei schlagen Angular-Builds fehl, weil `environment.ts` und `environment.prod.ts` sie importieren.
|
||||
|
||||
## Entwicklungsserver
|
||||
|
||||
Starte die Anwendung lokal mit:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Der Befehl führt `ng serve` aus. In `angular.json` ist `development` die Standardkonfiguration für `serve`.
|
||||
|
||||
## Build-Varianten
|
||||
|
||||
Development-Build:
|
||||
|
||||
```bash
|
||||
npm run build:dev
|
||||
```
|
||||
|
||||
Production-Build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Der Production-Build nutzt `environment.prod.ts`, aktiviert Output-Hashing und schreibt nach `dist/wgenerator`. Firebase Hosting erwartet den Browser-Build unter `dist/wgenerator/browser`.
|
||||
|
||||
## Tests und Linting
|
||||
|
||||
Unit-Tests:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Linting mit automatischen Fixes:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Tests liegen als `*.spec.ts` neben den Implementierungen. Der Angular Unit-Test-Builder nutzt Vitest und lädt gemeinsame Testkonfiguration aus `src/test-vitest.ts`.
|
||||
|
||||
## Codeorganisation
|
||||
|
||||
Neue fachliche Funktionen sollten in das passende Feature-Modul unter `src/app/modules` einsortiert werden. Gemeinsame Services gehören nach `src/app/services`; wiederverwendbare UI-Komponenten, Pipes, Direktiven und Guards nach `src/app/widget-modules`.
|
||||
|
||||
Der bestehende Stil verwendet:
|
||||
|
||||
- TypeScript und Angular-Module mit lazy geladenen Feature-Modulen.
|
||||
- Standalone-Komponenten dort, wo sie bereits im Code genutzt werden.
|
||||
- LESS für Component-Styles.
|
||||
- RxJS-Observables für Firestore-Livedaten.
|
||||
- Services als fachliche Fassade über `DbService`.
|
||||
|
||||
## Datenzugriff in neuen Funktionen
|
||||
|
||||
Firestore-Zugriffe sollten über `DbService` oder vorhandene Fachservices erfolgen. Direkte AngularFire-Zugriffe sind nur sinnvoll, wenn der vorhandene Wrapper eine benötigte Operation nicht abbildet.
|
||||
|
||||
Bei neuen fachlichen Datenflüssen ist zu prüfen:
|
||||
|
||||
- ob ein Live-Observable oder ein einmaliger Promise-Zugriff benötigt wird,
|
||||
- ob `shareReplay` sinnvoll ist, um Listener wiederzuverwenden,
|
||||
- ob denormalisierte Felder wie `songUsage` oder `songIds` aktualisiert werden müssen,
|
||||
- ob Firestore-Regeln und UI-Rollen die neue Aktion angemessen abdecken.
|
||||
|
||||
## Rollen und UI-Zugriff
|
||||
|
||||
Neue geschützte Routen sollten `AuthGuard` und bei Bedarf `RoleGuard` mit `requiredRoles` nutzen. Sichtbare Aktionen in Templates sollten über `*appRole` und bei besitzabhängigen Aktionen über `*appOwner` gesteuert werden.
|
||||
|
||||
Da Firestore-Regeln für fachliche Collections nur Login voraussetzen, müssen rollenbasierte Einschränkungen in Routing, UI und Services konsistent umgesetzt werden.
|
||||
|
||||
## Deployment-Ablauf
|
||||
|
||||
Für ein reguläres Deployment:
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
Der Befehl baut die Production-Version und führt anschließend `firebase deploy` aus. Für einen Beta-Channel:
|
||||
|
||||
```bash
|
||||
npm run deploy-beta
|
||||
```
|
||||
|
||||
Vor Deployments sollten mindestens Build und relevante Tests lokal oder im CI erfolgreich sein.
|
||||
|
||||
## Manuelle Admin-Wartung
|
||||
|
||||
Das README beschreibt zwei manuelle Browser-Konsolenbefehle für Admins:
|
||||
|
||||
```js
|
||||
await window.wgeneratorAdmin.rebuildSongUsage()
|
||||
await window.wgeneratorAdmin.rebuildShowSongIds()
|
||||
```
|
||||
|
||||
Diese Migrationen sind read-heavy und sollten bewusst ausgeführt werden, wenn historische Daten nachgezogen oder Indizes rekonstruiert werden müssen.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Projektdokumentation
|
||||
|
||||
`wgenerator` ist eine Angular-Anwendung zur Verwaltung von Liedern, Veranstaltungen und Live-Präsentationen. Die Dokumentation ist nach fachlichen Seiten und technischen Querschnittsthemen getrennt, damit Änderungen an einer Route oder einem Modul gezielt nachvollzogen werden können.
|
||||
|
||||
## Einstieg
|
||||
|
||||
- [Technischer Aufbau](technical-architecture.md)
|
||||
- [Datenmodell](data-model.md)
|
||||
- [Rollen und Berechtigungen](roles-and-permissions.md)
|
||||
- [Betrieb und Konfiguration](operations.md)
|
||||
- [Entwicklung und Qualitätssicherung](development.md)
|
||||
|
||||
## Seitendokumentation
|
||||
|
||||
### Lieder
|
||||
|
||||
- [Liedliste](pages/songs-list.md)
|
||||
- [Lieddetails](pages/song-detail.md)
|
||||
- [Lied anlegen](pages/song-new.md)
|
||||
- [Lied bearbeiten](pages/song-edit.md)
|
||||
|
||||
### Veranstaltungen
|
||||
|
||||
- [Veranstaltungsliste](pages/shows-list.md)
|
||||
- [Veranstaltungsdetails](pages/show-detail.md)
|
||||
- [Veranstaltung anlegen](pages/show-new.md)
|
||||
- [Veranstaltung bearbeiten](pages/show-edit.md)
|
||||
|
||||
### Präsentation
|
||||
|
||||
- [Präsentation auswählen](pages/presentation-select.md)
|
||||
- [Präsentationssteuerung](pages/presentation-remote.md)
|
||||
- [Präsentationsmonitor](pages/presentation-monitor.md)
|
||||
|
||||
### Benutzer
|
||||
|
||||
- [Login](pages/user-login.md)
|
||||
- [Benutzerprofil](pages/user-info.md)
|
||||
- [Benutzer anlegen](pages/user-new.md)
|
||||
- [Passwort zurücksetzen](pages/user-password.md)
|
||||
- [Passwort-E-Mail gesendet](pages/user-password-send.md)
|
||||
- [Logout](pages/user-logout.md)
|
||||
|
||||
### Öffentlich zugängliche Seiten
|
||||
|
||||
- [Branding](pages/brand.md)
|
||||
- [Neue Benutzer ohne Rollen](pages/brand-new-user.md)
|
||||
- [Gastansicht](pages/guest.md)
|
||||
|
||||
## Fachlicher Ablauf
|
||||
|
||||
Lieder werden in der Lieddatenbank gepflegt. Eine Leitung erstellt daraus Veranstaltungen, sortiert Songs, veröffentlicht Abläufe und teilt optional eine Gastansicht. Das Präsentationsteam wählt eine aktuelle Veranstaltung aus und steuert Folien, Hintergründe, freien Text und Zoom live über die Remote-Seite. Der Monitor liest diese Zustände in Echtzeit und rendert die Ausgabe für Beamer oder Display.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Betrieb und Konfiguration
|
||||
|
||||
## Lokale Einrichtung
|
||||
|
||||
Abhängigkeiten werden wie im CI-Workflow installiert:
|
||||
|
||||
```bash
|
||||
npm ci -f
|
||||
```
|
||||
|
||||
Der lokale Entwicklungsserver startet mit:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
`npm start` führt `ng serve` aus. Die Standard-Serve-Konfiguration ist `development`.
|
||||
|
||||
## Firebase-Konfiguration
|
||||
|
||||
`src/environments/environment.ts` und `src/environments/environment.prod.ts` importieren beide `src/environments/firebase.ts`. Diese Datei ist nicht versioniert und muss lokal vorhanden sein. In CI wird sie aus Secrets erzeugt.
|
||||
|
||||
Beispielstruktur:
|
||||
|
||||
```ts
|
||||
export const firebase = {
|
||||
apiKey: '...',
|
||||
authDomain: '...',
|
||||
databaseURL: 'https://worshipgenerator.firebaseio.com',
|
||||
projectId: '...',
|
||||
storageBucket: '...',
|
||||
messagingSenderId: '...',
|
||||
appId: '...',
|
||||
};
|
||||
```
|
||||
|
||||
Die Anwendung nutzt `environment.url` für Rücksprung-URLs, zum Beispiel beim Passwort-Reset. Aktuell zeigt sowohl Development als auch Production auf `https://worshipgenerator.web.app`.
|
||||
|
||||
## Firebase Hosting und Emulatoren
|
||||
|
||||
`firebase.json` veröffentlicht den Production-Build aus `dist/wgenerator/browser`. Alle Routen werden per Rewrite auf `/index.html` geleitet, damit Angular-Routing direkt auf Hosting funktioniert.
|
||||
|
||||
Cache-Verhalten:
|
||||
|
||||
- HTML-/SPA-Routen ohne Dateiendung: `Cache-Control: max-age=1`.
|
||||
- JS- und CSS-Dateien: `Cache-Control: max-age=846000`.
|
||||
|
||||
Konfigurierte Emulator-Ports:
|
||||
|
||||
- Firestore: `8080`
|
||||
- Realtime Database: `9000`
|
||||
- Hosting: `5000`
|
||||
|
||||
## Build, Test und Lint
|
||||
|
||||
Wichtige Befehle:
|
||||
|
||||
```bash
|
||||
npm run build:dev
|
||||
npm run build
|
||||
npm test
|
||||
npm run lint
|
||||
```
|
||||
|
||||
- `npm run build:dev` baut mit Development-Konfiguration, ohne Optimierung und mit Source Maps.
|
||||
- `npm run build` baut die Production-Version mit File-Replacement auf `environment.prod.ts` und Output-Hashing.
|
||||
- `npm test` führt Unit-Tests mit Vitest aus.
|
||||
- `npm run lint` führt Angular ESLint aus und wendet automatische Fixes an.
|
||||
|
||||
Production-Budgets sind in `angular.json` definiert: initiale Warnung ab `500kB`, Fehler ab `10MB`; Component-Styles warnen ab `40kB` und schlagen ab `80kB` fehl.
|
||||
|
||||
## CI
|
||||
|
||||
Der Gitea-Workflow `.gitea/workflows/angular-build.yml` läuft bei Pushes auf den Branch `dev` in einem `node:20-bullseye`-Container.
|
||||
|
||||
Die Pipeline:
|
||||
|
||||
1. checkt den Code aus,
|
||||
2. installiert Abhängigkeiten mit `npm ci -f`,
|
||||
3. erzeugt `src/environments/firebase.ts` aus Secrets,
|
||||
4. führt `npm run build` aus.
|
||||
|
||||
Die Pipeline deployt nicht automatisch.
|
||||
|
||||
## Deployment
|
||||
|
||||
Production-Build:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Deployment nach Firebase:
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
Beta-Deployment in einen Firebase-Hosting-Channel:
|
||||
|
||||
```bash
|
||||
npm run deploy-beta
|
||||
```
|
||||
|
||||
`npm run deploy` baut zuerst die Production-Version und führt anschließend `firebase deploy` aus. Für lokale Deployments muss die Firebase CLI authentifiziert sein und Zugriff auf das Zielprojekt besitzen.
|
||||
|
||||
## Manuelle Migrationen
|
||||
|
||||
Das README dokumentiert zwei Admin-Migrationen, die im Browser mit einem angemeldeten Admin-Benutzer über `window.wgeneratorAdmin` ausgeführt werden:
|
||||
|
||||
```js
|
||||
await window.wgeneratorAdmin.rebuildSongUsage()
|
||||
await window.wgeneratorAdmin.rebuildShowSongIds()
|
||||
```
|
||||
|
||||
`rebuildSongUsage` rekonstruiert die Song-Nutzungszähler unter `users/{uid}.songUsage` aus nicht archivierten Shows und deren Show-Songs.
|
||||
|
||||
`rebuildShowSongIds` rekonstruiert den `songIds`-Index auf Show-Dokumenten aus `shows/{showId}/songs`.
|
||||
|
||||
Beide Migrationen lesen viele Dokumente und sind als manuelle Wartungsoperationen gedacht.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Neuer Benutzer ohne Rollen
|
||||
|
||||
## Route
|
||||
|
||||
`/brand/new-user`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite begrüßt neu registrierte Benutzer, die noch keine Berechtigungen erhalten haben. Sie macht sichtbar, dass der Account existiert, aber noch administrative Freischaltung benötigt.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `NewUserComponent` liest den aktuellen Benutzer über `UserService.user$`.
|
||||
- Die angezeigte Person stammt aus dem Firestore-Dokument `users/{uid}`.
|
||||
|
||||
## UI
|
||||
|
||||
Die Ansicht zeigt `BrandComponent`, die Begrüßung `WILLKOMMEN`, den Namen des aktuellen Benutzers und den Hinweis, dass noch keine Berechtigungen zugeteilt wurden.
|
||||
|
||||
## Aktionen
|
||||
|
||||
Die Seite bietet keine eigene Aktion. Der nächste fachliche Schritt ist die Rollenvergabe durch einen Administrator auf `/user/info`.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Route ist öffentlich konfiguriert, zeigt den personalisierten Inhalt aber nur, wenn `UserService.user$` einen Benutzer liefert. Sie ist für angemeldete Benutzer ohne Rollen gedacht.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Nach erfolgreicher Selbstregistrierung navigiert `UserSessionService.createNewUser` zu dieser Route.
|
||||
- Die Seite prüft Rollen nicht selbst und vergibt keine Rechte.
|
||||
- Wenn kein Benutzer aus `user$` kommt, wird nur das Branding angezeigt.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Branding
|
||||
|
||||
## Route
|
||||
|
||||
`/brand`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite zeigt eine einfache Markenansicht mit Logo und Copyright-Hinweis.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
Die Seite lädt keine fachlichen Daten. `BrandComponent` rendert nur statische Inhalte und `LogoComponent`.
|
||||
|
||||
## UI
|
||||
|
||||
Die Ansicht besteht aus dem Logo und dem Copyright-Text `© 2019 - 2026 - Benjamin Ifland`.
|
||||
|
||||
## Aktionen
|
||||
|
||||
Die Seite bietet keine interaktiven Aktionen.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Seite ist öffentlich erreichbar und hat keinen Guard.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- `BrandComponent` ist als Standalone-Import im `BrandModule` eingebunden.
|
||||
- Die Komponente wird in `/brand/new-user` wiederverwendet, damit der Freischaltungszustand dieselbe Markenansicht nutzt.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Gastansicht
|
||||
|
||||
## Route
|
||||
|
||||
`/guest/:id`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Gastansicht zeigt eine öffentlich geteilte Version einer Veranstaltung. Sie richtet sich an externe Personen ohne Login und stellt Veranstaltungstyp, Datum und Liedtexte bereit.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `GuestComponent` liest die Routen-ID aus `ActivatedRoute.params`.
|
||||
- `GuestShowDataService.read(id)` lädt den initialen Datensatz aus der Firestore-Collection `guest`.
|
||||
- `GuestShowDataService.read$(id)` abonniert anschließend Live-Updates für denselben Datensatz.
|
||||
- `GuestShowService.share(show, songs)` erstellt oder aktualisiert Gastdatensätze aus einer Veranstaltung und erzeugt die URL `/guest/{shareId}`.
|
||||
|
||||
## UI
|
||||
|
||||
Die geladene Veranstaltung zeigt links den übersetzten Veranstaltungstyp und rechts das Datum im Format `dd.MM.yyyy`. Die Songs werden in einem Swiper dargestellt. Jede Folie enthält Titel, optional Künstler und den Liedtext über `SongTextComponent`.
|
||||
|
||||
Für Lade-, Fehler- und Nicht-gefunden-Zustände zeigt die Seite einfache Meldungen.
|
||||
|
||||
## Aktionen
|
||||
|
||||
Die Gastseite selbst bietet keine Bearbeitungsaktionen. Benutzer können durch die Song-Folien wischen; Aktualisierungen am geteilten Datensatz erscheinen über das Live-Abonnement.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Route ist öffentlich erreichbar und nicht durch Authentifizierung geschützt. Zugriffsschutz erfolgt ausschließlich über die Kenntnis der Share-ID in der URL.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Der Zustand der Seite ist als `GuestShowState` modelliert: `loading`, `loaded`, `not-found` oder `error`.
|
||||
- Datumswerte werden normalisiert und unterstützen `Date`, Firestore-Timestamps mit `toDate`, Objekte mit `seconds`, Strings und Zahlen.
|
||||
- Falls der initiale Ladevorgang keinen Datensatz findet, wird `not-found` angezeigt.
|
||||
- Fehler beim initialen Laden oder bei Live-Aktualisierungen führen zu einer Fehlermeldung.
|
||||
- `ensureSwiperElement()` registriert das Web Component für den Swiper.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Seite: Präsentationsmonitor
|
||||
|
||||
## Route
|
||||
|
||||
`/presentation/monitor`
|
||||
|
||||
Die Route öffnet die Vollbildausgabe für Beamer oder Display. Sie wird typischerweise über den Button `Präsentation starten` auf `/presentation/remote` geöffnet.
|
||||
|
||||
## Zweck
|
||||
|
||||
Der Monitor rendert den aktuellen Präsentationszustand der aktiven Show. Er zeigt Titelfolie, leere Fläche, freien Text oder Liedabschnitte und ergänzt bei Liedern die rechtlichen Angaben.
|
||||
|
||||
## Datenfluss
|
||||
|
||||
Die `MonitorComponent` liest `global/static.currentShow` über `GlobalSettingsService.get$`. Aus dieser ID lädt sie die Show über `ShowService.read$(showId)` und übernimmt daraus die Live-Felder für Anzeigezustand, Datum, Veranstaltungstyp, Zoom, Hintergrund und freien Text.
|
||||
|
||||
Für Song-Inhalte beobachtet der Monitor `presentationSongId`. Bei `title`, `dynamicText` oder fehlender Song-ID wird kein Song gesetzt. Bei anderen Werten lädt er den passenden Show-Song über `ShowSongService.read$(showId, presentationSongId)`.
|
||||
|
||||
Konfigurationsdaten für rechtliche Hinweise kommen aus `ConfigService.get$()`. Die Komponente `app-legal` nutzt diese Daten unter anderem für die CCLI-Lizenznummer.
|
||||
|
||||
## UI-Anzeige
|
||||
|
||||
Beim Initialisieren ruft der Monitor `openFullscreen()` auf. Die Anzeige liegt als fixierte Vollbildfläche über der Anwendung und blendet den Mauszeiger aus.
|
||||
|
||||
Der Monitor kennt folgende Zustände:
|
||||
|
||||
- `title`: zeigt den übersetzten Veranstaltungstyp und das Datum.
|
||||
- `empty`: zeigt die leere Präsentationsfläche mit Logo beziehungsweise Hintergrund.
|
||||
- `dynamicText`: zeigt `presentationDynamicCaption` und `presentationDynamicText`.
|
||||
- Show-Song-ID: rendert `app-song-text` mit Songtitel, Liedtext und aktivem Abschnitt.
|
||||
|
||||
Bei Songs werden Akkorde ausgeblendet (`chordMode="hide"`), Kommentare nicht angezeigt und die interne Abschnittsnavigation des Song-Text-Widgets deaktiviert. Der aktive Abschnitt kommt aus `presentationSection`.
|
||||
|
||||
## Zusammenspiel von Remote und Monitor
|
||||
|
||||
Der Monitor ist ein reiner Live-Reader. Alle Bedienhandlungen erfolgen in der Remote-Steuerung; der Monitor reagiert auf die Felder, die dort auf der aktiven Show gespeichert werden.
|
||||
|
||||
Wenn die Remote `presentationSongId`, `presentationSection`, `presentationZoom`, `presentationBackground`, `presentationDynamicCaption` oder `presentationDynamicText` ändert, aktualisiert der Monitor seine Darstellung über die abonnierten Datenströme. Beim Wechsel der aktiven Show über `/presentation/select` liest der Monitor die neue `currentShow` aus den globalen Settings und richtet die Ausgabe darauf aus.
|
||||
|
||||
## Live-Felder auf der Show
|
||||
|
||||
Der Monitor wertet folgende Felder aus:
|
||||
|
||||
- `presentationSongId`: entscheidet über Titelfolie, Leerseite, freien Text oder Songanzeige.
|
||||
- `presentationSection`: bestimmt den aktiven Abschnitt im Songtext.
|
||||
- `presentationDynamicCaption`: Überschrift für den freien Text.
|
||||
- `presentationDynamicText`: Inhalt für den freien Text; Zeilenumbrüche bleiben durch die Monitor-Styles erhalten.
|
||||
- `presentationZoom`: wird als Pixelwert auf die Vollbildfläche gesetzt.
|
||||
- `presentationBackground`: schaltet die Hintergrundgrafik.
|
||||
|
||||
Zusätzlich nutzt er `showType` und `date` für die Titelfolie.
|
||||
|
||||
## Hintergründe
|
||||
|
||||
`presentationBackground` unterstützt diese Werte:
|
||||
|
||||
- `none`: schwarzer Hintergrund ohne Bild.
|
||||
- `blue`: `assets/bg-dark-blue.jpg`
|
||||
- `green`: `assets/bg-dark-green.jpg`
|
||||
- `leder`: `assets/bg-leder.jpg`
|
||||
- `praise`: `assets/bg-praise.jpg`
|
||||
- `bible`: `assets/bg-bible.jpg`
|
||||
|
||||
Die Bildhintergründe werden vollflächig gerendert, weich eingeblendet und je nach Motiv mit Blur und reduzierter Deckkraft versehen. Bei aktivem Bildhintergrund wird das Logo in Songzuständen vollständig ausgeblendet.
|
||||
|
||||
## Rechtliche Angaben
|
||||
|
||||
Bei Songanzeige rendert `app-legal` die verfügbaren Metadaten des Songs:
|
||||
|
||||
- Künstler
|
||||
- Label
|
||||
- Nutzungsbedingungen
|
||||
- Herkunft
|
||||
- Liednummer beziehungsweise CCLI-Liednummer
|
||||
|
||||
Wenn `song.legalOwner` den Wert `CCLI` hat und Konfiguration geladen ist, wird zusätzlich die CCLI-Lizenznummer aus `ConfigService` angezeigt.
|
||||
|
||||
## Technische Besonderheiten
|
||||
|
||||
Songwechsel werden um 600 ms verzögert. Vor dem Umschalten setzt die Komponente intern `songId` auf `empty`, damit die `songSwitch`-Animation sauber zwischen den Zuständen wechseln kann. Ein laufender Timeout wird beim nächsten Wechsel oder beim Zerstören der Komponente abgebrochen.
|
||||
|
||||
Die Show-Daten werden mit `shareReplay(1)` geteilt, damit die separaten Streams für allgemeine Präsentationsdaten und Song-Ladevorgänge dieselbe Show-Quelle verwenden. Subscriptions werden über `destroy$` beendet.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Seite: Präsentationssteuerung
|
||||
|
||||
## Route
|
||||
|
||||
`/presentation/remote`
|
||||
|
||||
Dies ist die Standardseite des Presentation-Moduls. Ein Aufruf von `/presentation` leitet auf diese Route weiter.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Remote-Seite ist die Live-Bedienoberfläche für das Präsentationsteam. Sie bestimmt, welche Folie der Monitor zeigt, welche Hintergrundgrafik aktiv ist, wie groß der Text dargestellt wird und welcher freie Text live eingeblendet werden soll.
|
||||
|
||||
## Datenfluss
|
||||
|
||||
Die `RemoteComponent` liest zuerst `global/static.currentShow` über `GlobalSettingsService.get$`. Sobald eine aktuelle Show vorhanden ist, lädt sie:
|
||||
|
||||
- die Show über `ShowService.read$(showId)`
|
||||
- die zugeordneten Show-Songs über `ShowSongService.list$(showId)`
|
||||
- die verfügbaren Songs für das Live-Hinzufügen über `SongService.list$()`
|
||||
|
||||
Die Show-Songs werden mit `TextRenderingService.parse(song.text, null, false)` in Präsentationsabschnitte zerlegt. Für die Anzeige nutzt die Remote nur die Song-IDs, Titel und geparsten Abschnitte. Die sichtbare Reihenfolge folgt `show.order`; Songs, die nicht in dieser Reihenfolge stehen, werden in der Abschnittsliste nicht gerendert.
|
||||
|
||||
Änderungen schreibt die Remote direkt auf das aktuelle Show-Dokument. Freier Text wird über Subjects gesammelt und mit `debounceTime(1000)` verzögert gespeichert, damit beim Tippen nicht jeder Tastendruck sofort einen Schreibvorgang auslöst.
|
||||
|
||||
## UI-Bedienung
|
||||
|
||||
Oben zeigt die Karte den Veranstaltungstyp und das Datum der aktiven Show. Über das Ordner-Symbol gelangt man zur Auswahlseite `/presentation/select`.
|
||||
|
||||
Die Bedienfläche enthält feste Ziele und die nach Show-Reihenfolge sortierten Songs:
|
||||
|
||||
- `Veranstaltung`: setzt `presentationSongId` auf `title` und zeigt auf dem Monitor die Titelfolie.
|
||||
- `Leer`: setzt `presentationSongId` auf `empty` und zeigt eine leere Präsentationsfläche.
|
||||
- Songtitel: setzt die Song-ID mit `presentationSection: -1`; der Monitor lädt den Song, ohne einen konkreten Abschnitt auszuwählen.
|
||||
- Songabschnitt: setzt die Song-ID und den Abschnittsindex. Angezeigt werden Abschnittstyp, Abschnittsnummer und die erste Textzeile des Abschnitts.
|
||||
- `Freier Text`: setzt `presentationSongId` auf `dynamicText`. Überschrift und Text werden über die Eingabefelder darunter gepflegt.
|
||||
|
||||
Am unteren Rand befinden sich die Live-Einstellungen:
|
||||
|
||||
- `Präsentation starten` öffnet `/presentation/monitor`.
|
||||
- `Hintergrund` setzt `presentationBackground`.
|
||||
- Der Slider setzt `presentationZoom` im Bereich von 10 bis 100 in Schritten von 2.
|
||||
|
||||
Zusätzlich bindet die Seite `app-add-song` mit `addedLive="true"` ein. Damit können Songs während der Präsentation zur Show hinzugefügt werden; die Remote bekommt die aktualisierte Show-Song-Liste anschließend über die Live-Datenströme.
|
||||
|
||||
## Zusammenspiel von Remote und Monitor
|
||||
|
||||
Remote und Monitor kommunizieren nicht direkt miteinander. Die Remote schreibt den gewünschten Präsentationszustand auf die aktive Show, der Monitor liest dieselben Felder live und rendert daraus die Ausgabe.
|
||||
|
||||
Für den Betrieb bedeutet das:
|
||||
|
||||
- Die Auswahlseite setzt `global/static.currentShow`.
|
||||
- Die Remote liest diese Show, zeigt deren Songs und schreibt Präsentationsbefehle auf das Show-Dokument.
|
||||
- Der Monitor liest dieselbe Show und aktualisiert seine Anzeige, sobald sich die Live-Felder ändern.
|
||||
|
||||
Dadurch kann die Remote auf einem Bediengerät laufen, während der Monitor auf einem Beamer- oder Display-Gerät geöffnet ist.
|
||||
|
||||
## Live-Felder auf der Show
|
||||
|
||||
Die Remote pflegt folgende Felder aus `Show`:
|
||||
|
||||
- `presentationSongId`: aktiver Inhalt. Besondere Werte sind `title`, `empty` und `dynamicText`; ansonsten enthält das Feld eine Show-Song-ID.
|
||||
- `presentationSection`: Index des aktiven Liedabschnitts. Bei Titeln, Leerseite, freiem Text oder Songtitel-Auswahl wird `-1` verwendet.
|
||||
- `presentationDynamicCaption`: Überschrift des freien Textes.
|
||||
- `presentationDynamicText`: Inhalt des freien Textes; Zeilenumbrüche werden vom Monitor erhalten.
|
||||
- `presentationZoom`: Schriftgröße für den Monitor.
|
||||
- `presentationBackground`: Hintergrundmodus. Unterstützt sind `none`, `blue`, `green`, `leder`, `praise` und `bible`.
|
||||
|
||||
## Technische Besonderheiten
|
||||
|
||||
Die Komponente nutzt `ChangeDetectionStrategy.OnPush` und ruft nach Live-Updates `markForCheck()` auf. Subscriptions werden über `destroy$` mit `takeUntil` beendet.
|
||||
|
||||
Der aktive Show-Kontext wird aus `GlobalSettingsService` abgeleitet und mit `distinctUntilChanged()` stabilisiert. Sobald sich `currentShow` ändert, laden Show und Show-Songs automatisch neu.
|
||||
|
||||
Die Remote ist tastaturbedienbar: auswählbare Folien reagieren neben Klicks auch auf `Enter` und `Space` und sind als `role="button"` mit `tabindex="0"` ausgezeichnet.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Seite: Präsentation auswählen
|
||||
|
||||
## Route
|
||||
|
||||
`/presentation/select`
|
||||
|
||||
Die Route gehört zum Presentation-Modul. Der leere Presentation-Pfad leitet auf `/presentation/remote` weiter; die Auswahlseite wird über den Ordner-Button der Remote-Steuerung geöffnet.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite legt fest, welche Veranstaltung live präsentiert wird. Sie ist der Einstiegspunkt, wenn das Präsentationsteam vor oder während einer Veranstaltung auf eine andere Show wechseln muss.
|
||||
|
||||
## Datenfluss
|
||||
|
||||
Die `SelectComponent` lädt Shows über `ShowService.list$(true)`. Dadurch werden veröffentlichte Veranstaltungen angezeigt; eigene unveröffentlichte Shows werden in diesem Modus nicht zusätzlich eingeblendet. Anschließend filtert die Komponente clientseitig auf Shows, deren Datum neuer ist als ein Monat vor dem aktuellen Zeitpunkt.
|
||||
|
||||
Die Liste wird nach Datum sortiert und zeigt pro Eintrag den Besitzer, den übersetzten Veranstaltungstyp und das Datum. Bei der Auswahl einer Show passieren zwei Schreibvorgänge:
|
||||
|
||||
- `GlobalSettingsService.set({currentShow: show.id})` schreibt die aktive Show in das globale Dokument `global/static`.
|
||||
- `ShowService.update$(show.id, {presentationSongId: 'title'})` setzt die Präsentation der gewählten Show auf die Titelfolie.
|
||||
|
||||
Danach navigiert die Seite nach `/presentation/remote`.
|
||||
|
||||
## UI-Bedienung
|
||||
|
||||
Die Seite zeigt eine Karte mit der Überschrift `Bitte eine Veranstaltung auswählen`. Wenn keine passende Veranstaltung vorhanden ist, erscheint der Hinweis `Es ist derzeit keine Veranstaltung vorhanden`.
|
||||
|
||||
Jede verfügbare Veranstaltung wird als vollbreiter Button dargestellt. Ein Klick auf einen Eintrag aktiviert diese Show global und öffnet direkt die Remote-Steuerung. Die Seite besitzt kein eigenes Menü.
|
||||
|
||||
## Zusammenspiel von Remote und Monitor
|
||||
|
||||
Die Auswahlseite steuert nicht direkt den Monitor. Sie bestimmt aber über `global/static.currentShow`, welche Show Remote und Monitor anschließend beobachten. Da beide Seiten denselben globalen Wert lesen, wechseln Remote-Steuerung und Monitor auf dieselbe Veranstaltung, sobald die Auswahl gespeichert ist.
|
||||
|
||||
Das zusätzliche Setzen von `presentationSongId` auf `title` sorgt dafür, dass der Monitor nach dem Wechsel nicht versehentlich den zuletzt aktiven Liedabschnitt oder freien Text der Show anzeigt, sondern mit der Veranstaltungsfolie startet.
|
||||
|
||||
## Live-Felder auf der Show
|
||||
|
||||
Die Auswahlseite schreibt nur ein Präsentationsfeld auf der Show:
|
||||
|
||||
- `presentationSongId`: wird beim Auswählen auf `title` gesetzt.
|
||||
|
||||
Alle weiteren Live-Felder werden anschließend über die Remote-Steuerung gepflegt:
|
||||
|
||||
- `presentationSection`
|
||||
- `presentationDynamicCaption`
|
||||
- `presentationDynamicText`
|
||||
- `presentationZoom`
|
||||
- `presentationBackground`
|
||||
|
||||
## Technische Besonderheiten
|
||||
|
||||
Die aktive Show liegt nicht im lokalen Komponentenstatus, sondern im globalen Settings-Dokument. Das macht den Wechsel geräteübergreifend sichtbar: Ein Gerät kann die Show auswählen, während ein anderes Gerät bereits den Monitor geöffnet hat.
|
||||
|
||||
Die Showliste wird über RxJS im Template mit `AsyncPipe` angezeigt. Die Komponente blendet den Inhalt beim Initialisieren über den `fade`-Trigger ein.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Seite: Veranstaltungsdetails
|
||||
|
||||
## Route
|
||||
|
||||
`/shows/:showId`
|
||||
|
||||
Die Route zeigt eine einzelne Veranstaltung anhand der URL-Variable `showId`. In `shows-routing.module.ts` ist kein eigener `canActivate`-Guard für diese Detailroute definiert.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Detailseite ist der zentrale Arbeitsbereich für eine Veranstaltung. Hier werden die Ablaufreihenfolge gepflegt, Lieder für diese Veranstaltung angepasst, Texte angezeigt, die Veranstaltung veröffentlicht oder archiviert, ein Gastlink erzeugt, die CCLI-Meldung abgeschlossen und ein DOCX-Dokument exportiert.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `ActivatedRoute.params` liefert `showId`.
|
||||
- `ShowService.read$(showId)` laedt das Show-Dokument.
|
||||
- `ShowSongService.list$(showId)` laedt die Lieder aus der Subcollection `shows/{showId}/songs`.
|
||||
- `SongService.list$()` wird nur für unveröffentlichte Shows geladen, damit weitere Lieder hinzugefügt werden können.
|
||||
- `GuestShowService.share(show, songs)` erzeugt oder aktualisiert die Gastansicht für geteilte veröffentlichte Veranstaltungen.
|
||||
- `DocxService.create(showId, options?)` laedt Show, Show-Songs, Besitzer und Konfiguration für den DOCX-Export.
|
||||
- `UserService` wird beim Archivieren genutzt, um Song-Nutzungszaehler zu verringern oder wieder zu erhoehen.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
- `app-page-frame` mit Titel `Veranstaltungen`.
|
||||
- Hauptkarte mit Veranstaltungstyp, Datum und Status im Kopf.
|
||||
- Metazeile mit öffentlich/geschlossen, Besitzername, Veröffentlichungs-Badge und CCLI-Melde-Badge für den Besitzer.
|
||||
- Checkbox `Text anzeigen`.
|
||||
- Icon-Aktionen für Textgroesse und Vollbild.
|
||||
- Drag-and-drop-Liedliste mit `app-song`-Einträgen.
|
||||
- `app-add-song` zum Hinzufügen weiterer Lieder bei unveröffentlichten Veranstaltungen.
|
||||
- Vollbild-/Swiper-Ansicht mit Uhrzeit, nächstem Lied und Tastatursteuerung.
|
||||
- Aktionsleiste für Archivierung, Veröffentlichung, Teilen, CCLI, Bearbeiten und DOCX-Download.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Reihenfolge ändern: Drag-and-drop aktualisiert `show.order`.
|
||||
- Lied hinzufügen: nur bei unveröffentlichten Shows, über `app-add-song`.
|
||||
- Tonart ändern: die Tonart eines Show-Songs wird direkt in der Show-Subcollection gespeichert.
|
||||
- Liedtext für diese Veranstaltung bearbeiten: speichert nur den Text des Show-Songs, nicht den globalen Song.
|
||||
- Lied entfernen: entfernt den Show-Song, passt `order` an und entfernt die Song-ID aus `songIds`, wenn kein weiteres Vorkommen desselben Songs in der Show existiert.
|
||||
- Text anzeigen: blendet Liedtexte ein und aktiviert je nach Lied die Akkorddarstellung.
|
||||
- Vollbild starten: aktiviert die Swiper-Ansicht, vergroessert den Text und nutzt Browser-Fullscreen.
|
||||
- Vollbild verlassen: setzt Textgroesse und Fullscreen zurück.
|
||||
- DOCX herunterladen: öffnet ein Menü mit zwei Exportvarianten.
|
||||
|
||||
## Veröffentlichung
|
||||
|
||||
Die Aktion `Veröffentlichen` setzt `published` auf `true`.
|
||||
|
||||
- Geschlossene bzw. nicht öffentliche Shows erhalten `reportedType: 'not-required'`.
|
||||
- Öffentliche Shows werden auf CCLI-pflichtige Lieder geprüft.
|
||||
- Wenn mindestens ein Show-Song `legalOwner === 'CCLI'` und eine `legalOwnerId` hat, wird `reportedType: 'pending'` gesetzt.
|
||||
- Wenn keine CCLI-Meldung erforderlich ist, wird `reportedType: 'not-required'` gesetzt.
|
||||
|
||||
Die Aktion `Veröffentlichung zurückziehen` setzt `published` auf `false` und `reportedType` auf `null`.
|
||||
|
||||
Veröffentlichte Veranstaltungen sind in der Detailseite inhaltlich weitgehend gesperrt: Die Reihenfolge ist nicht per Drag-and-drop änderbar, es werden keine neuen Songs geladen, und die Bearbeiten-Aktion für Showkopf sowie Song-Bearbeitung stehen nicht zur Verfügung.
|
||||
|
||||
## Archivierung
|
||||
|
||||
Die Aktion `Archivieren` öffnet einen Bestätigungsdialog. Nach Bestätigung setzt die Seite `archived: true`.
|
||||
|
||||
Beim Archivieren wird für jeden geladenen Show-Song `UserService.decSongCount(songId)` aufgerufen. Beim Wiederherstellen setzt die Seite `archived: false` und ruft entsprechend `UserService.incSongCount(songId)` auf.
|
||||
|
||||
Archivierte Shows verschwinden aus den normalen Listen und können in der Veranstaltungsliste für eigene Veranstaltungen über den Archiv-Filter wieder sichtbar gemacht werden.
|
||||
|
||||
## CCLI-Meldung
|
||||
|
||||
Die CCLI-Aktion erscheint nur für veröffentlichte Shows mit `reportedType === 'pending'`.
|
||||
|
||||
Der Report-Dialog sammelt eindeutige CCLI-pflichtige Lieder aus der aktuellen Reihenfolge. Eindeutigkeit wird über `songId` oder ersatzweise `title:legalOwnerId` hergestellt. Für jedes Lied zeigt der Dialog Titel, CCLI-Nummer und einen Link zu `https://reporting.ccli.com/search?s={nummer}`.
|
||||
|
||||
Nach Klick auf `Alle CCLI-Titel wurden gemeldet` setzt die Seite `reportedType` auf `reported`.
|
||||
|
||||
## Teilen
|
||||
|
||||
Die Teilen-Aktion ist nur für veröffentlichte Shows sichtbar.
|
||||
|
||||
`GuestShowService.share()` schreibt die für Gaeste benötigten Daten in die Guest-Show-Datenquelle:
|
||||
|
||||
- `showType`
|
||||
- `date`
|
||||
- `updatedAt`
|
||||
- geordnete Songliste
|
||||
|
||||
Wenn die Show noch keine `shareId` hat, wird ein neuer Guest-Show-Eintrag angelegt und die `shareId` im Show-Dokument gespeichert. Wenn bereits eine `shareId` existiert, wird die bestehende Gastansicht aktualisiert.
|
||||
|
||||
Der Share-Dialog zeigt den Link `/guest/{shareId}`, erzeugt einen QR-Code und bietet über Clipboard/Web Share API eine Teilen-Aktion an.
|
||||
|
||||
## DOCX-Export
|
||||
|
||||
Der Download-Button öffnet ein Menü mit zwei Varianten:
|
||||
|
||||
- `Ablauf für Lobpreisgruppe`: erstellt ein DOCX mit Songtexten und Akkorden entsprechend dem jeweiligen `chordMode`.
|
||||
- `Handout mit Copyright Infos`: erstellt ein DOCX mit `chordMode: 'hide'` und Copyright-/Lizenzinformationen.
|
||||
|
||||
`DocxService` laedt die Bibliothek `docx` dynamisch, rendert die Songs in `show.order`, transponiert Texte über `TextRenderingService.parse()` und erzeugt den Download im Browser über einen temporaeren Blob-Link.
|
||||
|
||||
## Statuslogik
|
||||
|
||||
Der Seitenstatus im Kopf wird so berechnet:
|
||||
|
||||
- `veröffentlicht`, wenn `show.published` wahr ist
|
||||
- `gemeldet`, wenn `show.reportedType === 'reported'`
|
||||
- sonst `entwurf`
|
||||
|
||||
Badge-Typen:
|
||||
|
||||
- `reportedType: 'pending'` wird als Fehler-/Warnstatus dargestellt.
|
||||
- `reportedType: 'reported'` wird als OK-Status dargestellt.
|
||||
- `reportedType: 'not-required'` und fehlender Meldewert erhalten keinen hervorgehobenen Badge.
|
||||
- `published` wird als OK-Status dargestellt, nicht veröffentlichte Shows ohne Hervorhebung.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
- Die Route selbst hat im Shows-Routing keinen eigenen Guard.
|
||||
- Archivieren, Wiederherstellen, Veröffentlichen, Veröffentlichung zurückziehen, Teilen, CCLI-Meldung und Bearbeiten sind per `RoleDirective` auf `leader` und per `OwnerDirective` auf den Besitzer der Show beschränkt.
|
||||
- Der DOCX-Download ist in der Aktionsleiste ohne diese Owner-/Leader-Einschränkung sichtbar.
|
||||
- Die Bearbeiten-Aktion für den Showkopf erscheint nur bei unveröffentlichten Shows.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- `show$` und `songs$` werden mit `shareReplay({bufferSize: 1, refCount: true})` geteilt.
|
||||
- Die Seite haelt `showSongs` als lokale Liste, weil Sortierung, Export, Teilen und CCLI-Meldung die geordnete Liste synchron benoetigen.
|
||||
- `orderedShowSongs(show)` mappt `show.order` auf die geladenen Show-Songs und filtert fehlende Einträge aus.
|
||||
- Keyboard-Navigation im Vollbild reagiert auf Pfeiltasten und steuert das Swiper-Element.
|
||||
- `getSongKeyColumnWidth()` berechnet eine stabile Spaltenbreite für Tonart-Labels inklusive Transposition.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Seite: Veranstaltung bearbeiten
|
||||
|
||||
## Route
|
||||
|
||||
`/shows/:showId/edit`
|
||||
|
||||
Die Route bearbeitet den Showkopf einer bestehenden Veranstaltung anhand der URL-Variable `showId`. In `shows-routing.module.ts` ist für diese Route kein eigener `canActivate`-Guard definiert.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite ändert Datum und Veranstaltungstyp einer vorhandenen Veranstaltung. Sie ist für Korrekturen an den Kopfdaten gedacht; Songauswahl, Reihenfolge, Texte, Veröffentlichung, Archivierung, Teilen, CCLI-Meldung und DOCX-Export werden auf der Detailseite verwaltet.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `ActivatedRoute.params` liefert `showId`.
|
||||
- `ShowService.read$(showId)` laedt die Show einmalig.
|
||||
- `ShowService.SHOW_TYPE_PUBLIC` und `ShowService.SHOW_TYPE_PRIVATE` liefern die auswählbaren Typen.
|
||||
- `ShowService.update$(id, data)` speichert die Änderungen.
|
||||
- `ShowDataService.update()` schreibt die Aktualisierung in `shows/{id}`.
|
||||
|
||||
`EditComponent` haelt außerdem `ShowDataService.list$` als `shows$`, nutzt diesen Stream in der aktuellen Vorlage aber nicht sichtbar.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
- `app-page-frame` mit Titel `Veranstaltungen`.
|
||||
- Karte `Veranstaltung ändern` mit Rücksprung zur Detailseite `/shows/{id}`.
|
||||
- Formularfeld `Art der Veranstaltung` als gruppierte Auswahl.
|
||||
- Formularfeld `Datum` mit Material-Datepicker.
|
||||
- Button `Speichern`.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Seite laden: liest die Show und befüllt das Formular mit `id`, `date` und `showType`.
|
||||
- Speichern: markiert alle Felder als berührt, prüft Pflichtfelder und aktualisiert Datum sowie Typ.
|
||||
- Nach dem Speichern: Navigation zurück nach `/shows/{id}`.
|
||||
|
||||
## Statuslogik
|
||||
|
||||
Beim Initialisieren wird das Formular zuerst zurückgesetzt und danach einmalig aus der geladenen Show befüllt. Das Speichern bricht ab, wenn das Formular ungültig ist oder `id`, `date` oder `showType` fehlen.
|
||||
|
||||
Beim Speichern wird nur Folgendes geändert:
|
||||
|
||||
- `date` als Firestore `Timestamp.fromDate(date)`
|
||||
- `showType`
|
||||
|
||||
Die Seite ändert keine Werte für `published`, `reportedType`, `archived`, `order`, `songIds` oder `shareId`.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
- Die Route selbst hat im Shows-Routing keinen eigenen Guard.
|
||||
- Der Einstieg zur Bearbeitungsseite ist auf der Detailseite nur für `leader`, den Besitzer der Show und unveröffentlichte Shows sichtbar.
|
||||
- Direkte URL-Aufrufe werden auf Komponentenebene nicht zusätzlich durch `RoleDirective` oder `OwnerDirective` abgefangen.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Das Formular ist ein Reactive Form mit den Controls `id`, `date` und `showType`.
|
||||
- Die Show wird mit `take(1)` nur einmal in das Formular übernommen.
|
||||
- Die Typauswahl verwendet dieselben Konstanten wie die Neuanlage und zeigt die Werte über `ShowTypePipe` an.
|
||||
- Änderungen am Feld `showType` berechnen `public` nicht neu; gespeichert wird nur der Typ. Falls der Öffentlich-/Privat-Status nach Typwechsel relevant ist, muss diese Logik separat berücksichtigt werden.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Seite: Veranstaltung anlegen
|
||||
|
||||
## Route
|
||||
|
||||
`/shows/new`
|
||||
|
||||
Die Route ist im Shows-Routing mit `RoleGuard` geschuetzt und verlangt die Rolle `leader`.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite erstellt den Kopf einer neuen Veranstaltung. Nach der Anlage wird direkt zur Detailseite navigiert, wo Lieder hinzugefügt und der weitere Ablauf gepflegt werden.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `ShowService.SHOW_TYPE_PUBLIC` und `ShowService.SHOW_TYPE_PRIVATE` liefern die auswählbaren Veranstaltungstypen.
|
||||
- `ShowService.new$(data)` erstellt das Show-Dokument.
|
||||
- `UserService.user$` wird innerhalb von `ShowService.new$()` verwendet, um den Besitzer zu setzen.
|
||||
- `ShowDataService.add()` schreibt die neue Show in die Firestore-Collection `shows`.
|
||||
|
||||
`NewComponent` haelt außerdem `ShowDataService.list$` als `shows$`, nutzt diesen Stream in der aktuellen Vorlage aber nicht sichtbar.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
- `app-page-frame` mit Titel `Veranstaltungen`.
|
||||
- Karte `Neue Veranstaltung` mit Rücksprung nach `/shows`.
|
||||
- Formularfeld `Art der Veranstaltung` als gruppierte Auswahl.
|
||||
- Formularfeld `Datum` mit Material-Datepicker.
|
||||
- Button `Anlegen`.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Formular ausfuellen: Typ und Datum sind Pflichtfelder.
|
||||
- Anlegen: markiert alle Felder als berührt, prüft die Formularvaliditaet und ruft `ShowService.new$()` auf.
|
||||
- Nach erfolgreicher Anlage: Navigation zu `/shows/{id}`.
|
||||
|
||||
## Statuslogik
|
||||
|
||||
Beim Initialisieren wird das Formular zurückgesetzt. Das Speichern bricht ab, wenn Pflichtfelder fehlen oder kein Benutzer ermittelt werden kann.
|
||||
|
||||
`ShowService.new$()` ergänzt die eingegebenen Daten um:
|
||||
|
||||
- `owner`: ID des aktuellen Benutzers
|
||||
- `order: []`
|
||||
- `songIds: []`
|
||||
- `public`: abgeleitet daraus, ob `showType` in `SHOW_TYPE_PUBLIC` enthalten ist
|
||||
|
||||
Veröffentlichungs-, Melde- und Archivstatus werden auf dieser Seite nicht direkt gesetzt.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
- Zugriff auf die Route erfordert `leader`.
|
||||
- Die Seite selbst enthält keine zusätzliche Owner-Prüfung, da die Show erst beim Speichern für den aktuellen Benutzer erzeugt wird.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Das Formular ist ein typisiertes Reactive Form mit `FormControl<Date | null>` und `FormControl<string | null>`.
|
||||
- Die Auswahl trennt öffentliche und private Veranstaltungstypen in `mat-optgroup`.
|
||||
- Die Anzeige der Typen erfolgt über `ShowTypePipe`.
|
||||
- `ShowService.new$()` gibt `null` zurück, wenn kein `showType` oder kein Benutzer vorhanden ist; die Navigation nutzt dann defensiv einen leeren String.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Seite: Veranstaltungsliste
|
||||
|
||||
## Route
|
||||
|
||||
`/shows`
|
||||
|
||||
Die Route ist der Standardpfad des Shows-Moduls. In `shows-routing.module.ts` ist kein eigener `canActivate`-Guard auf dieser Route definiert.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Veranstaltungsliste ist der Einstieg in den Veranstaltungsbereich. Sie trennt eigene, noch zu bearbeitende Veranstaltungen von bereits veröffentlichten Veranstaltungen und bietet für berechtigte Benutzer Filter sowie den Einstieg zur Neuanlage.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `ShowService.list$()` liefert die für den aktuellen Benutzer sichtbaren Veranstaltungen.
|
||||
- `ShowService.list$(false, true)` liefert eigene Veranstaltungen inklusive archivierter eigener Einträge.
|
||||
- `ShowService.listPublicSince$(lastMonths)` fragt veröffentlichte, nicht archivierte Veranstaltungen serverseitig nach Zeitraum ab.
|
||||
- Falls die serverseitige Abfrage keine Ergebnisse liefert, nutzt die Liste einen lokalen Fallback aus geladenen veröffentlichten und nicht archivierten Shows.
|
||||
- `FilterStoreService.showFilter$` speichert Zeitraum, Ersteller, Veranstaltungstyp und Archiv-Filter.
|
||||
- `UserService.user$` bestimmt den aktuellen Benutzer und dessen Rolle.
|
||||
- `FilterComponent.owners$()` baut die Ersteller-Auswahl aus den in Shows verwendeten `owner`-IDs und `UserService.getUserbyId$()`.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
- Seitenrahmen `app-page-frame` mit Titel `Veranstaltungen`.
|
||||
- Sidebar mit `app-filter`, sichtbar für Rollen mit Sidebar-Zugriff.
|
||||
- Karte `Meine Veranstaltungen` für eigene Entwürfe, nicht gemeldete veröffentlichte Veranstaltungen und optional archivierte Veranstaltungen.
|
||||
- Karte `Veröffentlichte Veranstaltungen` für veröffentlichte, nicht archivierte Veranstaltungen.
|
||||
- Listeneintraege zeigen Datum, Erstellername, übersetzten Veranstaltungstyp und optional ein Status-Badge.
|
||||
- Button `Neue Veranstaltung anlegen`, der nach `/shows/new` führt.
|
||||
- Hinweiszeile bei aktiven Filtern mit Anzahl der angezeigten Veranstaltungen und Aktion `Filter zurücksetzen`.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Veranstaltung öffnen: Klick auf einen Listeneintrag navigiert relativ zu `show.id`, also nach `/shows/:showId`.
|
||||
- Neue Veranstaltung anlegen: nur über den Button in `Meine Veranstaltungen`.
|
||||
- Filter ändern: Zeitraum, Ersteller, Veranstaltungstyp und Archiv-Anzeige werden direkt im `FilterStoreService` aktualisiert.
|
||||
- Filter zurücksetzen: stellt den Standardfilter wieder her.
|
||||
|
||||
## Statuslogik
|
||||
|
||||
Der Standardfilter ist:
|
||||
|
||||
- Zeitraum: letzter Monat
|
||||
- Ersteller: alle
|
||||
- Veranstaltungstyp: alle
|
||||
- Archiviert: aus
|
||||
|
||||
Öffentliche Veranstaltungen werden absteigend nach Datum sortiert und zusätzlich nach Ersteller und Veranstaltungstyp gefiltert. Der Zeitraum wird in 30-Tage-Monaten berechnet; `alle` nutzt den Wert `99999`.
|
||||
|
||||
Eigene Veranstaltungen erscheinen in `Meine Veranstaltungen`, wenn sie dem aktuellen Benutzer gehören und eine der folgenden Bedingungen erfüllen:
|
||||
|
||||
- nicht veröffentlicht
|
||||
- `reportedType === 'pending'`
|
||||
- archiviert und der Archiv-Filter ist aktiv
|
||||
|
||||
Status-Badges in `Meine Veranstaltungen`:
|
||||
|
||||
- `archiviert`, wenn `show.archived` gesetzt ist
|
||||
- `nicht gemeldet`, wenn die Show veröffentlicht ist
|
||||
- `unveröffentlicht`, wenn die Show nicht veröffentlicht ist
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
- Die Route selbst hat im Shows-Routing keinen eigenen Guard.
|
||||
- Sidebar und Filter sind nur sichtbar, wenn die Rolle `admin` oder `leader` enthält.
|
||||
- Der Button `Neue Veranstaltung anlegen` ist per `RoleDirective` auf `leader` beschränkt.
|
||||
- Benutzer ohne Sidebar-Zugriff sehen nur die veröffentlichten Veranstaltungen ohne Filterbereich.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- `ShowService.list$()` filtert archivierte Shows grundsaetzlich aus, lässt eigene archivierte Shows aber zu, wenn `includeOwnArchived` aktiv ist.
|
||||
- `ShowDataService.listPublicSince$()` verwendet Firestore-Constraints `published == true`, optional `date >= startTimestamp` und `orderBy('date', 'desc')`; archivierte Shows werden danach clientseitig entfernt.
|
||||
- Die Seite nutzt `combineLatest`, `switchMap`, `map` und `shareReplay`-basierte Streams; die Darstellung erfolgt komplett reaktiv über `AsyncPipe`.
|
||||
- Filterwerte bleiben über `FilterStoreService` erhalten und werden sowohl in der Sidebar als auch für die Listenberechnung verwendet.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Lieddetails
|
||||
|
||||
## Route
|
||||
|
||||
`/songs/:songId`
|
||||
|
||||
Die Detailseite ist ein Child-Pfad des Songs-Moduls. Der übergeordnete `/songs`-Eintrag im App-Router verlangt Authentifizierung und die Rolle `user`. Die Detailroute selbst definiert keinen zusätzlichen `canActivate`-Guard.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Detailseite ist die primäre Leseseite für einen einzelnen Song. Sie zeigt Stammdaten, Liedtext, Akkorddarstellung, Kommentare, Attribute, Anhänge und nutzerbezogene Verwendungsinformationen. Außerdem bietet sie rollenabhängige Aktionen zum Bearbeiten, Löschen und zum Hinzufügen des Songs zu einer eigenen unveröffentlichten Veranstaltung.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `SongService.read$(songId)` liest den Song aus der Collection `songs`.
|
||||
- `FileDataService.read$(songId)` liest die Subcollection `songs/{songId}/files`.
|
||||
- `UserService.user$` liefert den angemeldeten Benutzer, dessen bevorzugter `chordMode` für die Liedtextanzeige genutzt wird.
|
||||
- `UserService.user$` liefert außerdem `songUsage`, aus dem die nutzerbezogene Verwendungsanzahl des Songs bestimmt wird.
|
||||
- `ShowService.list$()` liefert Shows, um eigene Shows des Benutzers zu finden und unveröffentlichte Veranstaltungen für die Aktion `Zu Veranstaltung hinzufügen` anzubieten.
|
||||
- `ShowSongService.new$()` erstellt beim Hinzufügen zu einer Show den Show-Song-Eintrag.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
Die Seite verwendet `app-page-frame` ohne Menü. Im Content-Bereich liegt eine Detail-Card mit der Überschrift aus Liednummer und Titel sowie einem Zurück-Link zur Liste.
|
||||
|
||||
Für berechtigte Rollen werden Stammdaten angezeigt:
|
||||
|
||||
- Typ
|
||||
- Tonart
|
||||
- Tempo
|
||||
- Status
|
||||
- Rechteinhaber und Rechteinhaber-ID
|
||||
- CCLI-Link, wenn `legalOwner === 'CCLI'` und eine `legalOwnerId` vorhanden ist
|
||||
- Künstler
|
||||
- Verlag
|
||||
- Quelle
|
||||
- Verwendungsanzahl mit Tooltip
|
||||
|
||||
Der Liedtext wird über `app-song-text` gerendert. Die Komponente erhält den Songtext, den Chord-Modus des Benutzers und `validateChordNotation=true`. Wenn `showSwitch` aktiv ist, kann die Akkordanzeige zwischen Ausblenden, nur erste Strophe und Anzeigen wechseln.
|
||||
|
||||
Attribute aus `song.flags` werden als Chips dargestellt. Der Kommentar wird als eigener Textblock angezeigt. Anhänge erscheinen in einer separaten Card `Anhänge`; jeder Anhang löst seine Firebase-Storage-Download-URL auf und wird als externer Link geöffnet.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- `Bearbeiten`: navigiert nach `/songs/:songId/edit`.
|
||||
- `Löschen`: löscht den Song über `SongService.delete(songId)` und navigiert anschließend zurück nach `/songs`.
|
||||
- `Zu Veranstaltung hinzufügen`: erstellt einen Show-Song-Eintrag, hängt dessen ID an die Reihenfolge (`order`) der gewählten Show an und navigiert zur Show unter `/shows/:showId`.
|
||||
- Akkordschalter im Songtext: wechselt den lokalen Chord-Modus der Anzeige.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
Der Seitenzugriff erfordert über die Parent-Route die Rolle `user`. Innerhalb der Seite gelten zusätzliche Rollen:
|
||||
|
||||
- `leader` und `contributor`: sehen Metadaten, Attribute, Kommentar und Verwendungsinformationen.
|
||||
- `contributor`: sieht die Aktion `Bearbeiten`.
|
||||
- `leader`: sieht `Zu Veranstaltung hinzufügen`, sofern unveröffentlichte Shows vorhanden sind.
|
||||
- `admin`: sieht die Aktion `Löschen`.
|
||||
|
||||
## Relevante technische Hinweise
|
||||
|
||||
Die Song-ID wird aus den Routenparametern gelesen und in mehreren Observables verwendet. `song$`, `files$`, `songCount$`, `songUsageShows$` und `songUsageTooltip$` reagieren dadurch auf Parameteränderungen.
|
||||
|
||||
Die Verwendungsanzeige kombiniert zwei Quellen: den Zähler `user.songUsage[song.id]` und eine aus Shows berechnete Liste eigener Shows, deren `songIds` den Song enthalten. Der Tooltip zeigt entweder einen Leerzustand, einen Hinweis auf noch nicht indexierte Show-Zuordnungen oder eine datierte Liste der gefundenen Shows.
|
||||
|
||||
Anhänge sind Metadaten in Firestore und Binärdaten in Firebase Storage. Die Detailseite kann Anhänge nur öffnen; Upload und Löschen werden auf der Bearbeitungsseite erledigt.
|
||||
|
||||
Das Löschen fragt im aktuellen Komponentenstand keine Bestätigung ab. Dokumentations- und UI-Texte sollten deshalb deutlich machen, dass die Aktion direkt über den Service ausgeführt wird.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Lied bearbeiten
|
||||
|
||||
## Route
|
||||
|
||||
`/songs/:songId/edit`
|
||||
|
||||
Die Route ist im Songs-Modul als `path: ':songId/edit'` definiert. Der übergeordnete `/songs`-Eintrag im App-Router verlangt Authentifizierung und die Rolle `user`. Die Edit-Route selbst definiert keinen zusätzlichen `canActivate`-Guard, verwendet aber `EditSongGuard` als `canDeactivate`-Guard.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Bearbeitungsseite bündelt die Pflege eines Songs. Sie erlaubt das Bearbeiten von Inhalt, musikalischen Angaben, rechtlichen Metadaten und Attributen, verwaltet Anhänge und zeigt die letzte Änderungshistorie. Sie ist die Anschlussseite nach dem Anlegen eines neuen Songs und die zentrale Stelle für spätere Songpflege.
|
||||
|
||||
## Aufbau
|
||||
|
||||
`EditComponent` ist eine Container-Komponente innerhalb eines `app-page-frame` ohne Menü. Sie rendert drei Teilbereiche:
|
||||
|
||||
- `EditSongComponent`: Formular für Songtext, Stammdaten und Rechtsinformationen.
|
||||
- `EditFileComponent`: Upload und Verwaltung angehängter Dateien.
|
||||
- `HistoryComponent`: Anzeige der letzten gespeicherten Änderungen.
|
||||
|
||||
Der Container hält über `ViewChild` eine Referenz auf `EditSongComponent`, damit der `EditSongGuard` beim Verlassen der Route ungespeicherte Formularänderungen prüfen kann.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `SongService.read$(songId)` lädt den Song für Formular und Historie.
|
||||
- `EditService.createSongForm(song)` erzeugt das reaktive Formular aus dem geladenen Song.
|
||||
- `SongService.update$(songId, data)` speichert Änderungen und ergänzt einen Eintrag in `song.edits`.
|
||||
- `UserService.currentUser()` liefert beim Speichern den Namen des aktuellen Benutzers für die Änderungshistorie.
|
||||
- `FileDataService.read$(songId)` liest vorhandene Anhänge aus `songs/{songId}/files`.
|
||||
- `UploadService` schreibt neue Dateien nach Firebase Storage unter `/attachments/{songId}` und speichert die Metadaten anschließend in Firestore.
|
||||
- `FileService` löst Download-URLs auf und löscht Anhänge aus Storage sowie aus der Firestore-Subcollection.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
Der Song-Editor zeigt eine Card mit der Überschrift `{Nummer} bearbeiten` und einem Zurück-Link zur Detailseite. Das Formular enthält:
|
||||
|
||||
- Titel
|
||||
- Typ
|
||||
- Tonart
|
||||
- Tempo
|
||||
- Status
|
||||
- Songtext
|
||||
- Kommentar
|
||||
- Attribute als Chips
|
||||
- rechtlicher Status
|
||||
- Rechteinhaber
|
||||
- Rechteinhaber-ID mit CCLI-Link, wenn der Rechteinhaber CCLI ist
|
||||
- Künstler
|
||||
- Verlag / Copyright
|
||||
- Nutzungsbedingungen
|
||||
- abweichende Quelle
|
||||
|
||||
Während das Songtextfeld fokussiert ist, zeigt die Seite eine Vorschau über `app-song-text` sowie Bearbeitungshinweise zum Aufbau von Strophen, Refrain, Bridge und Akkordzeilen. Akkordvalidierungsfehler werden unter `Akkordschreibweise korrigieren` mit Zeile, Meldung, betroffenem Token und optionalem Vorschlag angezeigt.
|
||||
|
||||
Der Dateibereich zeigt eine Upload-Auswahl, einen Upload-Button, während des Uploads einen Fortschrittsbalken und die vorhandenen Anhänge. Jeder Anhang kann geöffnet und über ein Papierkorb-Symbol gelöscht werden.
|
||||
|
||||
Die Historie zeigt vorhandene `song.edits` mit Benutzername und Datum.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- `Speichern`: validiert das Formular, speichert die Rohwerte über `SongService.update$()`, markiert das Formular als unverändert und navigiert nach `/songs/:songId`.
|
||||
- Attribut hinzufügen: Eingabe im Chip-Feld wird bei Enter, Komma oder Blur an die semikolongetrennte `flags`-Liste angehängt.
|
||||
- Attribut entfernen: entfernt den Chip und schreibt die verbleibenden Attribute zurück in `flags`.
|
||||
- Datei auswählen: übernimmt die aktuelle Dateiauswahl aus dem File-Input.
|
||||
- Datei hochladen: lädt die erste ausgewählte Datei nach Firebase Storage und speichert danach die Dateimetadaten.
|
||||
- Datei löschen: entfernt die Datei aus Firebase Storage und löscht den Firestore-Metadatensatz.
|
||||
- Seite verlassen mit ungespeicherten Änderungen: öffnet einen Speicherdialog; bei Bestätigung wird vor der Navigation gespeichert.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
Der Zugriff auf das Songs-Modul erfordert Authentifizierung und die Rolle `user`. Die Edit-Route selbst erzwingt in `songs-routing.module.ts` keine `contributor`-Rolle. Der Einstieg aus der Detailseite ist allerdings nur für `contributor` sichtbar, weil der Button `Bearbeiten` rollenabhängig gerendert wird.
|
||||
|
||||
Für die Dokumentation ist deshalb wichtig: Die UI versteckt den regulären Einstieg für Nicht-Contributors, die Route selbst enthält aber keinen zusätzlichen Aktivierungs-Guard. Falls direkte URL-Aufrufe ebenfalls verhindert werden sollen, müsste die Route analog zu `/songs/new` mit einem `RoleGuard` erweitert werden.
|
||||
|
||||
## Relevante technische Hinweise
|
||||
|
||||
Die Akkordvalidierung wird bei jeder Änderung des Songtextes aktualisiert. `TextRenderingService.validateChordNotation()` erkennt unter anderem alternative Schreibweisen wie `is/es`, falsche Dur/Moll-Großschreibung, nicht normalisierte Suffixe, unbekannte Akkordtokens und Tabulatoren in Akkordzeilen. Validierungsfehler werden als Formularfehler `chordNotation` am Text-Control gesetzt; dadurch ist der Speichern-Button deaktiviert, solange der Text ungültig ist.
|
||||
|
||||
`SongService.update$()` liest vor dem Speichern den aktuellen Song, hängt einen neuen Historieneintrag mit Benutzername und `Timestamp.now()` an und schreibt anschließend die Änderungen. Dadurch wird die Historie nur bei Speichervorgängen über diesen Service erweitert.
|
||||
|
||||
Der Upload-Service ignoriert Upload-Fehler im aktuellen Stand bewusst im Error-Callback. Die UI zeigt Fortschritt und erfolgreiche Metadaten danach über die Dateiliste, aber keine eigene Fehlermeldung.
|
||||
|
||||
Der `EditSongGuard` schützt nur vor Navigation aus dem Angular-Router heraus. Er fragt `EditSongComponent.askForSave(nextState)` ab und navigiert nach dem Dialog selbst weiter. Bei nicht dirty Formularen wird die Navigation direkt erlaubt.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Lied anlegen
|
||||
|
||||
## Route
|
||||
|
||||
`/songs/new`
|
||||
|
||||
Die Route ist im Songs-Modul als `path: 'new'` definiert. Sie verwendet zusätzlich zum übergeordneten `/songs`-Schutz einen `RoleGuard` mit `requiredRoles: ['contributor']`.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite legt einen neuen Song mit minimalen Pflichtdaten an. Sie erfasst nur Liednummer und Titel, erstellt daraus einen Firestore-Datensatz und leitet den Benutzer anschließend direkt auf die Bearbeitungsseite weiter, damit Text, Metadaten, Rechteinformationen und Anhänge ergänzt werden können.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `SongService.list$()` liefert die vorhandenen Songs.
|
||||
- `NewComponent.getFreeSongNumber()` ermittelt aus den vorhandenen Liednummern die erste freie positive Nummer.
|
||||
- `SongService.new(songNumber, title)` erstellt den neuen Song.
|
||||
- `SongDataService.add()` schreibt den Datensatz in die Collection `songs`.
|
||||
|
||||
Beim Erstellen setzt `SongService.new()` zusätzlich Standardwerte:
|
||||
|
||||
- `status: 'draft'`
|
||||
- `legalType: 'open'`
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
Die Seite verwendet `app-page-frame` ohne Menü und eine Card mit der Überschrift `Neues Lied`. Die Card besitzt einen Zurück-Link zur Liedliste.
|
||||
|
||||
Das Formular besteht aus zwei Feldern:
|
||||
|
||||
- `Nummer`: numerische Liednummer, Pflichtfeld
|
||||
- `Titel`: Songtitel, Pflichtfeld
|
||||
|
||||
Beim Initialisieren wird das Formular zurückgesetzt. Danach wird aus der Songliste automatisch die erste freie Liednummer eingetragen. Der Titel bleibt leer und muss vom Benutzer ausgefüllt werden.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- `Anlegen`: liest die Formularwerte, erstellt den Song und navigiert nach `/songs/{newSongId}/edit`.
|
||||
- Zurück-Link der Card: führt zurück zur Liedliste.
|
||||
|
||||
Wenn keine Nummer vorhanden ist, bricht `onSave()` ab. Eine explizite Prüfung auf doppelte Nummern findet in dieser Komponente nicht statt; die automatische Vorbelegung soll Kollisionen im normalen Ablauf vermeiden.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
Der Zugriff auf das Songs-Modul erfordert Authentifizierung und die Rolle `user`. Diese konkrete Route verlangt zusätzlich die Rolle `contributor`. Benutzer ohne diese Rolle sollen nicht auf die Anlegeseite gelangen.
|
||||
|
||||
## Relevante technische Hinweise
|
||||
|
||||
Das Formular ist ein reaktives Angular-Formular mit `FormGroup` und `Validators.required` für beide Felder. Die Komponente nutzt `take(1)` und `takeUntilDestroyed()`, um die vorhandene Songliste einmalig auszuwerten und die Subscription sauber zu beenden.
|
||||
|
||||
Die neue Song-ID kommt von Firestore. Nach erfolgreichem Anlegen wird nicht die Detailseite geöffnet, sondern bewusst die Bearbeitungsroute. Dadurch bleibt der Anlegeprozess kurz und die vollständige Pflege des Songs findet im Editor statt.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Liedliste
|
||||
|
||||
## Route
|
||||
|
||||
`/songs`
|
||||
|
||||
Die Route liegt im Songs-Modul als leerer Child-Pfad (`path: ''`). Der übergeordnete App-Router lädt das Modul unter `/songs`, schützt es mit `AuthGuard` und `RoleGuard` und verlangt die Rolle `user`. Vor dem Anzeigen der Seite lädt `SongListResolver` einmalig die vollständig geladene Songliste.
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Liedliste ist der zentrale Einstieg in die Songdatenbank. Sie zeigt alle Songs nach Liednummer sortiert, ermöglicht das schnelle Auffinden über Suche und Filter und macht wichtige Pflegehinweise direkt in der Liste sichtbar. Von hier aus wechseln Benutzer in die Detailseite eines Songs oder, mit entsprechender Berechtigung, in den Anlegeprozess.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `SongListResolver` nutzt `SongService.listLoaded$()` und wartet mit `take(1)` auf die erste geladene Liste aus Firestore.
|
||||
- `SongService` delegiert die Datenzugriffe an `SongDataService`, das die Collection `songs` über `DbService.col$<Song>('songs')` liest und per `shareReplay` cacht.
|
||||
- Die aktuellen Filterwerte kommen aus `FilterStoreService.songFilter$`.
|
||||
- Die Textsuche läuft über `searchSongs`; zusätzliche Filter werden in `SongListComponent.filter()` geprüft.
|
||||
- `TextRenderingService.validateChordNotation()` validiert den Songtext jedes sichtbaren Listeneintrags auf Akkordschreibweisen.
|
||||
|
||||
## Wichtige UI-Elemente
|
||||
|
||||
Die Seite verwendet `app-page-frame` mit dem Titel `Lieder`. Im Sidebar-Bereich befindet sich `app-filter`; der Hauptbereich enthält eine Liste in einer Card.
|
||||
|
||||
Jeder Listeneintrag zeigt:
|
||||
|
||||
- Liednummer
|
||||
- Titel
|
||||
- Status- und Warnsymbole
|
||||
- Tonart
|
||||
|
||||
Wenn ein Filter aktiv ist, erscheint oberhalb der Liste ein Hinweis mit der Anzahl gefundener Lieder und der Aktion `Filter zurücksetzen`. Zusätzlich setzt `menuBadge` am Page-Frame ein optisches Signal, dass die Liste gefiltert ist.
|
||||
|
||||
Die Filterkomponente bietet:
|
||||
|
||||
- Freitextsuche nach Titel oder Text
|
||||
- Filter nach Songtyp (`Praise`, `Worship`, `Misc`)
|
||||
- Tonartfilter nach Grundton, Vorzeichen und Dur/Moll
|
||||
- Option `Parallele Tonart einschließen`
|
||||
- Filter nach rechtlichem Status
|
||||
- Filter nach Attributen aus den im Bestand vorhandenen `flags`
|
||||
|
||||
Nicht passende Tonart-Grundtöne werden deaktiviert, wenn im aktuellen Songbestand keine passende Tonart existiert.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Klick auf einen Listeneintrag navigiert relativ zur Song-ID, also auf `/songs/:songId`.
|
||||
- `Filter zurücksetzen` leert die gespeicherten Songfilter im `FilterStoreService`.
|
||||
- `Neuen Song anlegen` navigiert auf `/songs/new`.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
Der Zugriff auf das Songs-Modul erfordert Authentifizierung und die Rolle `user`. Innerhalb der Liedliste sind zusätzliche Informationen rollenabhängig:
|
||||
|
||||
- `contributor`: sieht Statusindikatoren für `draft`, `set` und `final`.
|
||||
- `contributor`: sieht den Button `Neuen Song anlegen`.
|
||||
- Rechtliche Warnungen für Songs mit `legalType === 'open'` werden unabhängig vom Contributor-Status am Eintrag angezeigt.
|
||||
|
||||
## Relevante technische Hinweise
|
||||
|
||||
Die Liste sortiert Songs clientseitig nach `number`. Der Resolver verwendet die geladene Songliste ohne `startWith([])`, damit die Route erst nach der ersten echten Firestore-Antwort angezeigt wird. Die spätere Filterung kombiniert `combineLatest` aus Filterstore und Routendaten.
|
||||
|
||||
Die Akkordvalidierung wird pro gefiltertem Song ausgeführt und markiert betroffene Titel mit einem Stern. Die Validierung verändert keine Daten; sie dient nur als Hinweis für Pflegebedarf.
|
||||
|
||||
Der Tonartfilter basiert auf `matchesKeyFilter()` aus `key.helper.ts`. Er unterstützt Dur/Moll, `#`/`b`-Varianten und optional parallele Tonarten. Die Filterwerte bleiben im zentralen Filterstore erhalten und werden beim erneuten Öffnen der Seite wieder in das Formular übernommen.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Benutzerprofil
|
||||
|
||||
## Route
|
||||
|
||||
`/user/info`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite zeigt das Profil des angemeldeten Benutzers, dessen Rollen und persönliche Anzeigeeinstellungen. Administratoren sehen zusätzlich die Benutzerverwaltung.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `InfoComponent` liest den aktuellen Benutzer über `UserService.user$`.
|
||||
- Änderungen an Benutzerdaten werden über `UserService.update$(uid, data)` in `users/{uid}` gespeichert.
|
||||
- Die Admin-Liste verwendet `UsersComponent`, `UserComponent` und `UserService.list$()` für alle Benutzer.
|
||||
|
||||
## UI
|
||||
|
||||
Im Profilbereich werden Begrüßung, Rollenanzeige und die bevorzugte Akkordanzeige dargestellt. Die Akkordanzeige ist ein Auswahlfeld mit den Werten leer, `hide`, `onlyFirst` und `show`.
|
||||
|
||||
Für Administratoren erscheint zusätzlich eine Karte `registrierte Benutzer`. Jeder Listeneintrag kann angeklickt werden und wechselt dann in einen Bearbeitungsmodus mit Namensfeld und Mehrfachauswahl für Rollen.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Akkordmodus ändern: speichert `chordMode` direkt am aktuellen Benutzer.
|
||||
- Abmelden: navigiert nach `../logout`.
|
||||
- Benutzer bearbeiten: Administratoren können Namen und Rollen einzelner Benutzer ändern.
|
||||
- Bearbeitung schließen: beendet den Bearbeitungsmodus des Benutzerlisteneintrags.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Route ist durch `AuthGuard` geschützt und setzt einen angemeldeten Benutzer voraus. Die Benutzerverwaltung wird über `*appRole="['admin']"` nur für Administratoren angezeigt. In `RoleDirective` gilt `admin` als Vollberechtigung; andere Rollen werden gegen die angeforderte Rollenliste geprüft.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Rollen werden als semikolongetrennter String im Feld `role` gespeichert und für die UI in Arrays umgewandelt.
|
||||
- Verfügbare Rollen kommen aus `ROLE_TYPES`: `admin`, `user`, `member`, `leader`, `presenter`, `contributor`.
|
||||
- Die Rollenanzeige nutzt `RolePipe`; bei fehlenden Rollen erscheint ein Warnhinweis.
|
||||
- Die Methode `transdormUserRoles` enthält einen Schreibfehler im Namen, ist aber die aktuell verwendete UI-Hilfsmethode.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Login
|
||||
|
||||
## Route
|
||||
|
||||
`/user/login`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite meldet bestehende Benutzer mit E-Mail-Adresse und Passwort an. Nach erfolgreicher Anmeldung wird der Benutzer zur Startseite weitergeleitet.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `LoginComponent` verwaltet ein reaktives Formular mit den Feldern `user` und `pass`.
|
||||
- `UserService.login(user, password)` delegiert an `UserSessionService.login`.
|
||||
- `UserSessionService` verwendet Firebase Auth (`signInWithEmailAndPassword`) und lädt danach den zugehörigen Datensatz aus `users/{uid}`.
|
||||
|
||||
## UI
|
||||
|
||||
Die Ansicht zeigt das Anwendungslogo, zwei Eingabefelder und drei vollbreite Aktionsbuttons. Fehlercodes aus Firebase Auth werden über `AuthMessagePipe` in deutsche Meldungen übersetzt.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- `Anmelden`: validiert das Formular, führt den Login aus, initialisiert bei Bedarf `songUsage` und navigiert bei Erfolg nach `/`.
|
||||
- `Passwort zurücksetzen`: navigiert nach `/user/password`.
|
||||
- `neuen Benutzer anlegen`: navigiert nach `/user/new`.
|
||||
- `Enter` in einem Eingabefeld löst ebenfalls den Login aus.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Login-Seite ist öffentlich erreichbar. Die spätere Nutzbarkeit geschützter Bereiche hängt vom geladenen Benutzerdokument und dessen Rollen ab.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Das Formular verlangt eine gültige E-Mail-Adresse und ein Passwort.
|
||||
- Wenn Firebase Auth erfolgreich ist, aber kein passender Firestore-Benutzer existiert, liefert der Login keine Benutzer-ID zurück.
|
||||
- Bekannte Fehlercodes wie `auth/user-not-found`, `auth/wrong-password` und `auth/invalid-email` werden lokalisiert angezeigt.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Abmelden
|
||||
|
||||
## Route
|
||||
|
||||
`/user/logout`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite beendet die aktuelle Benutzersitzung und leitet danach zur Startseite weiter.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `LogoutComponent` nutzt `UserService.logout()`.
|
||||
- `UserService.logout()` delegiert an `UserSessionService.logout`.
|
||||
- `UserSessionService` ruft Firebase Auth `signOut` auf.
|
||||
|
||||
## UI
|
||||
|
||||
Die Komponente hat aktuell kein sichtbares Template. Sie dient als technische Navigationsroute für den Logout-Vorgang.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- Nach Initialisierung der View wird der Benutzer abgemeldet.
|
||||
- Nach erfolgreichem Logout navigiert die Komponente nach `/`.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Route ist nicht durch einen Guard geschützt. Ist kein Benutzer angemeldet, läuft der Logout-Aufruf trotzdem über Firebase Auth und die anschließende Weiterleitung bleibt gleich.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Der Logout wird in `ngAfterViewInit` gestartet.
|
||||
- Der asynchrone Ablauf wird bewusst ohne sichtbaren Zwischenzustand ausgeführt.
|
||||
- Fehlerbehandlung oder Fehlermeldungen sind in der Komponente nicht implementiert.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Benutzer anlegen
|
||||
|
||||
## Route
|
||||
|
||||
`/user/new`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite erstellt einen neuen Benutzeraccount. Sie ist für die Selbstregistrierung gedacht und vergibt noch keine fachlichen Rollen.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `NewComponent` verwaltet ein reaktives Formular mit `email`, `name` und `password`.
|
||||
- `UserService.createNewUser(email, name, password)` delegiert an `UserSessionService.createNewUser`.
|
||||
- `UserSessionService` erstellt den Firebase-Auth-Benutzer und legt anschließend `users/{uid}` in Firestore an.
|
||||
|
||||
## UI
|
||||
|
||||
Die Ansicht besteht aus einer Karte mit den Feldern Name, E-Mail-Adresse und Passwort sowie einem Button `Benutzer anlegen`. Fehlercodes werden über `AuthMessagePipe` angezeigt.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- `Benutzer anlegen`: validiert das Formular, erstellt den Auth-Account, schreibt das Firestore-Benutzerdokument und navigiert danach zu `/brand/new-user`.
|
||||
- Schließen über `closeLink="../"`: führt zurück zur übergeordneten Benutzerroute.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Seite ist öffentlich erreichbar. Neue Benutzer erhalten initial keine Rolle. Das angelegte Dokument enthält `name`, `chordMode: 'onlyFirst'` und `songUsage: {}`; ein Administrator muss später passende Rollen vergeben.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Das Passwort muss mindestens sechs Zeichen lang sein.
|
||||
- Firebase-Fehler wie `auth/email-already-in-use`, `auth/invalid-email` und `auth/weak-password` werden in deutsche Meldungen übersetzt.
|
||||
- Nach der Registrierung ist der Benutzer durch Firebase Auth angemeldet, befindet sich aber ohne Rollen im Freischaltungszustand.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Passwort-Mail gesendet
|
||||
|
||||
## Route
|
||||
|
||||
`/user/password-send`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite bestätigt, dass eine E-Mail zum Setzen eines neuen Passworts gesendet wurde.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
Die Seite lädt keine eigenen Daten. `PasswordSendComponent` rendert ausschließlich statischen Inhalt.
|
||||
|
||||
## UI
|
||||
|
||||
Die Ansicht besteht aus einer Karte mit dem Hinweis, dass die E-Mail gesendet wurde und einen Link zur Eingabe des neuen Passworts enthält.
|
||||
|
||||
## Aktionen
|
||||
|
||||
Die Seite bietet keine eigene Aktion. Die fachliche Aktion findet vorher auf `/user/password` statt.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Seite ist öffentlich erreichbar. Eine aktive Anmeldung oder Rolle ist nicht erforderlich.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Es findet keine erneute Prüfung statt, ob die Reset-Mail tatsächlich zugestellt wurde.
|
||||
- Die Route dient nur als Erfolgsziel nach `UserService.changePassword`.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Passwort zurücksetzen
|
||||
|
||||
## Route
|
||||
|
||||
`/user/password`
|
||||
|
||||
## Zweck
|
||||
|
||||
Die Seite fordert für eine E-Mail-Adresse eine Passwort-Zurücksetzung an.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
- `PasswordComponent` verwaltet ein reaktives Formular mit dem Feld `user`.
|
||||
- `UserService.changePassword(user)` delegiert an `UserSessionService.changePassword`.
|
||||
- `UserSessionService` ruft Firebase Auth `sendPasswordResetEmail` mit `environment.url` als Rücksprung-URL auf.
|
||||
|
||||
## UI
|
||||
|
||||
Die Ansicht zeigt eine Karte `Passwort zurücksetzen`, ein E-Mail-Feld und den Button `neues Passwort anfordern`. Fehlercodes werden innerhalb der Button-Zeile angezeigt.
|
||||
|
||||
## Aktionen
|
||||
|
||||
- `neues Passwort anfordern`: validiert die E-Mail-Adresse, sendet die Reset-Mail und navigiert bei Erfolg nach `/user/password-send`.
|
||||
- `Enter` im Eingabefeld löst dieselbe Aktion aus.
|
||||
- Schließen über `closeLink="../"`: führt zurück zur übergeordneten Benutzerroute.
|
||||
|
||||
## Rollen und Berechtigungen
|
||||
|
||||
Die Seite ist öffentlich erreichbar. Eine aktive Anmeldung oder Rolle ist nicht erforderlich.
|
||||
|
||||
## Technische Hinweise
|
||||
|
||||
- Das Formular verlangt eine gültige E-Mail-Adresse.
|
||||
- Firebase Auth entscheidet, ob die Adresse bekannt ist und ob eine Reset-Mail gesendet werden kann.
|
||||
- Fehlercodes werden mit `AuthMessagePipe` lokalisiert; unbekannte Fehler werden als `Unbekannter Fehler` angezeigt.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Rollen und Berechtigungen
|
||||
|
||||
## Rollenmodell
|
||||
|
||||
Die Anwendung definiert Rollen in `src/app/services/user/roles.ts`:
|
||||
|
||||
- `admin`
|
||||
- `user`
|
||||
- `member`
|
||||
- `leader`
|
||||
- `presenter`
|
||||
- `contributor`
|
||||
- `none`
|
||||
|
||||
Ein Benutzerprofil liegt in Firestore unter `users/{uid}`. Das Feld `role` enthält eine oder mehrere Rollen als Semikolon-getrennte Zeichenkette, zum Beispiel `leader;presenter`. Neue Benutzer werden mit Name, Akkordmodus und leerem `songUsage` angelegt; Rollen werden anschließend über die Benutzerverwaltung vergeben.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
Firebase Auth verwaltet die Anmeldung. `UserSessionService` beobachtet `authState`, liest das zugehörige Benutzerprofil aus `users/{uid}` und stellt daraus `user$`, `userId$`, `loggedIn$` und Benutzerlisten bereit.
|
||||
|
||||
Beim Login initialisiert der Service fehlende `songUsage`-Daten mit einem leeren Objekt. Passwort-Reset-Mails werden über Firebase Auth mit der in `environment.url` konfigurierten Rücksprung-URL versendet.
|
||||
|
||||
## Routenberechtigungen
|
||||
|
||||
`AuthGuard` leitet nicht angemeldete Benutzer nach `/user/login` um. `RoleGuard` prüft die in der Route definierten `requiredRoles`; Benutzer mit `admin` werden immer zugelassen.
|
||||
|
||||
Die Hauptrouten sind wie folgt geschützt:
|
||||
|
||||
- `/songs`: Login und Rolle `user`.
|
||||
- `/shows`: Login und Rolle `leader` oder `member`.
|
||||
- `/presentation`: Login und Rolle `presenter`.
|
||||
- `/user/info`: Login.
|
||||
- `/user/login`, `/user/new`, `/user/password` und `/user/password-send`: öffentlich.
|
||||
- `/brand`: öffentlich.
|
||||
- `/guest`: öffentlich.
|
||||
|
||||
Zusätzliche Modulrechte:
|
||||
|
||||
- `/songs/new`: Rolle `contributor`.
|
||||
- `/shows/new`: Rolle `leader`.
|
||||
|
||||
Wenn ein Benutzer keine passende Rolle besitzt, leitet `RoleGuard` auf eine rollenabhängige Standardroute um. Benutzer ohne Rollen landen auf `/brand/new-user`.
|
||||
|
||||
## Template- und Aktionsrechte
|
||||
|
||||
`RoleDirective` (`*appRole`) blendet UI-Elemente anhand der Benutzerrollen ein oder aus. `admin` erhält auch hier umfassenden Zugriff. Die Rollenprüfung wertet die Semikolon-getrennte Rollenliste aus.
|
||||
|
||||
`OwnerDirective` (`*appOwner`) begrenzt Aktionen auf Besitzer fachlicher Objekte, insbesondere bei Shows. Dadurch kann die Oberfläche zwischen eigenen und fremden Shows unterscheiden.
|
||||
|
||||
Diese Direktiven steuern die sichtbaren Aktionen in der Oberfläche. Fachliche Services prüfen zusätzlich einzelne Voraussetzungen, zum Beispiel ob ein aktueller Benutzer vorhanden ist.
|
||||
|
||||
## Firestore-Regeln
|
||||
|
||||
Die serverseitigen Regeln in `firestore.rules` sind bewusst grob:
|
||||
|
||||
- `users/{uid}`: Benutzer dürfen ihr eigenes Dokument erstellen und schreiben; Admins dürfen Benutzer schreiben und listen; angemeldete Benutzer dürfen Benutzerdaten lesen.
|
||||
- `guest/{guestId}`: Lesen ist öffentlich, Schreiben benötigt Login.
|
||||
- `songs`, `songs/{song}/files`, `shows`, `shows/{show}/songs` und `global`: Lesen und Schreiben benötigt Login.
|
||||
|
||||
Die Regeln unterscheiden bei den fachlichen Collections nicht nach Rollen. Rollenbasierte Zugriffskontrolle findet daher primär in Angular-Routing, Direktiven und Services statt.
|
||||
|
||||
## Administrative Funktionen
|
||||
|
||||
Einige Wartungsfunktionen verlangen in den Services explizit die Rolle `admin`:
|
||||
|
||||
- `UserSongUsageService.rebuildSongUsage()` baut `users/{uid}.songUsage` aus allen nicht archivierten Shows neu auf.
|
||||
- `ShowSongIndexService.rebuildShowSongIds()` schreibt den `songIds`-Index aller Shows neu.
|
||||
|
||||
Beide Funktionen sind für manuelle Migrationen vorgesehen und werden im README über `window.wgeneratorAdmin` beschrieben.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Technische Architektur
|
||||
|
||||
## Überblick
|
||||
|
||||
Wgenerator ist eine Angular-Anwendung mit Firebase als Backend. Die App wird als Single Page Application gebaut und über Firebase Hosting ausgeliefert. Authentifizierung, Firestore und Storage werden über `@angular/fire` eingebunden.
|
||||
|
||||
Der Anwendungseinstieg liegt in `src/main.ts`; die fachlichen Bereiche werden über `src/app/app-routing.module.ts` lazy geladen. Der Standardpfad `/` leitet nach `/songs` weiter.
|
||||
|
||||
## Frontend-Struktur
|
||||
|
||||
Der fachliche Code liegt unter `src/app/modules`:
|
||||
|
||||
- `songs`: Liedliste, Lieddetails, Bearbeitung, Anhänge, Textdarstellung, Tonarten und Transposition.
|
||||
- `shows`: Veranstaltungslisten, Show-Details, Bearbeitung, Song-Zuordnung, Archivierung, Veröffentlichung, Gastfreigaben und DOCX-Export.
|
||||
- `presentation`: Auswahl, Fernsteuerung und Monitoransicht für Live-Präsentationen.
|
||||
- `user`: Login, Logout, Registrierung, Passwort-Reset und Benutzerinformationen.
|
||||
- `brand`: Start- und Hinweisbereich für neue oder noch nicht berechtigte Benutzer.
|
||||
- `guest`: öffentliche Gastansicht für freigegebene Shows.
|
||||
|
||||
Gemeinsame Services liegen in `src/app/services`. Wiederverwendbare UI-Bausteine, Pipes, Guards und Direktiven liegen in `src/app/widget-modules`.
|
||||
|
||||
## Routing und Modulgrenzen
|
||||
|
||||
Die Hauptrouten sind:
|
||||
|
||||
- `/songs`: `SongsModule`, mit Authentifizierung und Rolle `user`.
|
||||
- `/shows`: `ShowsModule`, mit Authentifizierung und Rolle `leader` oder `member`.
|
||||
- `/presentation`: `PresentationModule`, mit Authentifizierung und Rolle `presenter`.
|
||||
- `/user`: `UserModule`, teilweise öffentlich und teilweise per `AuthGuard` geschützt.
|
||||
- `/brand`: `BrandModule`, öffentlich.
|
||||
- `/guest`: `GuestModule`, öffentlich.
|
||||
|
||||
Zusätzliche Modulrouten schränken einzelne Aktionen ein. Beispielsweise benötigt `/songs/new` die Rolle `contributor` und `/shows/new` die Rolle `leader`.
|
||||
|
||||
## Datenzugriff
|
||||
|
||||
Der technische Firestore-Zugriff ist in `DbService` gekapselt. Der Service stellt Wrapper für Collections und Dokumente bereit:
|
||||
|
||||
- `col$` und `doc$` liefern Live-Observables mit `idField`.
|
||||
- `add`, `set`, `update` und `delete` kapseln Schreiboperationen.
|
||||
- Query-Constraints werden bei Bedarf an `col$` übergeben.
|
||||
|
||||
Fachservices bauen darauf auf:
|
||||
|
||||
- `SongDataService` und `SongService` verwalten `songs`.
|
||||
- `FileDataService`, `FileService` und `UploadService` verwalten Firestore-Metadaten und Firebase-Storage-Dateien zu Songs.
|
||||
- `ShowDataService`, `ShowService`, `ShowSongDataService` und `ShowSongService` verwalten Shows und deren eingebettete Show-Songs.
|
||||
- `GuestShowDataService` und `GuestShowService` verwalten öffentliche Gastfreigaben.
|
||||
- `UserSessionService`, `UserService` und `UserSongUsageService` verwalten Authentifizierung, Benutzerprofile, Rollen und Nutzungszähler.
|
||||
- `ConfigService` und `GlobalSettingsService` lesen beziehungsweise schreiben Dokumente unter `global`.
|
||||
|
||||
Mehrere Datenströme werden mit `shareReplay` gecacht, um Firestore-Listener bei Navigationen wiederzuverwenden.
|
||||
|
||||
## Authentifizierung und Autorisierung
|
||||
|
||||
Firebase Auth liefert den Login-Status. `UserSessionService` verbindet den Auth-User mit dem Firestore-Dokument `users/{uid}`. Rollen werden im Feld `role` als Semikolon-getrennte Zeichenkette gespeichert.
|
||||
|
||||
`AuthGuard` schützt Routen, die einen Firebase-Login benötigen. `RoleGuard` liest `requiredRoles` aus der Route-Data-Konfiguration und erlaubt Zugriff, wenn der Benutzer mindestens eine passende Rolle besitzt. `admin` gilt in der Anwendung als übergeordnete Rolle.
|
||||
|
||||
Die Firestore-Regeln erlauben angemeldeten Benutzern breite Lese- und Schreibzugriffe auf fachliche Collections. Deshalb sind UI-Guards und Services fachlich wichtig, ersetzen aber keine feingranulare serverseitige Autorisierung.
|
||||
|
||||
## Präsentationsarchitektur
|
||||
|
||||
Die Präsentation wird über Firestore synchronisiert:
|
||||
|
||||
- `global/static.currentShow` enthält die aktuell ausgewählte Show.
|
||||
- Das Show-Dokument enthält Präsentationsfelder wie `presentationSongId`, `presentationSection`, `presentationZoom`, `presentationBackground`, `presentationDynamicCaption` und `presentationDynamicText`.
|
||||
- Die Remote-Ansicht schreibt diese Werte.
|
||||
- Die Monitoransicht liest die Werte live und rendert den aktuellen Zustand.
|
||||
|
||||
Damit können mehrere Browserfenster über Firestore als gemeinsamen Zustand zusammenarbeiten.
|
||||
|
||||
## Firebase und Hosting
|
||||
|
||||
Die Firebase-Konfiguration wird über `src/environments/firebase.ts` importiert. `environment.ts` und `environment.prod.ts` unterscheiden `production`, nutzen aber dieselbe URL `https://worshipgenerator.web.app`.
|
||||
|
||||
`firebase.json` konfiguriert:
|
||||
|
||||
- Firestore-Regeln aus `firestore.rules`.
|
||||
- Firestore-Indexdatei `firestore.indexes.json`.
|
||||
- Hosting aus `dist/wgenerator/browser`.
|
||||
- SPA-Rewrite auf `/index.html`.
|
||||
- Cache-Header für HTML-Routen und statische JS/CSS-Dateien.
|
||||
- Emulator-Ports für Firestore, Realtime Database und Hosting.
|
||||
|
||||
## Build, Tests und Qualität
|
||||
|
||||
Angular nutzt den Builder `@angular/build:application`. LESS ist als Component-Style-Sprache konfiguriert; globale Styles kommen aus `src/custom-theme.scss`, `src/styles/styles.less` und `src/styles/shadow.less`.
|
||||
|
||||
Unit-Tests laufen über den Angular Unit-Test-Builder mit Vitest und `src/test-vitest.ts` als Setup-Datei. Linting läuft über Angular ESLint.
|
||||
Reference in New Issue
Block a user