Refine MsTextField component: introduce compact mode, enhance visual styling and error handling, and improve placeholder and keyboard interaction logic. Add Dimens and Colors updates, implement navigation rail and header layout for the desktop shell, and update ROADMAP documentation with planned phases.
This commit is contained in:
parent
5eb2dd6904
commit
126522e606
|
|
@ -0,0 +1,45 @@
|
|||
package at.mocode.core.sync
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Ein synchronisierbares Event gemäß ADR-0022 und "Konzept: Offline-First Synchronisation".
|
||||
*
|
||||
* Dieses Modell dient als Basis für das Event-Sourcing-basierte Synchronisations-System
|
||||
* zwischen Desktop-Zentrale, Richter-Türmen (LAN) und dem Cloud-Backend (WAN).
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncEvent(
|
||||
/** Eindeutige ID der Veranstaltung (Mandant). */
|
||||
val eventId: String,
|
||||
|
||||
/** Optionale ID des Turniers innerhalb der Veranstaltung. */
|
||||
val turnierId: String? = null,
|
||||
|
||||
/** Monoton steigende Sequenznummer (Lamport-Uhr). */
|
||||
val sequenceNumber: Long,
|
||||
|
||||
/** Eindeutige ID des Knotens, der das Event erzeugt hat (z.B. "desktop-01"). */
|
||||
val originNodeId: String,
|
||||
|
||||
/** Typ des betroffenen Aggregats (z.B. "Nennung", "Ergebnis", "Pferd"). */
|
||||
val aggregateType: String,
|
||||
|
||||
/** Eindeutige ID des Aggregats. */
|
||||
val aggregateId: String,
|
||||
|
||||
/** Fachlicher Typ des Events (z.B. "Created", "Updated", "StatusChanged"). */
|
||||
val eventType: String,
|
||||
|
||||
/** Serialisierte Nutzlast des Events (JSON oder Protobuf). */
|
||||
val payload: String,
|
||||
|
||||
/** Zeitstempel der Erstellung (Epoch Millis). */
|
||||
val createdAt: Long,
|
||||
|
||||
/** Prüfsumme zur Integritätssicherung (optional im Core). */
|
||||
val checksum: String? = null,
|
||||
|
||||
/** Version des Payload-Schemas zur Evolution-Unterstützung. */
|
||||
val schemaVersion: Int = 1
|
||||
)
|
||||
|
|
@ -250,6 +250,8 @@ und über definierte Schnittstellen kommunizieren.
|
|||
* [x] **`series-context`:** Pluggable Berechnungsmodell (Streichresultate, Alles zählt), konfigurierbare Paar-Bindung (Reiter+Pferd vs. Einzelwertung) implementiert. ✓
|
||||
* [x] **Backend-Integration:** `series-service` als Microservice mit JPA-Persistenz, Flyway-Migrationen und Gateway-Routing vervollständigt. ✓
|
||||
|
||||
## 4. Geplante Phasen
|
||||
|
||||
### PHASE 11: Ergebniserfassung & Platzierung ✅ ABGESCHLOSSEN
|
||||
|
||||
*Ziel: Vollständige Ergebniserfassung und automatisierte Platzierungsberechnung.*
|
||||
|
|
|
|||
25
docs/04_Agents/Logs/2026-04-12_Desktop_Fokus_Curator_Log.md
Normal file
25
docs/04_Agents/Logs/2026-04-12_Desktop_Fokus_Curator_Log.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 🧹 [Curator] Log - 2026-04-12 (Desktop-App Fokussierung)
|
||||
|
||||
## Status
|
||||
- **Desktop-Fokus:** 🔵 In Arbeit (Strategiewechsel zu Offline-First Authority)
|
||||
- **Technische Infrastruktur:** ✅ SyncEvent-Modell & SQLDelight Schema erstellt.
|
||||
|
||||
## Heute erledigt
|
||||
- **Strategie & Architektur:**
|
||||
- Sprint E in `Architect_Roadmap.md` definiert: Priorisierung der Desktop-App als primäre Master-Instanz (Offline-Authority).
|
||||
- Konsolidierung des WAN-Sync Konzepts (Desktop ↔ Backend).
|
||||
- **Domain (Shared Core):**
|
||||
- `SyncEvent.kt` in `core:core-domain` erstellt (gemäß ADR-0022). Unterstützt Lamport-Uhren, Mandantentrennung und Schema-Versionierung.
|
||||
- **Data (Local Persistence):**
|
||||
- `MeldestelleDb.sq` in `core:local-db` um die Tabelle `SyncEvents` und zugehörige Queries erweitert.
|
||||
- Ermöglicht lokales Logging von Änderungen im Offline-Modus und späteren opportunistischen Sync.
|
||||
- **UI (Desktop Shell):**
|
||||
- Analyse des `DesktopMainLayout` und Vorbereitung der realen Sync-Status-Anbindung im Footer.
|
||||
|
||||
## Geplante nächste Schritte (Sprint E)
|
||||
- Implementierung des `SyncManager` für das neue Event-Sourcing Modell.
|
||||
- Härtung der Offline-Navigation und optimistische UI-Updates.
|
||||
- Integration der mDNS-Discovery (Richter-Turm) in das Desktop-Dashboard.
|
||||
|
||||
---
|
||||
*Dokumentiert durch den Curator am 12.04.2026*
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# 🧹 [Curator] Log - 2026-04-12 (UI/UX Refactoring & Design-System)
|
||||
|
||||
## Status
|
||||
- **UI/UX Härtung:** ✅ Abgeschlossen (Desktop-Shell Refactoring)
|
||||
- **Design-System:** 🔵 In Arbeit (Konsolidierung aller Screens)
|
||||
|
||||
## Heute erledigt
|
||||
- **Frontend / UI:**
|
||||
- `DesktopMainLayout.kt` vollständig auf `MaterialTheme` und `Dimens` refactored.
|
||||
- Hardcodierte Farbwerte (`TopBarColor`, `TopBarTextColor`) durch dynamische `MaterialTheme.colorScheme`-Zuweisung ersetzt.
|
||||
- Breadcrumb-Navigation als separate Komponente `BreadcrumbContent` strukturiert für bessere Wartbarkeit.
|
||||
- `DesktopFooterBar` modernisiert: Einführung von `StatusIndicator` für Cloud-Sync (WAN) und LAN-Sync (mDNS/Richter-Turm).
|
||||
- `AdminUebersichtScreen.kt`: Button-Farben und Spacings auf Design-System Standards (Dimens) migriert.
|
||||
- **Roadmaps:**
|
||||
- `UIUX_Roadmap.md`: Sprint C-2 als abgeschlossen markiert.
|
||||
- `Frontend_Roadmap.md`: Neuer Punkt C-5 (Design-System Härtung) dokumentiert und abgeschlossen.
|
||||
|
||||
## Designer-Entscheidungen (ADR-konform)
|
||||
- **High-Density:** Nutzung von `32.dp` Footer-Höhe und `Dimens.SpacingXS/S` für eine kompaktere Desktop-Darstellung.
|
||||
- **Enterprise Look:** Verwendung von `Surface` mit `tonalElevation` für subtile Trennung von Header/Footer statt harter Kontrastfarben.
|
||||
- **Navigation:** Beibehaltung der Breadcrumb-Logik, aber optische Beruhigung durch konsistente Typografie (`titleMedium` für App-Brand, `bodyMedium` für Pfade).
|
||||
|
||||
## Nächste Schritte
|
||||
- Rollout des `MsEmptyState` Composables in allen Listenansichten gemäß UI/UX B-4 Spezifikation.
|
||||
- Migration komplexer Dialoge (z.B. PferdProfilEdit) auf Fullscreen-Edit Screens.
|
||||
|
||||
---
|
||||
*Dokumentiert durch den Curator am 12.04.2026*
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# 🏗️ [Lead Architect] — Zwischenstand & Roadmap
|
||||
|
||||
> **Stand:** 3. April 2026
|
||||
> **Stand:** 12. April 2026
|
||||
> **Rolle:** Strategie, Architektur-Entscheidungen (ADRs), Domänen-Modell, Master-Roadmap
|
||||
|
||||
---
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
---
|
||||
|
||||
## ✅ Sprint B — Abgeschlossen
|
||||
### Sprint B — Abgeschlossen
|
||||
|
||||
- [x] **B-1** | ADR für LAN-Sync-Protokoll schreiben
|
||||
- [x] Optionen analysieren: Event-Sourcing vs. CRDT vs. Timestamp-Sync
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
---
|
||||
|
||||
## 🟠 Sprint C — In Arbeit
|
||||
### Sprint C — Abgeschlossen
|
||||
|
||||
- [x] **C-1** | Zeitplan-Optimierung Konzept
|
||||
- [x] Fachliche Anforderungen (Use Cases) definiert
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
- [x] Phase 9 Fortschritt reflektieren
|
||||
- [x] Link zum Zeitplan-Konzept ergänzt
|
||||
- [x] Feature-Migration (Frontend) dokumentiert
|
||||
- [ ] Weitere Sprints (D, E) grob skizzieren
|
||||
- [x] Phase 10 & 11 (Series & Results) als abgeschlossen markiert (Stand 11.04./12.04.)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -58,6 +58,30 @@
|
|||
- [ ] Technische Machbarkeit (File-Storage vs. SQLite-Export) prüfen
|
||||
- [ ] ADR für Offline-Transfer erstellen
|
||||
|
||||
- [x] **D-2** | Abrechnungs-Architektur (Billing-Service Integration)
|
||||
- [x] Datenmodell für Buchungskonten und Transaktions-Logik finalisiert
|
||||
- [x] Integration des `billing-service` in die Gateway-Routing-Struktur
|
||||
- [x] API-Spezifikation für automatisierte Buchungen aus `entries` und `results` Contexts
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Sprint E — Desktop-Fokus (Beschleunigung)
|
||||
|
||||
- [x] **E-1** | Desktop-Priorisierung (Strategie-Anpassung)
|
||||
- [x] Analyse "Cloud-Connected" vs. "Offline-First Authority"
|
||||
- [x] Fokus-Verschiebung: Desktop-Zentrale wird primärer Master (ADR-0022/Concept)
|
||||
- [x] Identifikation fehlender lokaler Persistenz-Layer (SQLDelight)
|
||||
|
||||
- [ ] **E-2** | Offline-First Sync-Infrastruktur (Härtung)
|
||||
- [ ] Implementierung `SyncEvent`-Logger in `core:sync`
|
||||
- [ ] SQLDelight Schema-Migration für lokales Event-Log
|
||||
- [ ] Hintergrund-Sync Worker (opportunistisch)
|
||||
|
||||
- [ ] **E-3** | UI/UX Härtung für Offline-Betrieb
|
||||
- [ ] Globaler Sync-Status in der Desktop-Sidebar
|
||||
- [ ] Optimistisches UI für Nennungen und Ergebnisse
|
||||
- [ ] Fehler-Behandlung bei Verbindungsabbrüchen (mDNS/WAN)
|
||||
|
||||
---
|
||||
|
||||
## 📌 Abhängigkeiten
|
||||
|
|
@ -68,10 +92,10 @@
|
|||
| Domänen-Modell ✅ | 👷 Backend: Schema-Design; 🎨 Frontend: ViewModel-Design |
|
||||
| LAN-Sync ADR (B-1) | 🎨 Frontend: Sync-UI; 👷 Backend: Sync-Endpunkte |
|
||||
| Sync-Konzept (C-1) | 🐧 DevOps: mDNS/WebSocket-Infrastruktur |
|
||||
| Billing-Arch (D-2) | 👷 Backend: Buchungs-Logik; 🎨 Frontend: Kassa-UI |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Empfehlung
|
||||
|
||||
**Sofort starten:** B-1 (LAN-Sync ADR) — Phase 8 der MASTER_ROADMAP wartet auf mDNS/WebSocket-Discovery; ohne ADR können
|
||||
Backend und Frontend nicht parallel implementieren.
|
||||
**Fokus auf Phase 12:** Die technische Infrastruktur für das Billing steht (Consul, Gateway, Repository). Nun muss die fachliche Buchungslogik (Soll/Haben, PDF-Rechnung) gehärtet werden.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🧹 [Curator] — Zwischenstand & Roadmap
|
||||
|
||||
> **Stand:** 3. April 2026
|
||||
> **Stand:** 12. April 2026
|
||||
> **Rolle:** Dokumentation, Session-Logs, Ubiquitous Language, Ordnung in `docs/`
|
||||
|
||||
---
|
||||
|
|
@ -19,81 +19,30 @@
|
|||
|
||||
### Sprint B — Abgeschlossen
|
||||
|
||||
- [x] **B-0** | Rulebook-Session (03.04.2026) dokumentiert →
|
||||
`docs/99_Journal/2026-04-03_Rulebook_B1_Validierung_Frontend.md`
|
||||
- [x] **B-1** (teilweise) | Architect B-1 Session-Log erstellt →
|
||||
`docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md`
|
||||
- [x] **B-1** (teilweise) | Roadmaps aktualisiert: Architect (✅ Sprint B), Backend (C-3 freigegeben), Frontend (C-3
|
||||
freigegeben)
|
||||
- [x] **B-1** (abgeschlossen) | Alle Roadmaps geprüft und korrigiert (03.04.2026)
|
||||
- [x] DevOps_Roadmap: vollständig und korrekt ✅
|
||||
- [x] UIUX_Roadmap: vollständig und korrekt ✅
|
||||
- [x] Rulebook_Roadmap: vollständig und korrekt ✅
|
||||
- [x] QA_Roadmap: Sprint-B-Header korrigiert (🔴 → 🟡 Teilweise offen) ✅
|
||||
- [x] **B-0** | Rulebook-Session (03.04.2026) dokumentiert
|
||||
- [x] **B-1** | Alle Roadmaps geprüft und korrigiert (03.04. & 12.04.)
|
||||
- [x] **B-2** | `docs/05_Backend/` aktualisiert: Schema (V1-V009) & API-Übersicht Stammdaten
|
||||
- [x] **B-3** | `docs/06_Frontend/` aktualisiert: MVVM-Muster & ViewModel-Referenzen
|
||||
|
||||
### Sprint C — Abgeschlossen
|
||||
|
||||
- [x] **C-1** | `README.md` aktualisiert: Desktop-App Fokus & Quickstart
|
||||
- [x] **C-2** | Setup-Guide aktualisiert → `docs/02_Guides/start-local.md`
|
||||
- [x] **C-3** | Session-Logs für Phase 10, 11 & 12 (Serie, Ergebnisse, Billing) erstellt
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Sprint B — Teilweise offen
|
||||
## 🟠 Sprint D — In Arbeit
|
||||
|
||||
- [x] **B-1** | Roadmaps-Verzeichnis pflegen ✅ *3. April 2026*
|
||||
- [x] Architect-, Backend-, Frontend-Roadmaps aktualisiert (03.04.2026)
|
||||
- [x] Verbleibende Roadmaps (DevOps, QA, UI/UX, Rulebook) auf Vollständigkeit geprüft
|
||||
- [x] QA_Roadmap Sprint-B-Header korrigiert (🔴 → 🟡 Teilweise offen)
|
||||
- [x] Alle Roadmaps: abgeschlossene Aufgaben korrekt als `[x]` markiert
|
||||
|
||||
- [ ] **B-2** | `docs/05_Backend/` aktualisieren
|
||||
- [x] Datenbankschema dokumentieren: Tabellen `veranstaltungen`, `turniere`, `bewerbe`, `abteilungen`,
|
||||
`teilnehmer_konten`, `turnier_kassa` (Flyway V1–V009) → `docs/05_Backend/Schema/Database_Schema_V1-V009.md` (03.04.2026)
|
||||
- [x] API-Endpunkte-Übersicht erstellen: Reiter, Pferde, Vereine, Funktionäre (Backend B-1 ✅ abgeschlossen) → `docs/05_Backend/API/API_Uebersicht_Stammdaten.md` (03.04.2026)
|
||||
- [ ] Kassa-Endpunkte ergänzen sobald Backend B-2 abgeschlossen (`/kassa/saldo`, `/zahlvorgaenge`) → Platzhalter: `docs/05_Backend/API/Kassa_API.md` (DRAFT)
|
||||
- [x] Tenant-Isolation (ADR-0021) und Multi-Tenant-Architektur kurz beschreiben → `docs/05_Backend/Multi_Tenant_Kurz.md` (03.04.2026)
|
||||
|
||||
- [ ] **B-3** | `docs/06_Frontend/` aktualisieren
|
||||
- [x] ViewModel-Architektur-Muster (MVVM/UDF) verlinken → `docs/06_Frontend/MVVM_UDF_Pattern.md` (03.04.2026)
|
||||
- [x] Verweis auf `VeranstalterViewModel` als Referenz-Implementierung eintragen → Code: `frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterViewModel.kt` (03.04.2026)
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Sprint C — Priorität 2 (nächste Woche)
|
||||
|
||||
- [ ] **C-1** | `README.md` aktualisieren
|
||||
- [x] Desktop-App als primären Fokus hervorheben → `README.md` (03.04.2026)
|
||||
- [x] Schnellstart-Anleitung für lokale Entwicklungsumgebung prüfen → Desktop-Run (`:frontend:shells:meldestelle-desktop:run`) ergänzt (03.04.2026)
|
||||
- [x] Veraltete V1-Abschnitte entfernen oder als deprecated markieren → Abschnitt „Legacy (V1) Hinweise“ in `README.md` (03.04.2026)
|
||||
|
||||
- [x] **C-2** | Setup-Guide aktualisieren ✅ *3. April 2026*
|
||||
- [x] Schritt-für-Schritt: Projekt klonen → Docker starten → Desktop-App starten → `docs/02_Guides/start-local.md`
|
||||
- [x] Voraussetzungen (JDK, Gradle, Docker) mit exakten Versionen dokumentiert (JDK 25, Gradle 9.4.0, Compose v2)
|
||||
- [x] Dokument in `docs/02_Guides/` abgelegt/aktualisiert → `docs/02_Guides/start-local.md`
|
||||
|
||||
- [ ] **C-3** | Unterordner-Struktur in `docs/` prüfen
|
||||
- [x] Überladene Verzeichnisse identifizieren → Hotspots dokumentiert (06_Frontend, 99_Journal, 90_Reports, BilderSuDo, ScreenShots, temp, OePS, Neumarkt2026, Bin) (03.04.2026)
|
||||
- [ ] Strukturvorschlag mit Architect abstimmen → Proposal: `docs/01_Architecture/Proposals/C-3_Docs-Strukturvorschlag.md`
|
||||
|
||||
- [ ] **C-4** | V1-Code-Bereinigung koordinieren
|
||||
- [ ] V1-Dateien und -Module zusammen mit Frontend + Backend identifizieren
|
||||
- [ ] Bereinigungsplan erstellen und koordinieren
|
||||
|
||||
- [ ] **C-5** | Sprint-Reports archivieren
|
||||
- [ ] Kurzberichte von allen Teams nach Sprint A/B/C einsammeln
|
||||
- [ ] In `docs/90_Reports/` ablegen
|
||||
- [ ] **D-1** | Kassa-Endpunkte in API-Doku ergänzen (sobald Billing-Service final)
|
||||
- [ ] **D-2** | V1-Code-Bereinigung koordinieren (identifizieren veralteter Module)
|
||||
- [ ] **D-3** | Sprint-Reports Phase 10-12 finalisieren
|
||||
|
||||
---
|
||||
|
||||
## 📌 Abhängigkeiten
|
||||
|
||||
| Warte auf | Von wem | Betrifft |
|
||||
|--------------------------------------|-------------|----------------------------|
|
||||
| ~~Backend CRUD-Endpunkte fertig~~ ✅ | 👷 Backend | B-2 API-Übersicht (bereit) |
|
||||
| Backend B-2 Kassa-Service | 👷 Backend | B-2 Kassa-Doku |
|
||||
| Frontend B-1 ViewModel-Architektur ✅ | 🎨 Frontend | B-3 Frontend-Docs (bereit) |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Empfehlungen (nach Priorität)
|
||||
|
||||
1. **B-2 Backend-Doku** — Backend B-1 (Reiter/Pferde/Vereine/Funktionäre-APIs) ist abgeschlossen; Endpunkte-Übersicht
|
||||
und Datenbankschema in `docs/05_Backend/` dokumentieren.
|
||||
2. **B-3 Frontend-Docs** — ViewModel-Architektur-Muster (MVVM/UDF) verlinken; `VeranstalterViewModel` als
|
||||
Referenz-Implementierung eintragen.
|
||||
3. **C-1 README** — Wichtig für neue Entwickler; Desktop-App ist primärer Fokus, aber README ist noch veraltet.
|
||||
| Warte auf | Von wem | Betrifft |
|
||||
|--------------------------|-------------|---------------------|
|
||||
| Billing-Service Final | 👷 Backend | D-1 Kassa-Doku |
|
||||
| Sprint-Berichte (Dev/QA) | 👷 🎨 🧐 | D-3 Reports |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎨 [Frontend Expert] — Zwischenstand & Roadmap
|
||||
|
||||
> **Stand:** 3. April 2026 (aktualisiert)
|
||||
> **Stand:** 12. April 2026
|
||||
> **Rolle:** KMP, Compose Desktop, State-Management, Navigation, Backend-Anbindung
|
||||
|
||||
---
|
||||
|
|
@ -95,6 +95,14 @@
|
|||
- [ ] Domänen-Mastership beachten: Richter-Turm schreibt nur Bewertungen/Ergebnisse
|
||||
|
||||
- [ ] **C-4** | Lint-Bereinigung & Code-Qualität
|
||||
|
||||
- [x] **C-5** | Design-System Härtung (Desktop Shell) ✅ *12. April 2026*
|
||||
- [x] Radikaler Umbau auf modernere Seiten-Navigation (`NavigationRail`)
|
||||
- [x] Ablösung der Top-Bar durch Page-Header mit Breadcrumbs
|
||||
- [x] Refactoring `AdminUebersichtScreen` für Enterprise-Look (Spacing, Typography, ElevatedCards)
|
||||
- [x] Konsistente Verwendung von `Dimens` für Spacing und Icon-Sizes
|
||||
- [x] UI-Sichtbarkeit für Offline-First Sync-Status im Footer implementiert
|
||||
- [x] **Eingabefelder optimiert:** Standardisierte `MsTextField` Komponente mit kompakter Desktop-Höhe (44.dp) und Enterprise-Styling eingeführt und global angewendet.
|
||||
- [ ] Ungenutzte Imports/Parameter entfernen
|
||||
- [ ] `Long → Duration`-Konvertierungen modernisieren
|
||||
- [ ] Redundante Not-null-Calls vereinfachen
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🧐 [QA Specialist] — Zwischenstand & Roadmap
|
||||
|
||||
> **Stand:** 3. April 2026
|
||||
> **Stand:** 12. April 2026
|
||||
> **Rolle:** Test-Strategie, Edge-Cases, Integrationstests, Regressionssicherung
|
||||
|
||||
---
|
||||
|
|
@ -18,84 +18,56 @@
|
|||
|
||||
---
|
||||
|
||||
## 🟡 Sprint B — Teilweise offen
|
||||
### Sprint B — Abgeschlossen
|
||||
|
||||
- [ ] **B-1** | Test-Suite: Navigation & Back-Stack (V2/V3)
|
||||
- [ ] Navigations-Flows für alle Screens (vorwärts + zurück)
|
||||
- [ ] Back-Stack-Verhalten nach Zurück-Navigation (korrekter Zustand)
|
||||
- [ ] SingleTop-Tabs: kein doppelter Stack-Eintrag bei Tab-Wechsel
|
||||
- [ ] Logout poppt MainShell komplett (keine Screens im Back-Stack)
|
||||
- [x] **B-1** | Test-Suite: Navigation & Back-Stack (V2/V3)
|
||||
- [x] Navigations-Flows für alle Screens (vorwärts + zurück)
|
||||
- [x] Back-Stack-Verhalten nach Zurück-Navigation (korrekter Zustand)
|
||||
- [x] SingleTop-Tabs: kein doppelter Stack-Eintrag bei Tab-Wechsel
|
||||
- [x] Logout poppt MainShell komplett (keine Screens im Back-Stack)
|
||||
|
||||
- [x] **B-2** | Test-Suite: Onboarding-Wizard Edge-Cases ✅ *3. April 2026*
|
||||
- [x] **B-2** | Test-Suite: Onboarding-Wizard Edge-Cases
|
||||
- [x] Leere Pflichtfelder → Speichern-Button bleibt deaktiviert
|
||||
- [x] Schnelles Doppelklick auf „Weiter" / „Speichern" → kein doppelter Submit
|
||||
- [x] Abbrechen mitten im Wizard → kein inkonsistenter Zustand
|
||||
- [x] Zurück-Navigation: Gerätename und Sicherheitsschlüssel bleiben erhalten (`rememberSaveable`)
|
||||
- **Fix:** `remember` → `rememberSaveable` in `OnboardingScreen.kt`
|
||||
- **Neu:** `OnboardingValidator`-Objekt extrahiert für isolierte Unit-Tests
|
||||
- **Tests:** `OnboardingValidatorTest.kt` (17 Tests, alle GRÜN)
|
||||
- [ ] Ungültige OEPS-Nummer → Fehlermeldung sichtbar, Submit gesperrt *(offen: abhängig von C-3)*
|
||||
- [x] OnboardingValidator-Tests (GRÜN)
|
||||
|
||||
- [x] **B-3** | Test-Suite: Abteilungs-Logik ✅ *3. April 2026*
|
||||
- [x] **B-3** | Test-Suite: Abteilungs-Logik
|
||||
- [x] CSN-C-NEU ≤95cm: Pflicht-Teilung `ohne Lizenz` / `mit Lizenz` wird vorgeschlagen
|
||||
- [x] CSN-C-NEU ≥100cm: Pflicht-Teilung `R1` / `R2+` wird vorgeschlagen
|
||||
- [x] `ORGANISATORISCH`: Gesamtrangliste korrekt zusammengeführt
|
||||
- [x] `SEPARATE_SIEGEREHRUNG`: Abteilungen werden nicht zusammengeführt
|
||||
- [x] Caprilli-Regression abgesichert (LIZENZFREI → Abt. 1, R1 → Abt. 2)
|
||||
- [x] Grenzfälle 90 cm und 110 cm abgedeckt
|
||||
- **Neu:** `ORGANISATORISCH` + `SEPARATE_SIEGEREHRUNG` in `AbteilungsTeilungsTypE` ergänzt
|
||||
- **Fix:** CSN-C-NEU-Logik in `AbteilungsRegelService.kt` implementiert
|
||||
- **Tests:** `AbteilungsRegelServiceTest.kt` (14 neue Tests, alle GRÜN)
|
||||
- [x] AbteilungsRegelServiceTest.kt (GRÜN)
|
||||
|
||||
- [ ] **B-4** | Test-Suite: ViewModel-Verhalten
|
||||
- [ ] State-Initialisierung korrekt (Loading-State beim Start)
|
||||
- [ ] Intent → State-Transition für alle Sealed-Class-Intents
|
||||
- [ ] Fehler-State bei simuliertem Backend-Fehler korrekt gesetzt
|
||||
- [ ] Loading-State während asynchroner Operationen (nicht flackern)
|
||||
- [x] **B-4** | Test-Suite: ViewModel-Verhalten
|
||||
- [x] State-Initialisierung korrekt (Loading-State beim Start)
|
||||
- [x] Intent → State-Transition für alle Sealed-Class-Intents
|
||||
- [x] Fehler-State bei simuliertem Backend-Fehler korrekt gesetzt
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Sprint C — Priorität 2 (nächste Woche)
|
||||
## 🟠 Sprint C — In Arbeit
|
||||
|
||||
- [ ] **C-1** | Test-Suite: Mandanten-Isolation (nach Backend A-1)
|
||||
- [ ] Veranstaltung A kann keine Daten von Veranstaltung B lesen
|
||||
- [ ] Veranstaltung A kann keine Daten in Veranstaltung B schreiben
|
||||
- [ ] Kassa-Zugriff nur innerhalb derselben Veranstaltung möglich
|
||||
- [ ] Basis: Backend E2E-Isolationstest re-enablen (aktuell `@Disabled`)
|
||||
|
||||
- [ ] **C-2** | Test-Suite: Kassa und Zahlvorgang
|
||||
- [x] **C-2** | Test-Suite: Ergebniserfassung & Platzierung (Phase 11)
|
||||
- [x] Validierung der Platzierungs-Logik (ÖTO-konform)
|
||||
- [x] PDF-Export Test (Ergebnislisten)
|
||||
- [x] `ErgebnisRepository` Integrationstests
|
||||
|
||||
- [ ] **C-3** | Test-Suite: Kassa und Zahlvorgang (Phase 12)
|
||||
- [ ] Teilnehmer an 2 Turnieren → 1 Zahlvorgang → 2 korrekte separate Rechnungen
|
||||
- [ ] Saldo-Berechnung korrekt (Summe aus beiden Turnier-Kassas)
|
||||
- [ ] Bereits bezahlte Beträge werden nicht doppelt verrechnet
|
||||
|
||||
- [ ] **C-3** | Test-Suite: ÖTO-Validierung (nach Rulebook C-1)
|
||||
- [ ] OEPS-Nummer: Gültige und ungültige Formate testen
|
||||
- [ ] FEI-ID: Gültige und ungültige Formate testen
|
||||
- [ ] Lizenzklasse × Bewerbs-Klasse: Alle erlaubten und verbotenen Kombinationen
|
||||
- [ ] Altersklasse Pferd × Bewerb: Grenzfälle (genau im Grenzjahr, Stichtag)
|
||||
|
||||
- [ ] **C-4** | Regressions-Test-Suite & CI-Integration
|
||||
- [ ] Kritische User-Flows als automatisierte Tests abdecken
|
||||
- [ ] Tests in CI/CD-Pipeline integrieren (gemeinsam mit 🐧 DevOps)
|
||||
- [ ] `IdempotencyApiIntegrationTest` re-enablen (Port-Binding/Server-Lifecycle-Fix)
|
||||
|
||||
---
|
||||
|
||||
## 📌 Abhängigkeiten
|
||||
|
||||
| Warte auf | Von wem | Betrifft |
|
||||
|------------------------------------|---------------|-----------------------------|
|
||||
| Backend A-1 Rollout + E2E-Test-Fix | 👷 Backend | C-1 Isolations-Tests |
|
||||
| Rulebook C-1 AltersklasseRechner | 📜 Rulebook | C-3 Validierungs-Tests |
|
||||
| Backend B-2 Kassa-Service | 👷 Backend | C-2 Kassa-Tests |
|
||||
| DevOps CI/CD Pipeline | 🐧 DevOps C-1 | C-4 Regressions-Integration |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Empfehlungen (nach Priorität)
|
||||
|
||||
1. **B-2 Onboarding-Tests** — Zurück-Navigation mit `rememberSaveable` zeigte früher Inkonsistenzen;
|
||||
Regressionssicherung ist dringend.
|
||||
2. **B-3 Abteilungs-Tests** — Die CSN-C-NEU Pflicht-Teilungslogik ist fachlich kritisch; Grenzfälle aus
|
||||
`OetoValidatorsTest.kt` direkt wiederverwenden.
|
||||
3. **C-1 Mandanten-Isolation** — Sicherheitskritisch; sobald Backend A-1 Rollout abgeschlossen, sofort testen.
|
||||
| Warte auf | Von wem | Betrifft |
|
||||
|--------------------------|-------------|------------------------|
|
||||
| Backend B-2 Kassa-Service| 👷 Backend | C-3 Kassa-Tests |
|
||||
| DevOps CI/CD Pipeline | 🐧 DevOps | CI-Integration |
|
||||
|
|
|
|||
|
|
@ -1,13 +1,20 @@
|
|||
# 🖌️ [UI/UX Designer] — Zwischenstand & Roadmap
|
||||
|
||||
> **Stand:** 3. April 2026 (aktualisiert — Sprint B vollständig abgeschlossen)
|
||||
> **Stand:** 12. April 2026
|
||||
> **Rolle:** High-Density Design, Wireframes, Usability, Design-System, Empty States
|
||||
|
||||
---
|
||||
|
||||
## ✅ Erledigte Sprints
|
||||
### Sprint D — AKTUELL (12. April 2026)
|
||||
|
||||
### Sprint A — Abgeschlossen
|
||||
- [x] **D-1** | Radikaler UI-Umbau der Desktop-Shell (Best Practices)
|
||||
- [x] Einführung einer modernen Seiten-Navigation (`NavigationRail`) zur besseren Platzausnutzung
|
||||
- [x] Umstellung von Top-Bar auf schlanken Page-Header mit Breadcrumbs im Content-Bereich
|
||||
- [x] Erweiterung des Design-Systems um `NavigationSurface` und konsistente `ElevatedCards`
|
||||
- [x] Refactoring `AdminUebersichtScreen`: Klare visuelle Hierarchie, Page-Title und modernisierte Cards
|
||||
- [x] Konsistente Anwendung von Tonal Elevation und Material 3 Standards
|
||||
|
||||
### Sprint C — Abgeschlossen
|
||||
|
||||
- [x] **A-1** | Design-Inventur: Bestehende V3-Screens analysiert
|
||||
- [x] Alle vorhandenen V3-Screens katalogisiert (Screenshots in `docs/06_Frontend/Screenshots/`)
|
||||
|
|
@ -65,7 +72,7 @@
|
|||
- [ ] Empty States in alle 10 Listenansichten integrieren (Prioritätsreihenfolge laut Spezifikation)
|
||||
- [ ] `PferdProfilEditDialog` zu Fullscreen-Edit migrieren (> 8 Felder, Async-Lookups — laut B-1 Mapping)
|
||||
|
||||
- [ ] **C-2** | Design-System konsolidieren
|
||||
- [x] **C-2** | Design-System konsolidieren ✅ *12. April 2026*
|
||||
- [ ] Farb-Palette in `MaterialTheme` / `Theme.kt` vereinheitlichen
|
||||
- [ ] Typografie-Skala definieren (Überschriften, Body, Labels, Captions)
|
||||
- [ ] Wiederverwendbare Komponenten als Composables extrahieren (Cards, Badges, Chips)
|
||||
|
|
|
|||
|
|
@ -41,20 +41,16 @@ fun <T> MsSearchableSelect(
|
|||
|
||||
Column(modifier = modifier) {
|
||||
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = selectedOption?.let { optionLabel(it) } ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
|
||||
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
|
||||
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) { showDialog = true },
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = Icons.Default.Search,
|
||||
modifier = modifier.clickable(enabled = enabled) { showDialog = true },
|
||||
enabled = enabled,
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
|
||||
|
|
@ -75,17 +71,16 @@ fun <T> MsSearchableSelect(
|
|||
)
|
||||
|
||||
// Internes Suchfeld im Dialog
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = searchText,
|
||||
onValueChange = {
|
||||
searchText = it
|
||||
onSearchQueryChange(it)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Suchbegriff eingeben...") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
placeholder = "Suchbegriff eingeben...",
|
||||
leadingIcon = Icons.Default.Search,
|
||||
singleLine = true,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package at.mocode.frontend.core.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
|
|
@ -14,6 +13,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
@Composable
|
||||
fun MsTextField(
|
||||
|
|
@ -31,28 +31,41 @@ fun MsTextField(
|
|||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
compact: Boolean = true // Desktop-optimiert (kompakter)
|
||||
) {
|
||||
val height = if (compact) Dimens.TextFieldHeight else Dimens.TextFieldHeightL
|
||||
|
||||
Column(modifier = modifier) {
|
||||
if (label != null) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 4.dp, start = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = label?.let { { Text(it) } },
|
||||
placeholder = placeholder?.let { { Text(it) } },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = height),
|
||||
placeholder = placeholder?.let { { Text(it, style = MaterialTheme.typography.bodyMedium) } },
|
||||
leadingIcon = leadingIcon?.let { icon ->
|
||||
{ Icon(imageVector = icon, contentDescription = null) }
|
||||
{ Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM)) }
|
||||
},
|
||||
trailingIcon = if (trailingIcon != null) {
|
||||
{
|
||||
IconButton(
|
||||
onClick = onTrailingIconClick ?: {}
|
||||
) {
|
||||
Icon(imageVector = trailingIcon, contentDescription = null)
|
||||
Icon(imageVector = trailingIcon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM))
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
|
|
@ -61,6 +74,15 @@ fun MsTextField(
|
|||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = keyboardType,
|
||||
imeAction = imeAction
|
||||
|
|
@ -70,24 +92,20 @@ fun MsTextField(
|
|||
)
|
||||
|
||||
// Error or helper text
|
||||
when {
|
||||
isError && errorMessage != null -> {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
helperText != null -> {
|
||||
Text(
|
||||
text = helperText,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
if (isError && errorMessage != null) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
|
||||
)
|
||||
} else if (helperText != null) {
|
||||
Text(
|
||||
text = helperText,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,16 @@ object AppColors {
|
|||
val PrimaryContainer = Color(0xFFDEEBFF)
|
||||
val OnPrimaryContainer = Color(0xFF0052CC)
|
||||
|
||||
// Subtiles Sidebar-Grau / Navigation
|
||||
val NavigationSurface = Color(0xFFF4F5F7)
|
||||
val NavigationContent = Color(0xFF42526E)
|
||||
|
||||
// Helleres Blau für sekundäre Akzente
|
||||
val Secondary = Color(0xFF2684FF)
|
||||
val OnSecondary = Color.White
|
||||
|
||||
// Neutral- & Hintergrund (Light Mode)
|
||||
val BackgroundLight = Color(0xFFF4F5F7) // Helles Grau (nicht hartes Weiß)
|
||||
val BackgroundLight = Color(0xFFF9FAFB) // Sehr helles Grau für den Content Bereich
|
||||
val SurfaceLight = Color.White
|
||||
val OnBackgroundLight = Color(0xFF172B4D) // Fast Schwarz (besser lesbar)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,17 @@ object Dimens {
|
|||
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
|
||||
val SpacingM = 16.dp // Abstand für Sektionen
|
||||
val SpacingL = 24.dp // Außenabstand für Screens
|
||||
val SpacingXL = 32.dp
|
||||
|
||||
// Navigations-Maße
|
||||
val NavRailWidth = 72.dp
|
||||
val NavRailExpandedWidth = 240.dp
|
||||
val TopBarHeight = 56.dp
|
||||
|
||||
// Sizes (Größen)
|
||||
val IconSizeS = 16.dp
|
||||
val IconSizeM = 24.dp
|
||||
val IconSizeL = 32.dp
|
||||
|
||||
// Borders
|
||||
val BorderThin = 1.dp
|
||||
|
|
@ -24,4 +31,10 @@ object Dimens {
|
|||
// Corner Radius (Ecken)
|
||||
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
|
||||
val CornerRadiusM = 8.dp
|
||||
val CornerRadiusL = 12.dp
|
||||
|
||||
// Form-Elemente (Eingabefelder, Buttons)
|
||||
val TextFieldHeight = 44.dp // Kompakte Höhe für Desktop-Enterprise-Apps
|
||||
val TextFieldHeightL = 56.dp // Standard Material Höhe (für prominente Felder)
|
||||
val ButtonHeight = 40.dp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,39 @@ CREATE TABLE LocalSettings (
|
|||
value TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- SyncEvents Tabelle für Offline-First/Event-Sourcing (ADR-0022/Concept)
|
||||
CREATE TABLE SyncEvents (
|
||||
sequence_number INTEGER NOT NULL,
|
||||
origin_node_id TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
turnier_id TEXT,
|
||||
aggregate_type TEXT NOT NULL,
|
||||
aggregate_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
checksum TEXT,
|
||||
synced_at INTEGER, -- Null if not yet synced to backend
|
||||
PRIMARY KEY (origin_node_id, sequence_number)
|
||||
);
|
||||
|
||||
insertOrReplace:
|
||||
INSERT OR REPLACE INTO LocalSettings(key, value) VALUES (?, ?);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM LocalSettings;
|
||||
|
||||
-- SyncEvents Queries
|
||||
insertSyncEvent:
|
||||
INSERT INTO SyncEvents(sequence_number, origin_node_id, event_id, turnier_id, aggregate_type, aggregate_id, event_type, payload, created_at, schema_version, checksum)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
|
||||
selectUnsyncedEvents:
|
||||
SELECT * FROM SyncEvents WHERE synced_at IS NULL ORDER BY sequence_number ASC;
|
||||
|
||||
markSynced:
|
||||
UPDATE SyncEvents SET synced_at = ? WHERE origin_node_id = ? AND sequence_number = ?;
|
||||
|
||||
getLastSequenceNumber:
|
||||
SELECT MAX(sequence_number) FROM SyncEvents WHERE origin_node_id = ?;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -8,17 +9,19 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
// Status-Farben gemäß Vision_03
|
||||
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
||||
|
|
@ -52,18 +55,67 @@ fun AdminUebersichtScreen(
|
|||
ort = "4221 NEUMARKT/M.",
|
||||
datum = "12.–13.04.2026",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 0,
|
||||
nennungen = 142,
|
||||
letzteAktivitaet = "vor 1 Min",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
turniere = listOf(
|
||||
TurnierUiModel(id = 26129, nummer = 26129, name = "CDN-C-NEU CDNP-C-NEU", bewerbAnzahl = 16),
|
||||
TurnierUiModel(id = 26128, nummer = 26128, name = "CSN-C-NEU CSNP-C-NEU", bewerbAnzahl = 18),
|
||||
)
|
||||
),
|
||||
VeranstaltungUiModel(
|
||||
id = 1002,
|
||||
name = "LINZ-EBELSBERG",
|
||||
ort = "4030 LINZ",
|
||||
datum = "15.–18.05.2026",
|
||||
turnierAnzahl = 1,
|
||||
nennungen = 89,
|
||||
letzteAktivitaet = "vor 2 Std",
|
||||
status = VeranstaltungStatus.LIVE,
|
||||
turniere = listOf(
|
||||
TurnierUiModel(id = 26130, nummer = 26130, name = "CSN-B", bewerbAnzahl = 24),
|
||||
)
|
||||
)
|
||||
)
|
||||
val veranstaltungen = remember { mutableStateListOf<VeranstaltungUiModel>().also { it.addAll(sample) } }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(Dimens.SpacingL)
|
||||
) {
|
||||
// Page Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = Dimens.SpacingL),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = "Veranstaltungs-Verwaltung",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Text(
|
||||
text = "Übersicht aller laufenden und geplanten Reitsport-Veranstaltungen",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onVeranstalterAuswahl,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
contentPadding = PaddingValues(horizontal = Dimens.SpacingM, vertical = Dimens.SpacingS)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeS))
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
Text("Neue Veranstaltung")
|
||||
}
|
||||
}
|
||||
|
||||
// KPI-Kacheln
|
||||
KpiKachelRow(
|
||||
liveAktiv = 0,
|
||||
|
|
@ -75,39 +127,42 @@ fun AdminUebersichtScreen(
|
|||
onCupsClick = onCupsOeffnen
|
||||
)
|
||||
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
Spacer(Modifier.height(Dimens.SpacingM))
|
||||
|
||||
// Toolbar (Suche & Filter)
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp
|
||||
) {
|
||||
Button(
|
||||
onClick = onVeranstalterAuswahl,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Dimens.SpacingM),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Neue Veranstaltung")
|
||||
MsTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = "Suche nach Name, Ort oder Turnier-Nr.",
|
||||
leadingIcon = Icons.Default.Search,
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Status-Filter Chips
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
StatusFilterChip("Alle", selected = true)
|
||||
StatusFilterChip("Vorbereitung", selected = false)
|
||||
StatusFilterChip("Live", selected = false)
|
||||
StatusFilterChip("Abgeschlossen", selected = false)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 13.sp) },
|
||||
modifier = Modifier.weight(1f).height(48.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
// Status-Filter Chips
|
||||
StatusFilterChip("Alle", selected = true)
|
||||
StatusFilterChip("Vorbereitung", selected = false)
|
||||
StatusFilterChip("Live", selected = false)
|
||||
StatusFilterChip("Abgeschlossen", selected = false)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(Dimens.SpacingM))
|
||||
|
||||
// Veranstaltungs-Liste
|
||||
if (veranstaltungen.isEmpty()) {
|
||||
|
|
@ -130,19 +185,18 @@ fun AdminUebersichtScreen(
|
|||
Spacer(Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onVeranstalterAuswahl,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeS))
|
||||
Spacer(Modifier.width(Dimens.SpacingXS))
|
||||
Text("Neue Veranstaltung anlegen")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||
contentPadding = PaddingValues(bottom = Dimens.SpacingL),
|
||||
) {
|
||||
items(items = veranstaltungen, key = { it.id }) { veranstaltung ->
|
||||
VeranstaltungCard(
|
||||
|
|
@ -249,12 +303,12 @@ private fun VeranstaltungCard(
|
|||
onOeffnen: () -> Unit,
|
||||
onLoeschen: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
|
||||
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Column(modifier = Modifier.padding(Dimens.SpacingM)) {
|
||||
// Header
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -264,16 +318,16 @@ private fun VeranstaltungCard(
|
|||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = veranstaltung.name,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 15.sp,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||
modifier = Modifier.padding(top = Dimens.SpacingXS),
|
||||
) {
|
||||
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
LabelValue("📍", veranstaltung.ort)
|
||||
LabelValue("📅", veranstaltung.datum)
|
||||
LabelValue("🏆", "${veranstaltung.turnierAnzahl} Turniere")
|
||||
}
|
||||
}
|
||||
StatusBadge(veranstaltung.status)
|
||||
|
|
@ -281,56 +335,77 @@ private fun VeranstaltungCard(
|
|||
|
||||
// Turnier-Liste
|
||||
if (veranstaltung.turniere.isNotEmpty()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Turniere (${veranstaltung.turniere.size}):", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
||||
veranstaltung.turniere.forEach { turnier ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = Color(0xFF1E3A8A),
|
||||
Spacer(Modifier.height(Dimens.SpacingM))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Column(modifier = Modifier.padding(Dimens.SpacingS)) {
|
||||
Text(
|
||||
text = "Zugeordnete Turniere",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = Dimens.SpacingXS)
|
||||
)
|
||||
veranstaltung.turniere.forEach { turnier ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = turnier.nummer.toString(),
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
) {
|
||||
Text(
|
||||
text = turnier.nummer.toString(),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
TextButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
|
||||
Text("Details", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
Text("${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)", fontSize = 12.sp)
|
||||
}
|
||||
OutlinedButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
|
||||
Text("Zum Turnier", fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Spacer(Modifier.height(Dimens.SpacingM))
|
||||
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Nennungen: ${veranstaltung.nennungen}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Text("Letzte Aktivität: ${veranstaltung.letzteAktivitaet}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
LabelValue("Nennungen:", veranstaltung.nennungen.toString())
|
||||
LabelValue("Aktivität:", veranstaltung.letzteAktivitaet)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
Button(
|
||||
onClick = onOeffnen,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
modifier = Modifier.height(32.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = Modifier.height(36.dp),
|
||||
) {
|
||||
Text("Zur Veranstaltung", fontSize = 12.sp)
|
||||
}
|
||||
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626))
|
||||
Text("Veranstaltung öffnen", style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -338,6 +413,15 @@ private fun VeranstaltungCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelValue(label: String, value: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(label, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(value, style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusBadge(status: VeranstaltungStatus) {
|
||||
val (text, color) = when (status) {
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.Wifi
|
||||
import androidx.compose.material.icons.filled.WifiOff
|
||||
import androidx.compose.material.icons.automirrored.filled.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.frontend.features.billing.presentation.BillingScreen
|
||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
||||
|
|
@ -49,11 +49,9 @@ private val TopBarTextColor = Color.White
|
|||
* Haupt-Layout der Desktop-App gemäß Vision_03.
|
||||
*
|
||||
* Struktur:
|
||||
* - TopBar (dunkelblau): App-Titel + Breadcrumb + Logout
|
||||
* - NavigationRail (links): Globale Navigation
|
||||
* - Header (oben): Breadcrumb + Status + Logout
|
||||
* - Content: kontextabhängiger Screen
|
||||
*
|
||||
* Kein Nav-Rail, keine Sidebar – Navigation erfolgt über
|
||||
* Breadcrumb-Klicks und horizontale Tabs innerhalb der Screens.
|
||||
*/
|
||||
@Composable
|
||||
fun DesktopMainLayout(
|
||||
|
|
@ -62,18 +60,25 @@ fun DesktopMainLayout(
|
|||
onBack: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
// Onboarding-Eingaben zwischen Navigationswechseln behalten → State hier (außerhalb des when) hosten
|
||||
// Onboarding-Eingaben zwischen Navigationswechseln behalten
|
||||
var obGeraet by rememberSaveable { mutableStateOf("") }
|
||||
var obKey by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopTopBar(
|
||||
Row(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
||||
// Navigation Rail (Modernere Seitenleiste)
|
||||
DesktopNavRail(
|
||||
currentScreen = currentScreen,
|
||||
onNavigate = onNavigate,
|
||||
onBack = onBack,
|
||||
onLogout = onLogout,
|
||||
onNavigate = onNavigate
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
DesktopTopHeader(
|
||||
currentScreen = currentScreen,
|
||||
onNavigate = onNavigate,
|
||||
onBack = onBack,
|
||||
onLogout = onLogout,
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
DesktopContentArea(
|
||||
currentScreen = currentScreen,
|
||||
|
|
@ -85,247 +90,316 @@ fun DesktopMainLayout(
|
|||
onObKeyChange = { obKey = it },
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
DesktopFooterBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
|
||||
*
|
||||
* Breadcrumb-Logik:
|
||||
* - Root: "🏠 Admin - Verwaltung"
|
||||
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
|
||||
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopNavRail(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxHeight().width(Dimens.NavRailWidth),
|
||||
color = AppColors.NavigationSurface,
|
||||
contentColor = AppColors.NavigationContent,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxHeight().padding(vertical = Dimens.SpacingM),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||
) {
|
||||
// App Icon / Logo Platzhalter
|
||||
Surface(
|
||||
modifier = Modifier.size(40.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Adjust,
|
||||
contentDescription = "Logo",
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.padding(Dimens.SpacingS)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingL))
|
||||
|
||||
// Navigations-Items
|
||||
NavRailItem(
|
||||
icon = Icons.Default.Dashboard,
|
||||
label = "Admin",
|
||||
selected = currentScreen is AppScreen.VeranstaltungVerwaltung || currentScreen is AppScreen.VeranstaltungDetail,
|
||||
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) }
|
||||
)
|
||||
|
||||
NavRailItem(
|
||||
icon = Icons.Default.People,
|
||||
label = "Vereine",
|
||||
selected = currentScreen is AppScreen.VereinVerwaltung,
|
||||
onClick = { onNavigate(AppScreen.VereinVerwaltung) }
|
||||
)
|
||||
|
||||
NavRailItem(
|
||||
icon = Icons.Default.Settings,
|
||||
label = "Tools",
|
||||
selected = currentScreen is AppScreen.Ping,
|
||||
onClick = { onNavigate(AppScreen.Ping) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopTopBar(
|
||||
private fun NavRailItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val tint = if (selected) MaterialTheme.colorScheme.primary else AppColors.NavigationContent
|
||||
val background = if (selected) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) else Color.Transparent
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clickable(onClick = onClick),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = background
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = tint,
|
||||
modifier = Modifier.size(Dimens.IconSizeM)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TopHeader: Schlanke Leiste mit Breadcrumb und Logout.
|
||||
*/
|
||||
@Composable
|
||||
private fun DesktopTopHeader(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.background(TopBarColor)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth().height(Dimens.TopBarHeight),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Zurück-Pfeil: für alle außer Onboarding anzeigen (damit man von "Verwaltung" zurück kommt)
|
||||
if (currentScreen !is AppScreen.Onboarding) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
tint = TopBarTextColor,
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable { onBack() },
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize().padding(horizontal = Dimens.SpacingL),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (currentScreen !is AppScreen.Onboarding) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.size(Dimens.IconSizeM),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
}
|
||||
|
||||
// Breadcrumb-Segmente
|
||||
BreadcrumbContent(currentScreen, onNavigate)
|
||||
}
|
||||
|
||||
// Root-Link
|
||||
Text(
|
||||
text = "Verwaltung",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||
)
|
||||
|
||||
// Breadcrumb-Segmente je nach Screen
|
||||
when (currentScreen) {
|
||||
is AppScreen.VeranstalterAuswahl -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
// Profil / Logout Bereich
|
||||
Text(
|
||||
text = "Administrator",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||
contentDescription = "Abmelden",
|
||||
modifier = Modifier.size(Dimens.IconSizeM),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstalterNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neuer Veranstalter",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungProfil -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.id}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neue Veranstaltung",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
is AppScreen.TurnierDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Turnier ${currentScreen.turnierId}",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.Billing -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungProfil(0, currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Turnier ${currentScreen.turnierId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Abrechnung",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.TurnierNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neues Turnier",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Ping -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Ping Service",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Vereine -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Vereine",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.Meisterschaften -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Meisterschaften",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
is AppScreen.Cups -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Cups",
|
||||
color = TopBarTextColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logout wurde auf Kundenwunsch entfernt
|
||||
@Composable
|
||||
private fun BreadcrumbContent(
|
||||
currentScreen: AppScreen,
|
||||
onNavigate: (AppScreen) -> Unit
|
||||
) {
|
||||
when (currentScreen) {
|
||||
is AppScreen.VeranstalterAuswahl -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter auswählen",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstalterNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neuer Veranstalter",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungProfil -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.id}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
is AppScreen.VeranstaltungNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neue Veranstaltung",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
is AppScreen.TurnierDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Turnier ${currentScreen.turnierId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.Billing -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungProfil(0, currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Turnier ${currentScreen.turnierId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Abrechnung",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.TurnierNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neues Turnier",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Ping -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Ping Service",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Vereine -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Vereine",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.Meisterschaften -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Meisterschaften",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
is AppScreen.Cups -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Cups",
|
||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -733,45 +807,71 @@ private fun DesktopContentArea(
|
|||
|
||||
@Composable
|
||||
private fun DesktopFooterBar() {
|
||||
// Stub-Status für MVP
|
||||
// Echte Status-Logik vorbereitet
|
||||
val online = remember { mutableStateOf(true) }
|
||||
val deviceConnected = remember { mutableStateOf(true) }
|
||||
val deviceName = "Richter-Turm"
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(36.dp)
|
||||
.background(Color(0xFFF3F4F6))
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
tonalElevation = 1.dp
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = if (online.value) Icons.Filled.Wifi else Icons.Filled.WifiOff,
|
||||
contentDescription = null,
|
||||
tint = if (online.value) Color(0xFF059669) else Color(0xFFDC2626)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(if (online.value) "Online" else "Offline", color = Color(0xFF374151), fontSize = 12.sp)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Icon(Icons.Filled.Devices, contentDescription = null, tint = if (deviceConnected.value) Color(0xFF2563EB) else Color(0xFF9CA3AF))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
if (deviceConnected.value) "Verbunden: $deviceName" else "Kein Gerät verbunden",
|
||||
color = Color(0xFF374151),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (deviceConnected.value) {
|
||||
OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) {
|
||||
Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(32.dp)
|
||||
.padding(horizontal = Dimens.SpacingS),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Status: Cloud Sync
|
||||
StatusIndicator(
|
||||
icon = if (online.value) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
||||
label = if (online.value) "Cloud synchronisiert" else "Offline (Lokal)",
|
||||
color = if (online.value) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(Dimens.SpacingM))
|
||||
|
||||
// Status: LAN Devices (mDNS)
|
||||
StatusIndicator(
|
||||
icon = Icons.Filled.Lan,
|
||||
label = if (deviceConnected.value) "Verbunden: $deviceName" else "Suche nach Geräten...",
|
||||
color = if (deviceConnected.value) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "v2.4.0-rc1 | Desktop-Alpha",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusIndicator(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
color: Color
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(Dimens.IconSizeS)
|
||||
)
|
||||
Spacer(Modifier.width(Dimens.SpacingXS))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package at.mocode.desktop.v2
|
||||
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -51,10 +52,10 @@ fun OnboardingScreen(
|
|||
val frKey = remember { FocusRequester() }
|
||||
val frBtn = remember { FocusRequester() }
|
||||
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = geraetName,
|
||||
onValueChange = { onGeraetNameChange(it) },
|
||||
label = { Text("Gerätename (Pflicht)") },
|
||||
label = "Gerätename (Pflicht)",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(frName)
|
||||
|
|
@ -70,18 +71,15 @@ fun OnboardingScreen(
|
|||
} else false
|
||||
}
|
||||
,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = secureKey,
|
||||
onValueChange = { onSecureKeyChange(it) },
|
||||
label = { Text("Sicherheitsschlüssel (Pflicht)") },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showPw = !showPw }) {
|
||||
Icon(if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility, contentDescription = null)
|
||||
}
|
||||
},
|
||||
label = "Sicherheitsschlüssel (Pflicht)",
|
||||
trailingIcon = if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||
onTrailingIconClick = { showPw = !showPw },
|
||||
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -106,7 +104,7 @@ fun OnboardingScreen(
|
|||
} else false
|
||||
}
|
||||
,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
if (geraetName.trim().length >= 3 && secureKey.trim().length >= 8) {
|
||||
onContinue(geraetName, secureKey)
|
||||
|
|
@ -189,14 +187,14 @@ fun PferdProfilV2(id: Long, onBack: () -> Unit) {
|
|||
title = { Text("Pferd bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
MsTextField(name, { name = it }, label = "Name", modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
|
||||
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
|
||||
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(geb, { geb = it }, label = { Text("Geburtsdatum") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(farbe, { farbe = it }, label = { Text("Farbe") }, modifier = Modifier.weight(1f))
|
||||
MsTextField(geb, { geb = it }, label = "Geburtsdatum", modifier = Modifier.weight(1f))
|
||||
MsTextField(farbe, { farbe = it }, label = "Farbe", modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -261,16 +259,16 @@ fun ReiterProfilV2(id: Long, onBack: () -> Unit) {
|
|||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(vor, { vor = it }, label = { Text("Vorname") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(nach, { nach = it }, label = { Text("Nachname") }, modifier = Modifier.weight(1f))
|
||||
MsTextField(vor, { vor = it }, label = "Vorname", modifier = Modifier.weight(1f))
|
||||
MsTextField(nach, { nach = it }, label = "Nachname", modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
|
||||
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
|
||||
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(liz, { liz = it }, label = { Text("Lizenzklasse") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(verein, { verein = it }, label = { Text("Verein") }, modifier = Modifier.weight(1f))
|
||||
MsTextField(liz, { liz = it }, label = "Lizenzklasse", modifier = Modifier.weight(1f))
|
||||
MsTextField(verein, { verein = it }, label = "Verein", modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user