diff --git a/core/core-domain/src/commonMain/kotlin/at/mocode/core/sync/SyncEvent.kt b/core/core-domain/src/commonMain/kotlin/at/mocode/core/sync/SyncEvent.kt new file mode 100644 index 00000000..bc28a85a --- /dev/null +++ b/core/core-domain/src/commonMain/kotlin/at/mocode/core/sync/SyncEvent.kt @@ -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 +) diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index 5d3300bb..caccf485 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -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.* diff --git a/docs/04_Agents/Logs/2026-04-12_Desktop_Fokus_Curator_Log.md b/docs/04_Agents/Logs/2026-04-12_Desktop_Fokus_Curator_Log.md new file mode 100644 index 00000000..2d31ea25 --- /dev/null +++ b/docs/04_Agents/Logs/2026-04-12_Desktop_Fokus_Curator_Log.md @@ -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* diff --git a/docs/04_Agents/Logs/2026-04-12_UIUX_Refactoring_Curator_Log.md b/docs/04_Agents/Logs/2026-04-12_UIUX_Refactoring_Curator_Log.md new file mode 100644 index 00000000..0818f844 --- /dev/null +++ b/docs/04_Agents/Logs/2026-04-12_UIUX_Refactoring_Curator_Log.md @@ -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* diff --git a/docs/04_Agents/Roadmaps/Architect_Roadmap.md b/docs/04_Agents/Roadmaps/Architect_Roadmap.md index 5bb2629b..9bceec4d 100644 --- a/docs/04_Agents/Roadmaps/Architect_Roadmap.md +++ b/docs/04_Agents/Roadmaps/Architect_Roadmap.md @@ -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. diff --git a/docs/04_Agents/Roadmaps/Curator_Roadmap.md b/docs/04_Agents/Roadmaps/Curator_Roadmap.md index 9235bc0a..5c5738f4 100644 --- a/docs/04_Agents/Roadmaps/Curator_Roadmap.md +++ b/docs/04_Agents/Roadmaps/Curator_Roadmap.md @@ -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 | diff --git a/docs/04_Agents/Roadmaps/Frontend_Roadmap.md b/docs/04_Agents/Roadmaps/Frontend_Roadmap.md index 2bdfec72..71e03acc 100644 --- a/docs/04_Agents/Roadmaps/Frontend_Roadmap.md +++ b/docs/04_Agents/Roadmaps/Frontend_Roadmap.md @@ -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 diff --git a/docs/04_Agents/Roadmaps/QA_Roadmap.md b/docs/04_Agents/Roadmaps/QA_Roadmap.md index 38c77585..d1e06dac 100644 --- a/docs/04_Agents/Roadmaps/QA_Roadmap.md +++ b/docs/04_Agents/Roadmaps/QA_Roadmap.md @@ -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 | diff --git a/docs/04_Agents/Roadmaps/UIUX_Roadmap.md b/docs/04_Agents/Roadmaps/UIUX_Roadmap.md index 603791ae..bafd6ddc 100644 --- a/docs/04_Agents/Roadmaps/UIUX_Roadmap.md +++ b/docs/04_Agents/Roadmaps/UIUX_Roadmap.md @@ -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) diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt index b2940127..74e8f340 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt @@ -41,20 +41,16 @@ fun 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 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)) diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsTextField.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsTextField.kt index 9fe53e3a..007ddc01 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsTextField.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsTextField.kt @@ -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) + ) } } } diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Colors.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Colors.kt index bdbe6ea9..3538768d 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Colors.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Colors.kt @@ -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) diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Dimens.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Dimens.kt index 93715fd1..c9f7d246 100644 --- a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Dimens.kt +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/theme/Dimens.kt @@ -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 } diff --git a/frontend/core/local-db/src/commonMain/sqldelight/at/mocode/frontend/core/localdb/MeldestelleDb.sq b/frontend/core/local-db/src/commonMain/sqldelight/at/mocode/frontend/core/localdb/MeldestelleDb.sq index 074d1cd5..f286882b 100644 --- a/frontend/core/local-db/src/commonMain/sqldelight/at/mocode/frontend/core/localdb/MeldestelleDb.sq +++ b/frontend/core/local-db/src/commonMain/sqldelight/at/mocode/frontend/core/localdb/MeldestelleDb.sq @@ -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 = ?; diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/AdminUebersichtScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/AdminUebersichtScreen.kt index e754c756..350250c3 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/AdminUebersichtScreen.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/AdminUebersichtScreen.kt @@ -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().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) { diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt index 0b7b31b7..07332e34 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/layout/DesktopMainLayout.kt @@ -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 #" - * - Turnier: "🏠 Admin - Verwaltung / Veranstaltung # / Turnier " - */ +@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 + ) + } +} diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt index a5d744e1..bba61e39 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/v2/Screens.kt @@ -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)) } } }