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:
@@ -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] **`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. ✓
|
* [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
|
### PHASE 11: Ergebniserfassung & Platzierung ✅ ABGESCHLOSSEN
|
||||||
|
|
||||||
*Ziel: Vollständige Ergebniserfassung und automatisierte Platzierungsberechnung.*
|
*Ziel: Vollständige Ergebniserfassung und automatisierte Platzierungsberechnung.*
|
||||||
|
|||||||
@@ -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
|
# 🏗️ [Lead Architect] — Zwischenstand & Roadmap
|
||||||
|
|
||||||
> **Stand:** 3. April 2026
|
> **Stand:** 12. April 2026
|
||||||
> **Rolle:** Strategie, Architektur-Entscheidungen (ADRs), Domänen-Modell, Master-Roadmap
|
> **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] **B-1** | ADR für LAN-Sync-Protokoll schreiben
|
||||||
- [x] Optionen analysieren: Event-Sourcing vs. CRDT vs. Timestamp-Sync
|
- [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] **C-1** | Zeitplan-Optimierung Konzept
|
||||||
- [x] Fachliche Anforderungen (Use Cases) definiert
|
- [x] Fachliche Anforderungen (Use Cases) definiert
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
- [x] Phase 9 Fortschritt reflektieren
|
- [x] Phase 9 Fortschritt reflektieren
|
||||||
- [x] Link zum Zeitplan-Konzept ergänzt
|
- [x] Link zum Zeitplan-Konzept ergänzt
|
||||||
- [x] Feature-Migration (Frontend) dokumentiert
|
- [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
|
- [ ] Technische Machbarkeit (File-Storage vs. SQLite-Export) prüfen
|
||||||
- [ ] ADR für Offline-Transfer erstellen
|
- [ ] 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
|
## 📌 Abhängigkeiten
|
||||||
@@ -68,10 +92,10 @@
|
|||||||
| Domänen-Modell ✅ | 👷 Backend: Schema-Design; 🎨 Frontend: ViewModel-Design |
|
| Domänen-Modell ✅ | 👷 Backend: Schema-Design; 🎨 Frontend: ViewModel-Design |
|
||||||
| LAN-Sync ADR (B-1) | 🎨 Frontend: Sync-UI; 👷 Backend: Sync-Endpunkte |
|
| LAN-Sync ADR (B-1) | 🎨 Frontend: Sync-UI; 👷 Backend: Sync-Endpunkte |
|
||||||
| Sync-Konzept (C-1) | 🐧 DevOps: mDNS/WebSocket-Infrastruktur |
|
| Sync-Konzept (C-1) | 🐧 DevOps: mDNS/WebSocket-Infrastruktur |
|
||||||
|
| Billing-Arch (D-2) | 👷 Backend: Buchungs-Logik; 🎨 Frontend: Kassa-UI |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 💡 Empfehlung
|
## 💡 Empfehlung
|
||||||
|
|
||||||
**Sofort starten:** B-1 (LAN-Sync ADR) — Phase 8 der MASTER_ROADMAP wartet auf mDNS/WebSocket-Discovery; ohne ADR können
|
**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.
|
||||||
Backend und Frontend nicht parallel implementieren.
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🧹 [Curator] — Zwischenstand & Roadmap
|
# 🧹 [Curator] — Zwischenstand & Roadmap
|
||||||
|
|
||||||
> **Stand:** 3. April 2026
|
> **Stand:** 12. April 2026
|
||||||
> **Rolle:** Dokumentation, Session-Logs, Ubiquitous Language, Ordnung in `docs/`
|
> **Rolle:** Dokumentation, Session-Logs, Ubiquitous Language, Ordnung in `docs/`
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -19,81 +19,30 @@
|
|||||||
|
|
||||||
### Sprint B — Abgeschlossen
|
### Sprint B — Abgeschlossen
|
||||||
|
|
||||||
- [x] **B-0** | Rulebook-Session (03.04.2026) dokumentiert →
|
- [x] **B-0** | Rulebook-Session (03.04.2026) dokumentiert
|
||||||
`docs/99_Journal/2026-04-03_Rulebook_B1_Validierung_Frontend.md`
|
- [x] **B-1** | Alle Roadmaps geprüft und korrigiert (03.04. & 12.04.)
|
||||||
- [x] **B-1** (teilweise) | Architect B-1 Session-Log erstellt →
|
- [x] **B-2** | `docs/05_Backend/` aktualisiert: Schema (V1-V009) & API-Übersicht Stammdaten
|
||||||
`docs/99_Journal/2026-04-03_Architect_B1_LAN-Sync_ADR-0022.md`
|
- [x] **B-3** | `docs/06_Frontend/` aktualisiert: MVVM-Muster & ViewModel-Referenzen
|
||||||
- [x] **B-1** (teilweise) | Roadmaps aktualisiert: Architect (✅ Sprint B), Backend (C-3 freigegeben), Frontend (C-3
|
|
||||||
freigegeben)
|
### Sprint C — Abgeschlossen
|
||||||
- [x] **B-1** (abgeschlossen) | Alle Roadmaps geprüft und korrigiert (03.04.2026)
|
|
||||||
- [x] DevOps_Roadmap: vollständig und korrekt ✅
|
- [x] **C-1** | `README.md` aktualisiert: Desktop-App Fokus & Quickstart
|
||||||
- [x] UIUX_Roadmap: vollständig und korrekt ✅
|
- [x] **C-2** | Setup-Guide aktualisiert → `docs/02_Guides/start-local.md`
|
||||||
- [x] Rulebook_Roadmap: vollständig und korrekt ✅
|
- [x] **C-3** | Session-Logs für Phase 10, 11 & 12 (Serie, Ergebnisse, Billing) erstellt
|
||||||
- [x] QA_Roadmap: Sprint-B-Header korrigiert (🔴 → 🟡 Teilweise offen) ✅
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟡 Sprint B — Teilweise offen
|
## 🟠 Sprint D — In Arbeit
|
||||||
|
|
||||||
- [x] **B-1** | Roadmaps-Verzeichnis pflegen ✅ *3. April 2026*
|
- [ ] **D-1** | Kassa-Endpunkte in API-Doku ergänzen (sobald Billing-Service final)
|
||||||
- [x] Architect-, Backend-, Frontend-Roadmaps aktualisiert (03.04.2026)
|
- [ ] **D-2** | V1-Code-Bereinigung koordinieren (identifizieren veralteter Module)
|
||||||
- [x] Verbleibende Roadmaps (DevOps, QA, UI/UX, Rulebook) auf Vollständigkeit geprüft
|
- [ ] **D-3** | Sprint-Reports Phase 10-12 finalisieren
|
||||||
- [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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 Abhängigkeiten
|
## 📌 Abhängigkeiten
|
||||||
|
|
||||||
| Warte auf | Von wem | Betrifft |
|
| Warte auf | Von wem | Betrifft |
|
||||||
|--------------------------------------|-------------|----------------------------|
|
|--------------------------|-------------|---------------------|
|
||||||
| ~~Backend CRUD-Endpunkte fertig~~ ✅ | 👷 Backend | B-2 API-Übersicht (bereit) |
|
| Billing-Service Final | 👷 Backend | D-1 Kassa-Doku |
|
||||||
| Backend B-2 Kassa-Service | 👷 Backend | B-2 Kassa-Doku |
|
| Sprint-Berichte (Dev/QA) | 👷 🎨 🧐 | D-3 Reports |
|
||||||
| 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.
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🎨 [Frontend Expert] — Zwischenstand & Roadmap
|
# 🎨 [Frontend Expert] — Zwischenstand & Roadmap
|
||||||
|
|
||||||
> **Stand:** 3. April 2026 (aktualisiert)
|
> **Stand:** 12. April 2026
|
||||||
> **Rolle:** KMP, Compose Desktop, State-Management, Navigation, Backend-Anbindung
|
> **Rolle:** KMP, Compose Desktop, State-Management, Navigation, Backend-Anbindung
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -95,6 +95,14 @@
|
|||||||
- [ ] Domänen-Mastership beachten: Richter-Turm schreibt nur Bewertungen/Ergebnisse
|
- [ ] Domänen-Mastership beachten: Richter-Turm schreibt nur Bewertungen/Ergebnisse
|
||||||
|
|
||||||
- [ ] **C-4** | Lint-Bereinigung & Code-Qualität
|
- [ ] **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
|
- [ ] Ungenutzte Imports/Parameter entfernen
|
||||||
- [ ] `Long → Duration`-Konvertierungen modernisieren
|
- [ ] `Long → Duration`-Konvertierungen modernisieren
|
||||||
- [ ] Redundante Not-null-Calls vereinfachen
|
- [ ] Redundante Not-null-Calls vereinfachen
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 🧐 [QA Specialist] — Zwischenstand & Roadmap
|
# 🧐 [QA Specialist] — Zwischenstand & Roadmap
|
||||||
|
|
||||||
> **Stand:** 3. April 2026
|
> **Stand:** 12. April 2026
|
||||||
> **Rolle:** Test-Strategie, Edge-Cases, Integrationstests, Regressionssicherung
|
> **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)
|
- [x] **B-1** | Test-Suite: Navigation & Back-Stack (V2/V3)
|
||||||
- [ ] Navigations-Flows für alle Screens (vorwärts + zurück)
|
- [x] Navigations-Flows für alle Screens (vorwärts + zurück)
|
||||||
- [ ] Back-Stack-Verhalten nach Zurück-Navigation (korrekter Zustand)
|
- [x] Back-Stack-Verhalten nach Zurück-Navigation (korrekter Zustand)
|
||||||
- [ ] SingleTop-Tabs: kein doppelter Stack-Eintrag bei Tab-Wechsel
|
- [x] SingleTop-Tabs: kein doppelter Stack-Eintrag bei Tab-Wechsel
|
||||||
- [ ] Logout poppt MainShell komplett (keine Screens im Back-Stack)
|
- [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] Leere Pflichtfelder → Speichern-Button bleibt deaktiviert
|
||||||
- [x] Schnelles Doppelklick auf „Weiter" / „Speichern" → kein doppelter Submit
|
- [x] Schnelles Doppelklick auf „Weiter" / „Speichern" → kein doppelter Submit
|
||||||
- [x] Abbrechen mitten im Wizard → kein inkonsistenter Zustand
|
- [x] Abbrechen mitten im Wizard → kein inkonsistenter Zustand
|
||||||
- [x] Zurück-Navigation: Gerätename und Sicherheitsschlüssel bleiben erhalten (`rememberSaveable`)
|
- [x] Zurück-Navigation: Gerätename und Sicherheitsschlüssel bleiben erhalten (`rememberSaveable`)
|
||||||
- **Fix:** `remember` → `rememberSaveable` in `OnboardingScreen.kt`
|
- [x] OnboardingValidator-Tests (GRÜN)
|
||||||
- **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] **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 ≤95cm: Pflicht-Teilung `ohne Lizenz` / `mit Lizenz` wird vorgeschlagen
|
||||||
- [x] CSN-C-NEU ≥100cm: Pflicht-Teilung `R1` / `R2+` wird vorgeschlagen
|
- [x] CSN-C-NEU ≥100cm: Pflicht-Teilung `R1` / `R2+` wird vorgeschlagen
|
||||||
- [x] `ORGANISATORISCH`: Gesamtrangliste korrekt zusammengeführt
|
- [x] `ORGANISATORISCH`: Gesamtrangliste korrekt zusammengeführt
|
||||||
- [x] `SEPARATE_SIEGEREHRUNG`: Abteilungen werden nicht zusammengeführt
|
- [x] `SEPARATE_SIEGEREHRUNG`: Abteilungen werden nicht zusammengeführt
|
||||||
- [x] Caprilli-Regression abgesichert (LIZENZFREI → Abt. 1, R1 → Abt. 2)
|
- [x] AbteilungsRegelServiceTest.kt (GRÜN)
|
||||||
- [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)
|
|
||||||
|
|
||||||
- [ ] **B-4** | Test-Suite: ViewModel-Verhalten
|
- [x] **B-4** | Test-Suite: ViewModel-Verhalten
|
||||||
- [ ] State-Initialisierung korrekt (Loading-State beim Start)
|
- [x] State-Initialisierung korrekt (Loading-State beim Start)
|
||||||
- [ ] Intent → State-Transition für alle Sealed-Class-Intents
|
- [x] Intent → State-Transition für alle Sealed-Class-Intents
|
||||||
- [ ] Fehler-State bei simuliertem Backend-Fehler korrekt gesetzt
|
- [x] Fehler-State bei simuliertem Backend-Fehler korrekt gesetzt
|
||||||
- [ ] Loading-State während asynchroner Operationen (nicht flackern)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟠 Sprint C — Priorität 2 (nächste Woche)
|
## 🟠 Sprint C — In Arbeit
|
||||||
|
|
||||||
- [ ] **C-1** | Test-Suite: Mandanten-Isolation (nach Backend A-1)
|
- [ ] **C-1** | Test-Suite: Mandanten-Isolation (nach Backend A-1)
|
||||||
- [ ] Veranstaltung A kann keine Daten von Veranstaltung B lesen
|
- [ ] 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`)
|
- [ ] 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
|
- [ ] Teilnehmer an 2 Turnieren → 1 Zahlvorgang → 2 korrekte separate Rechnungen
|
||||||
- [ ] Saldo-Berechnung korrekt (Summe aus beiden Turnier-Kassas)
|
- [ ] Saldo-Berechnung korrekt (Summe aus beiden Turnier-Kassas)
|
||||||
- [ ] Bereits bezahlte Beträge werden nicht doppelt verrechnet
|
- [ ] 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
|
## 📌 Abhängigkeiten
|
||||||
|
|
||||||
| Warte auf | Von wem | Betrifft |
|
| Warte auf | Von wem | Betrifft |
|
||||||
|------------------------------------|---------------|-----------------------------|
|
|--------------------------|-------------|------------------------|
|
||||||
| Backend A-1 Rollout + E2E-Test-Fix | 👷 Backend | C-1 Isolations-Tests |
|
| Backend B-2 Kassa-Service| 👷 Backend | C-3 Kassa-Tests |
|
||||||
| Rulebook C-1 AltersklasseRechner | 📜 Rulebook | C-3 Validierungs-Tests |
|
| DevOps CI/CD Pipeline | 🐧 DevOps | CI-Integration |
|
||||||
| 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.
|
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
# 🖌️ [UI/UX Designer] — Zwischenstand & Roadmap
|
# 🖌️ [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
|
> **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] **A-1** | Design-Inventur: Bestehende V3-Screens analysiert
|
||||||
- [x] Alle vorhandenen V3-Screens katalogisiert (Screenshots in `docs/06_Frontend/Screenshots/`)
|
- [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)
|
- [ ] Empty States in alle 10 Listenansichten integrieren (Prioritätsreihenfolge laut Spezifikation)
|
||||||
- [ ] `PferdProfilEditDialog` zu Fullscreen-Edit migrieren (> 8 Felder, Async-Lookups — laut B-1 Mapping)
|
- [ ] `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
|
- [ ] Farb-Palette in `MaterialTheme` / `Theme.kt` vereinheitlichen
|
||||||
- [ ] Typografie-Skala definieren (Überschriften, Body, Labels, Captions)
|
- [ ] Typografie-Skala definieren (Überschriften, Body, Labels, Captions)
|
||||||
- [ ] Wiederverwendbare Komponenten als Composables extrahieren (Cards, Badges, Chips)
|
- [ ] Wiederverwendbare Komponenten als Composables extrahieren (Cards, Badges, Chips)
|
||||||
|
|||||||
+8
-13
@@ -41,20 +41,16 @@ fun <T> MsSearchableSelect(
|
|||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
|
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = selectedOption?.let { optionLabel(it) } ?: "",
|
value = selectedOption?.let { optionLabel(it) } ?: "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
|
label = label,
|
||||||
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
|
placeholder = placeholder,
|
||||||
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
leadingIcon = Icons.Default.Search,
|
||||||
modifier = Modifier
|
modifier = modifier.clickable(enabled = enabled) { showDialog = true },
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(enabled = enabled) { showDialog = true },
|
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
textStyle = MaterialTheme.typography.bodyMedium,
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
|
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
|
||||||
@@ -75,17 +71,16 @@ fun <T> MsSearchableSelect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Internes Suchfeld im Dialog
|
// Internes Suchfeld im Dialog
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = searchText,
|
value = searchText,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
searchText = it
|
searchText = it
|
||||||
onSearchQueryChange(it)
|
onSearchQueryChange(it)
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
placeholder = { Text("Suchbegriff eingeben...") },
|
placeholder = "Suchbegriff eingeben...",
|
||||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
leadingIcon = Icons.Default.Search,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|||||||
+36
-18
@@ -1,12 +1,11 @@
|
|||||||
package at.mocode.frontend.core.designsystem.components
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
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.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MsTextField(
|
fun MsTextField(
|
||||||
@@ -31,28 +31,41 @@ fun MsTextField(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
singleLine: Boolean = true,
|
singleLine: Boolean = true,
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
|
||||||
keyboardType: KeyboardType = KeyboardType.Text,
|
keyboardType: KeyboardType = KeyboardType.Text,
|
||||||
imeAction: ImeAction = ImeAction.Default,
|
imeAction: ImeAction = ImeAction.Default,
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.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) {
|
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(
|
OutlinedTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
label = label?.let { { Text(it) } },
|
.fillMaxWidth()
|
||||||
placeholder = placeholder?.let { { Text(it) } },
|
.heightIn(min = height),
|
||||||
|
placeholder = placeholder?.let { { Text(it, style = MaterialTheme.typography.bodyMedium) } },
|
||||||
leadingIcon = leadingIcon?.let { icon ->
|
leadingIcon = leadingIcon?.let { icon ->
|
||||||
{ Icon(imageVector = icon, contentDescription = null) }
|
{ Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM)) }
|
||||||
},
|
},
|
||||||
trailingIcon = if (trailingIcon != null) {
|
trailingIcon = if (trailingIcon != null) {
|
||||||
{
|
{
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onTrailingIconClick ?: {}
|
onClick = onTrailingIconClick ?: {}
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = trailingIcon, contentDescription = null)
|
Icon(imageVector = trailingIcon, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeM))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else null,
|
} else null,
|
||||||
@@ -61,6 +74,15 @@ fun MsTextField(
|
|||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
maxLines = maxLines,
|
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(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = keyboardType,
|
keyboardType = keyboardType,
|
||||||
imeAction = imeAction
|
imeAction = imeAction
|
||||||
@@ -70,27 +92,23 @@ fun MsTextField(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Error or helper text
|
// Error or helper text
|
||||||
when {
|
if (isError && errorMessage != null) {
|
||||||
isError && errorMessage != null -> {
|
|
||||||
Text(
|
Text(
|
||||||
text = errorMessage,
|
text = errorMessage,
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
|
||||||
)
|
)
|
||||||
}
|
} else if (helperText != null) {
|
||||||
|
|
||||||
helperText != null -> {
|
|
||||||
Text(
|
Text(
|
||||||
text = helperText,
|
text = helperText,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
modifier = Modifier.padding(start = 8.dp, top = 2.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+5
-1
@@ -14,12 +14,16 @@ object AppColors {
|
|||||||
val PrimaryContainer = Color(0xFFDEEBFF)
|
val PrimaryContainer = Color(0xFFDEEBFF)
|
||||||
val OnPrimaryContainer = Color(0xFF0052CC)
|
val OnPrimaryContainer = Color(0xFF0052CC)
|
||||||
|
|
||||||
|
// Subtiles Sidebar-Grau / Navigation
|
||||||
|
val NavigationSurface = Color(0xFFF4F5F7)
|
||||||
|
val NavigationContent = Color(0xFF42526E)
|
||||||
|
|
||||||
// Helleres Blau für sekundäre Akzente
|
// Helleres Blau für sekundäre Akzente
|
||||||
val Secondary = Color(0xFF2684FF)
|
val Secondary = Color(0xFF2684FF)
|
||||||
val OnSecondary = Color.White
|
val OnSecondary = Color.White
|
||||||
|
|
||||||
// Neutral- & Hintergrund (Light Mode)
|
// 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 SurfaceLight = Color.White
|
||||||
val OnBackgroundLight = Color(0xFF172B4D) // Fast Schwarz (besser lesbar)
|
val OnBackgroundLight = Color(0xFF172B4D) // Fast Schwarz (besser lesbar)
|
||||||
|
|
||||||
|
|||||||
+13
@@ -13,10 +13,17 @@ object Dimens {
|
|||||||
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
|
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
|
||||||
val SpacingM = 16.dp // Abstand für Sektionen
|
val SpacingM = 16.dp // Abstand für Sektionen
|
||||||
val SpacingL = 24.dp // Außenabstand für Screens
|
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)
|
// Sizes (Größen)
|
||||||
val IconSizeS = 16.dp
|
val IconSizeS = 16.dp
|
||||||
val IconSizeM = 24.dp
|
val IconSizeM = 24.dp
|
||||||
|
val IconSizeL = 32.dp
|
||||||
|
|
||||||
// Borders
|
// Borders
|
||||||
val BorderThin = 1.dp
|
val BorderThin = 1.dp
|
||||||
@@ -24,4 +31,10 @@ object Dimens {
|
|||||||
// Corner Radius (Ecken)
|
// Corner Radius (Ecken)
|
||||||
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
|
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
|
||||||
val CornerRadiusM = 8.dp
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
+31
@@ -3,8 +3,39 @@ CREATE TABLE LocalSettings (
|
|||||||
value TEXT NOT NULL
|
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:
|
insertOrReplace:
|
||||||
INSERT OR REPLACE INTO LocalSettings(key, value) VALUES (?, ?);
|
INSERT OR REPLACE INTO LocalSettings(key, value) VALUES (?, ?);
|
||||||
|
|
||||||
selectAll:
|
selectAll:
|
||||||
SELECT * FROM LocalSettings;
|
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 = ?;
|
||||||
|
|||||||
+142
-58
@@ -1,6 +1,7 @@
|
|||||||
package at.mocode.veranstaltung.feature.presentation
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.models.VeranstaltungStatus
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
|
||||||
// Status-Farben gemäß Vision_03
|
// Status-Farben gemäß Vision_03
|
||||||
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
private val StatusVorbereitung = Color(0xFFEA580C) // Orange
|
||||||
@@ -52,18 +55,67 @@ fun AdminUebersichtScreen(
|
|||||||
ort = "4221 NEUMARKT/M.",
|
ort = "4221 NEUMARKT/M.",
|
||||||
datum = "12.–13.04.2026",
|
datum = "12.–13.04.2026",
|
||||||
turnierAnzahl = 2,
|
turnierAnzahl = 2,
|
||||||
nennungen = 0,
|
nennungen = 142,
|
||||||
letzteAktivitaet = "vor 1 Min",
|
letzteAktivitaet = "vor 1 Min",
|
||||||
status = VeranstaltungStatus.VORBEREITUNG,
|
status = VeranstaltungStatus.VORBEREITUNG,
|
||||||
turniere = listOf(
|
turniere = listOf(
|
||||||
TurnierUiModel(id = 26129, nummer = 26129, name = "CDN-C-NEU CDNP-C-NEU", bewerbAnzahl = 16),
|
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),
|
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) } }
|
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
|
// KPI-Kacheln
|
||||||
KpiKachelRow(
|
KpiKachelRow(
|
||||||
liveAktiv = 0,
|
liveAktiv = 0,
|
||||||
@@ -75,39 +127,42 @@ fun AdminUebersichtScreen(
|
|||||||
onCupsClick = onCupsOeffnen
|
onCupsClick = onCupsOeffnen
|
||||||
)
|
)
|
||||||
|
|
||||||
// Toolbar
|
Spacer(Modifier.height(Dimens.SpacingM))
|
||||||
|
|
||||||
|
// Toolbar (Suche & Filter)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(Dimens.SpacingM),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||||
) {
|
) {
|
||||||
Button(
|
MsTextField(
|
||||||
onClick = onVeranstalterAuswahl,
|
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
Text("Neue Veranstaltung")
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 13.sp) },
|
placeholder = "Suche nach Name, Ort oder Turnier-Nr.",
|
||||||
modifier = Modifier.weight(1f).height(48.dp),
|
leadingIcon = Icons.Default.Search,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status-Filter Chips
|
// Status-Filter Chips
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||||
StatusFilterChip("Alle", selected = true)
|
StatusFilterChip("Alle", selected = true)
|
||||||
StatusFilterChip("Vorbereitung", selected = false)
|
StatusFilterChip("Vorbereitung", selected = false)
|
||||||
StatusFilterChip("Live", selected = false)
|
StatusFilterChip("Live", selected = false)
|
||||||
StatusFilterChip("Abgeschlossen", selected = false)
|
StatusFilterChip("Abgeschlossen", selected = false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HorizontalDivider()
|
Spacer(Modifier.height(Dimens.SpacingM))
|
||||||
|
|
||||||
// Veranstaltungs-Liste
|
// Veranstaltungs-Liste
|
||||||
if (veranstaltungen.isEmpty()) {
|
if (veranstaltungen.isEmpty()) {
|
||||||
@@ -130,19 +185,18 @@ fun AdminUebersichtScreen(
|
|||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = onVeranstalterAuswahl,
|
onClick = onVeranstalterAuswahl,
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
|
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(Dimens.IconSizeS))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(Dimens.SpacingXS))
|
||||||
Text("Neue Veranstaltung anlegen")
|
Text("Neue Veranstaltung anlegen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(bottom = Dimens.SpacingL),
|
||||||
) {
|
) {
|
||||||
items(items = veranstaltungen, key = { it.id }) { veranstaltung ->
|
items(items = veranstaltungen, key = { it.id }) { veranstaltung ->
|
||||||
VeranstaltungCard(
|
VeranstaltungCard(
|
||||||
@@ -249,12 +303,12 @@ private fun VeranstaltungCard(
|
|||||||
onOeffnen: () -> Unit,
|
onOeffnen: () -> Unit,
|
||||||
onLoeschen: () -> Unit,
|
onLoeschen: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
ElevatedCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
border = if (veranstaltung.status == VeranstaltungStatus.VORBEREITUNG)
|
shape = MaterialTheme.shapes.medium,
|
||||||
BorderStroke(1.dp, Color(0xFF3B82F6)) else null,
|
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(Dimens.SpacingM)) {
|
||||||
// Header
|
// Header
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -264,16 +318,16 @@ private fun VeranstaltungCard(
|
|||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = veranstaltung.name,
|
text = veranstaltung.name,
|
||||||
fontWeight = FontWeight.SemiBold,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontSize = 15.sp,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||||
modifier = Modifier.padding(top = 2.dp),
|
modifier = Modifier.padding(top = Dimens.SpacingXS),
|
||||||
) {
|
) {
|
||||||
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
LabelValue("📍", veranstaltung.ort)
|
||||||
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
LabelValue("📅", veranstaltung.datum)
|
||||||
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
|
LabelValue("🏆", "${veranstaltung.turnierAnzahl} Turniere")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
StatusBadge(veranstaltung.status)
|
StatusBadge(veranstaltung.status)
|
||||||
@@ -281,56 +335,77 @@ private fun VeranstaltungCard(
|
|||||||
|
|
||||||
// Turnier-Liste
|
// Turnier-Liste
|
||||||
if (veranstaltung.turniere.isNotEmpty()) {
|
if (veranstaltung.turniere.isNotEmpty()) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(Dimens.SpacingM))
|
||||||
Text("Turniere (${veranstaltung.turniere.size}):", fontSize = 12.sp, fontWeight = FontWeight.Medium)
|
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 ->
|
veranstaltung.turniere.forEach { turnier ->
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = MaterialTheme.shapes.small,
|
shape = MaterialTheme.shapes.small,
|
||||||
color = Color(0xFF1E3A8A),
|
color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = turnier.nummer.toString(),
|
text = turnier.nummer.toString(),
|
||||||
color = Color.White,
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
fontSize = 11.sp,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text("${turnier.name} (${turnier.bewerbAnzahl} Bewerbe)", fontSize = 12.sp)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OutlinedButton(onClick = onOeffnen, modifier = Modifier.height(28.dp)) {
|
|
||||||
Text("Zum Turnier", fontSize = 11.sp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
// 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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||||
Text("Nennungen: ${veranstaltung.nennungen}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
LabelValue("Nennungen:", veranstaltung.nennungen.toString())
|
||||||
Text("Letzte Aktivität: ${veranstaltung.letzteAktivitaet}", fontSize = 12.sp, color = Color(0xFF6B7280))
|
LabelValue("Aktivität:", veranstaltung.letzteAktivitaet)
|
||||||
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Button(
|
Button(
|
||||||
onClick = onOeffnen,
|
onClick = onOeffnen,
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
shape = MaterialTheme.shapes.small,
|
||||||
modifier = Modifier.height(32.dp),
|
modifier = Modifier.height(36.dp),
|
||||||
) {
|
) {
|
||||||
Text("Zur Veranstaltung", fontSize = 12.sp)
|
Text("Veranstaltung öffnen", style = MaterialTheme.typography.labelMedium)
|
||||||
}
|
|
||||||
IconButton(onClick = onLoeschen, modifier = Modifier.size(32.dp)) {
|
|
||||||
Icon(Icons.Default.Delete, contentDescription = "Löschen", tint = Color(0xFFDC2626))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
@Composable
|
||||||
private fun StatusBadge(status: VeranstaltungStatus) {
|
private fun StatusBadge(status: VeranstaltungStatus) {
|
||||||
val (text, color) = when (status) {
|
val (text, color) = when (status) {
|
||||||
|
|||||||
+217
-117
@@ -4,20 +4,20 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.*
|
||||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.Devices
|
|
||||||
import androidx.compose.material.icons.filled.Wifi
|
|
||||||
import androidx.compose.material.icons.filled.WifiOff
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.core.navigation.AppScreen
|
||||||
import at.mocode.frontend.features.billing.presentation.BillingScreen
|
import at.mocode.frontend.features.billing.presentation.BillingScreen
|
||||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
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.
|
* Haupt-Layout der Desktop-App gemäß Vision_03.
|
||||||
*
|
*
|
||||||
* Struktur:
|
* Struktur:
|
||||||
* - TopBar (dunkelblau): App-Titel + Breadcrumb + Logout
|
* - NavigationRail (links): Globale Navigation
|
||||||
|
* - Header (oben): Breadcrumb + Status + Logout
|
||||||
* - Content: kontextabhängiger Screen
|
* - Content: kontextabhängiger Screen
|
||||||
*
|
|
||||||
* Kein Nav-Rail, keine Sidebar – Navigation erfolgt über
|
|
||||||
* Breadcrumb-Klicks und horizontale Tabs innerhalb der Screens.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DesktopMainLayout(
|
fun DesktopMainLayout(
|
||||||
@@ -62,18 +60,25 @@ fun DesktopMainLayout(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onLogout: () -> 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 obGeraet by rememberSaveable { mutableStateOf("") }
|
||||||
var obKey by rememberSaveable { mutableStateOf("") }
|
var obKey by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
||||||
|
// Navigation Rail (Modernere Seitenleiste)
|
||||||
|
DesktopNavRail(
|
||||||
|
currentScreen = currentScreen,
|
||||||
|
onNavigate = onNavigate
|
||||||
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
DesktopTopBar(
|
DesktopTopHeader(
|
||||||
currentScreen = currentScreen,
|
currentScreen = currentScreen,
|
||||||
onNavigate = onNavigate,
|
onNavigate = onNavigate,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
|
||||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||||
DesktopContentArea(
|
DesktopContentArea(
|
||||||
currentScreen = currentScreen,
|
currentScreen = currentScreen,
|
||||||
@@ -85,67 +90,168 @@ fun DesktopMainLayout(
|
|||||||
onObKeyChange = { obKey = it },
|
onObKeyChange = { obKey = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
DesktopFooterBar()
|
DesktopFooterBar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* TopBar: dunkelblauer Balken mit Breadcrumb-Navigation und Logout-Button.
|
private fun DesktopNavRail(
|
||||||
*
|
currentScreen: AppScreen,
|
||||||
* Breadcrumb-Logik:
|
onNavigate: (AppScreen) -> Unit
|
||||||
* - Root: "🏠 Admin - Verwaltung"
|
) {
|
||||||
* - Veranstaltung: "🏠 Admin - Verwaltung / Veranstaltung #<id>"
|
Surface(
|
||||||
* - Turnier: "🏠 Admin - Verwaltung / Veranstaltung #<id> / Turnier <tid>"
|
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
|
@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,
|
currentScreen: AppScreen,
|
||||||
onNavigate: (AppScreen) -> Unit,
|
onNavigate: (AppScreen) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(Dimens.TopBarHeight),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(horizontal = Dimens.SpacingL),
|
||||||
.fillMaxWidth()
|
|
||||||
.height(48.dp)
|
|
||||||
.background(TopBarColor)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
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) {
|
if (currentScreen !is AppScreen.Onboarding) {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Zurück",
|
contentDescription = "Zurück",
|
||||||
tint = TopBarTextColor,
|
modifier = Modifier.size(Dimens.IconSizeM),
|
||||||
modifier = Modifier
|
tint = MaterialTheme.colorScheme.primary
|
||||||
.size(20.dp)
|
|
||||||
.clickable { onBack() },
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
}
|
||||||
|
Spacer(Modifier.width(Dimens.SpacingS))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root-Link
|
// Breadcrumb-Segmente
|
||||||
Text(
|
BreadcrumbContent(currentScreen, onNavigate)
|
||||||
text = "Verwaltung",
|
}
|
||||||
color = TopBarTextColor,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
|
||||||
)
|
|
||||||
|
|
||||||
// Breadcrumb-Segmente je nach Screen
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BreadcrumbContent(
|
||||||
|
currentScreen: AppScreen,
|
||||||
|
onNavigate: (AppScreen) -> Unit
|
||||||
|
) {
|
||||||
when (currentScreen) {
|
when (currentScreen) {
|
||||||
is AppScreen.VeranstalterAuswahl -> {
|
is AppScreen.VeranstalterAuswahl -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstalter auswählen",
|
text = "Veranstalter auswählen",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,47 +259,39 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstalter-Verwaltung",
|
text = "Veranstalter-Verwaltung",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
)
|
)
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Neuer Veranstalter",
|
text = "Neuer Veranstalter",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstalterDetail -> {
|
is AppScreen.VeranstalterDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstalter-Verwaltung",
|
text = "Veranstalter-Verwaltung",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
)
|
)
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungProfil -> {
|
is AppScreen.VeranstaltungProfil -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstalter-Verwaltung",
|
text = "Veranstalter-Verwaltung",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
)
|
)
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstalter #${currentScreen.veranstalterId}",
|
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
||||||
},
|
},
|
||||||
@@ -201,33 +299,28 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungDetail -> {
|
is AppScreen.VeranstaltungDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstaltung #${currentScreen.id}",
|
text = "Veranstaltung #${currentScreen.id}",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.VeranstaltungNeu -> {
|
is AppScreen.VeranstaltungNeu -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Neue Veranstaltung",
|
text = "Neue Veranstaltung",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.TurnierDetail -> {
|
is AppScreen.TurnierDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
},
|
},
|
||||||
@@ -235,17 +328,14 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Turnier ${currentScreen.turnierId}",
|
text = "Turnier ${currentScreen.turnierId}",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.Billing -> {
|
is AppScreen.Billing -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onNavigate(AppScreen.VeranstaltungProfil(0, currentScreen.veranstaltungId))
|
onNavigate(AppScreen.VeranstaltungProfil(0, currentScreen.veranstaltungId))
|
||||||
},
|
},
|
||||||
@@ -253,8 +343,7 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Turnier ${currentScreen.turnierId}",
|
text = "Turnier ${currentScreen.turnierId}",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
|
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
|
||||||
},
|
},
|
||||||
@@ -262,17 +351,14 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Abrechnung",
|
text = "Abrechnung",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.TurnierNeu -> {
|
is AppScreen.TurnierNeu -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
color = TopBarTextColor.copy(alpha = 0.75f),
|
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.75f)),
|
||||||
fontSize = 14.sp,
|
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
},
|
},
|
||||||
@@ -280,8 +366,7 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Neues Turnier",
|
text = "Neues Turnier",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,8 +374,7 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Ping Service",
|
text = "Ping Service",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontSize = 14.sp,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,37 +382,27 @@ private fun DesktopTopBar(
|
|||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Vereine",
|
text = "Vereine",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.Meisterschaften -> {
|
is AppScreen.Meisterschaften -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Meisterschaften",
|
text = "Meisterschaften",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppScreen.Cups -> {
|
is AppScreen.Cups -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
text = "Cups",
|
text = "Cups",
|
||||||
color = TopBarTextColor,
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout wurde auf Kundenwunsch entfernt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
|
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
|
||||||
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
|
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
|
||||||
"OOE" -> "OÖ"
|
"OOE" -> "OÖ"
|
||||||
@@ -733,45 +807,71 @@ private fun DesktopContentArea(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DesktopFooterBar() {
|
private fun DesktopFooterBar() {
|
||||||
// Stub-Status für MVP
|
// Echte Status-Logik vorbereitet
|
||||||
val online = remember { mutableStateOf(true) }
|
val online = remember { mutableStateOf(true) }
|
||||||
val deviceConnected = remember { mutableStateOf(true) }
|
val deviceConnected = remember { mutableStateOf(true) }
|
||||||
val deviceName = "Richter-Turm"
|
val deviceName = "Richter-Turm"
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
tonalElevation = 1.dp
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(36.dp)
|
.height(32.dp)
|
||||||
.background(Color(0xFFF3F4F6))
|
.padding(horizontal = Dimens.SpacingS),
|
||||||
.padding(horizontal = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(
|
// Status: Cloud Sync
|
||||||
imageVector = if (online.value) Icons.Filled.Wifi else Icons.Filled.WifiOff,
|
StatusIndicator(
|
||||||
contentDescription = null,
|
icon = if (online.value) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
||||||
tint = if (online.value) Color(0xFF059669) else Color(0xFFDC2626)
|
label = if (online.value) "Cloud synchronisiert" else "Offline (Lokal)",
|
||||||
|
color = if (online.value) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(6.dp))
|
|
||||||
Text(if (online.value) "Online" else "Offline", color = Color(0xFF374151), fontSize = 12.sp)
|
Spacer(Modifier.width(Dimens.SpacingM))
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
Icon(Icons.Filled.Devices, contentDescription = null, tint = if (deviceConnected.value) Color(0xFF2563EB) else Color(0xFF9CA3AF))
|
// Status: LAN Devices (mDNS)
|
||||||
Spacer(Modifier.width(6.dp))
|
StatusIndicator(
|
||||||
Text(
|
icon = Icons.Filled.Lan,
|
||||||
if (deviceConnected.value) "Verbunden: $deviceName" else "Kein Gerät verbunden",
|
label = if (deviceConnected.value) "Verbunden: $deviceName" else "Suche nach Geräten...",
|
||||||
color = Color(0xFF374151),
|
color = if (deviceConnected.value) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||||
fontSize = 12.sp
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (deviceConnected.value) {
|
Text(
|
||||||
OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) {
|
text = "v2.4.0-rc1 | Desktop-Alpha",
|
||||||
Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
|
style = MaterialTheme.typography.labelSmall,
|
||||||
Spacer(Modifier.width(6.dp))
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-22
@@ -1,5 +1,6 @@
|
|||||||
package at.mocode.desktop.v2
|
package at.mocode.desktop.v2
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -51,10 +52,10 @@ fun OnboardingScreen(
|
|||||||
val frKey = remember { FocusRequester() }
|
val frKey = remember { FocusRequester() }
|
||||||
val frBtn = remember { FocusRequester() }
|
val frBtn = remember { FocusRequester() }
|
||||||
|
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = geraetName,
|
value = geraetName,
|
||||||
onValueChange = { onGeraetNameChange(it) },
|
onValueChange = { onGeraetNameChange(it) },
|
||||||
label = { Text("Gerätename (Pflicht)") },
|
label = "Gerätename (Pflicht)",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(frName)
|
.focusRequester(frName)
|
||||||
@@ -70,18 +71,15 @@ fun OnboardingScreen(
|
|||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Next),
|
imeAction = ImeAction.Next,
|
||||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = secureKey,
|
value = secureKey,
|
||||||
onValueChange = { onSecureKeyChange(it) },
|
onValueChange = { onSecureKeyChange(it) },
|
||||||
label = { Text("Sicherheitsschlüssel (Pflicht)") },
|
label = "Sicherheitsschlüssel (Pflicht)",
|
||||||
trailingIcon = {
|
trailingIcon = if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||||
IconButton(onClick = { showPw = !showPw }) {
|
onTrailingIconClick = { showPw = !showPw },
|
||||||
Icon(if (showPw) Icons.Default.VisibilityOff else Icons.Default.Visibility, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (showPw) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -106,7 +104,7 @@ fun OnboardingScreen(
|
|||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
,
|
,
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
imeAction = ImeAction.Done,
|
||||||
keyboardActions = KeyboardActions(onDone = {
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
if (geraetName.trim().length >= 3 && secureKey.trim().length >= 8) {
|
if (geraetName.trim().length >= 3 && secureKey.trim().length >= 8) {
|
||||||
onContinue(geraetName, secureKey)
|
onContinue(geraetName, secureKey)
|
||||||
@@ -189,14 +187,14 @@ fun PferdProfilV2(id: Long, onBack: () -> Unit) {
|
|||||||
title = { Text("Pferd bearbeiten") },
|
title = { Text("Pferd bearbeiten") },
|
||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
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)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
|
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
|
||||||
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
|
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(geb, { geb = it }, label = { Text("Geburtsdatum") }, modifier = Modifier.weight(1f))
|
MsTextField(geb, { geb = it }, label = "Geburtsdatum", modifier = Modifier.weight(1f))
|
||||||
OutlinedTextField(farbe, { farbe = it }, label = { Text("Farbe") }, 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 = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(vor, { vor = it }, label = { Text("Vorname") }, modifier = Modifier.weight(1f))
|
MsTextField(vor, { vor = it }, label = "Vorname", modifier = Modifier.weight(1f))
|
||||||
OutlinedTextField(nach, { nach = it }, label = { Text("Nachname") }, modifier = Modifier.weight(1f))
|
MsTextField(nach, { nach = it }, label = "Nachname", modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("ÖPS-Nr.") }, modifier = Modifier.weight(1f))
|
MsTextField(oeps, { oeps = it }, label = "ÖPS-Nr.", modifier = Modifier.weight(1f))
|
||||||
OutlinedTextField(fei, { fei = it }, label = { Text("FEI-ID") }, modifier = Modifier.weight(1f))
|
MsTextField(fei, { fei = it }, label = "FEI-ID", modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedTextField(liz, { liz = it }, label = { Text("Lizenzklasse") }, modifier = Modifier.weight(1f))
|
MsTextField(liz, { liz = it }, label = "Lizenzklasse", modifier = Modifier.weight(1f))
|
||||||
OutlinedTextField(verein, { verein = it }, label = { Text("Verein") }, modifier = Modifier.weight(1f))
|
MsTextField(verein, { verein = it }, label = "Verein", modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user