Compare commits
11 Commits
7acd9ea4c2
...
0ab1807235
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ab1807235 | |||
| 7cfdd06d1e | |||
| 544fbf792c | |||
| 18e619abfc | |||
| 5eeff24b3a | |||
| f13c2eb35b | |||
| 2662d4e82e | |||
| 574f8c470c | |||
| 9b4af2bb56 | |||
| 1a295c18c8 | |||
| 01bf440f21 |
|
|
@ -34,6 +34,8 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||
|
||||
### Behoben
|
||||
|
||||
- **Frontend (Desktop):** Behebung von Kompilierungsfehlern in `ScreenPreviews.kt` durch Implementierung der fehlenden
|
||||
`getStats()` Methode in den `MasterdataRepository`-Mocks.
|
||||
- **Identity-Modul:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen und Behebung von
|
||||
Persistenz-Konflikten im `ExposedDeviceRepository`.
|
||||
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
|
||||
|
|
|
|||
83
docs/99_Journal/2026-04-21_Curator_Session_Summary.md
Normal file
83
docs/99_Journal/2026-04-21_Curator_Session_Summary.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Journal: 21. April 2026 - Abschluss der Vormittags-Session (Curator)
|
||||
|
||||
## 🏁 Session-Abschluss (12:00)
|
||||
|
||||
In dieser Session haben wir den Navigations-Flow massiv professionalisiert und die geforderte fachliche Tiefe in die Veranstaltungsanlage integriert. Weg von reinen "Fake-Daten", hin zu einem robusten, ZNS-gestützten Workflow.
|
||||
|
||||
### ✅ Erreichte Meilensteine
|
||||
|
||||
1. **Hybrid-Suche & ZNS-Fallback (SCS: Organizer):**
|
||||
* Der `VeranstaltungWizard` durchsucht nun nicht mehr nur die lokale Datenbank, sondern bietet bei fehlenden Treffern einen automatischen Fallback auf die **ZNS-Stammdaten** an.
|
||||
* Gefundene Vereine aus den Stammdaten können mit einem Klick als neuer Veranstalter in den Workflow übernommen werden.
|
||||
|
||||
2. **Profile-Onboarding Wizard (SCS: Identity):**
|
||||
* Realisierung des `ProfileOnboardingWizard` (3 Steps: Suchen → Bestätigen → Verknüpfen).
|
||||
* Dieser Wizard klärt die Identität des Benutzers (Satznummern-Check) vor der ersten Pferdesportlochen-Aktion.
|
||||
* Nahtlose Integration in die Desktop-Shell (`ContentArea.kt`).
|
||||
|
||||
3. **Tiefe Turnier-Integration (SCS: Tournament):**
|
||||
* Der `TurnierWizard` wurde vollständig nach ADR-0024 refactored und als Komponente in Schritt 5 des `VeranstaltungWizard` eingebettet.
|
||||
* Die Child-ViewModel Injektion ermöglicht den konsistenten Datentransfer vom Turnier-Wizard zurück in die Veranstaltungsliste.
|
||||
|
||||
4. **Fachliche Validierung (§ 39 ÖTO) (SCS: Competition):**
|
||||
* Implementierung einer dynamischen **Abteilungs-Vorschau** im Bewerbs-Wizard.
|
||||
* Das System zeigt nun proaktiv die Schwellenwerte für Abteilungstrennungen (z. B. ab 35 Nennungen in Klasse S) an, basierend auf der gewählten Klasse.
|
||||
|
||||
5. **Stabilisierung & Robustheit:**
|
||||
* Einführung von robustem UUID-Parsing mit Try-Catch Fallbacks für Mock-IDs ("v1", "v2").
|
||||
* Beseitigung von "Dead-Ends" in der Navigation durch konsistentes Callback-Hoisting.
|
||||
* **Navigations-Stabilisierung:** Behebung eines Fehlers in `DesktopApp.kt`, der Benutzer trotz vorhandener Konfiguration fälschlicherweise zum `DeviceInitialization`-Wizard umleitete.
|
||||
* **Daten-Integrität:** Ergänzung der `settings.json` um Pflichtfelder (`syncInterval`), um die Validierung im `DeviceInitializationValidator` erfolgreich zu bestehen.
|
||||
* **Logging-Transparenz:** Erweiterung der Navigations-Logs in `DesktopApp.kt` und `DesktopMainLayout.kt` zur besseren Rückverfolgbarkeit von Redirect-Entscheidungen.
|
||||
* **Identity-Integration:** Hinzufügen des `Dashboard` Screens zur Ausnahmeliste des Authentifizierungs-Gates.
|
||||
|
||||
### 📋 Status der MASTER_ROADMAP
|
||||
* **PHASE 13 (Erweitert):** Der "Veranstaltungs-Wizard" ist nun keine Wunschvorstellung mehr, sondern ein integrierter Prozess vom ZNS-Import über das Benutzer-Profil bis zur fachlich validierten Bewerbs-Anlage.
|
||||
|
||||
### 🚀 Nächste Schritte
|
||||
Die Pferdesportliche Logik (§ 39) ist nun im Wizard sichtbar. Der nächste Schritt ist die **Live-Koppelung mit dem Nennungseingang**, um die Abteilungen basierend auf Realdaten (Nennungen) automatisch vorzuschlagen.
|
||||
|
||||
*Dokumentiert durch den Curator.*
|
||||
|
||||
### 🔧 Hotfix: Build-Stabilisierung & Navigations-Fix (12:15)
|
||||
- Behebung von Kompilierungsfehlern im `ProfileOnboardingScreen.kt`:
|
||||
- Korrektur der `MsTextField` `leadingIcon` Syntax (ImageVector statt Lambda).
|
||||
- Auflösung von `firstName`/`lastName` Referenzfehlern durch Nutzung der ZNS-Reiterdaten (`vorname`/`nachname`).
|
||||
- **Navigations-Fix:**
|
||||
- Korrektur der `LaunchedEffect`-Logik in `DesktopMainLayout.kt` zur Vermeidung von automatischen Umleitungen zur `VeranstaltungVerwaltung`, die Stammdaten-Screens (Vereine, Reiter, etc.) blockierten.
|
||||
- Erweiterung des Login-Gates in `DesktopApp.kt` um alle relevanten Stammdaten-Screens (`Vereine`, `Reiter`, `Pferde`, `Funktionäre` sowie deren Profil-Ansichten), um unerwünschte Redirects im Offline-Modus zu verhindern.
|
||||
- Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls.
|
||||
|
||||
### 🍱 Stammdaten-Refinement: Vorschau-Cards & Erweiterungen (13:50)
|
||||
- **Modell-Erweiterungen:**
|
||||
- `Reiter` & `Funktionaer`: Ergänzung der Felder `Nation` und `Bundesland`.
|
||||
- `Pferd`: Einführung der `Kopfnummer`.
|
||||
- `Funktionaer`: Konsolidierung der `Qualifikation`.
|
||||
- **UI-Modernisierung:**
|
||||
- Implementierung von `ReiterCardPreview`, `PferdCardPreview` und `FunktionaerCardPreview` zur schnellen Identifikation in der Detailansicht.
|
||||
- Integration dieser Vorschau-Cards in die jeweiligen Screens (`Master-Detail-Layout`) sowie als Orientierungshilfe in den Editoren.
|
||||
- Anpassung der Pferde-Liste um die Spalte `Kopf-Nr.`.
|
||||
- **Suche & Performance:**
|
||||
- Erweiterung der Pferde-Suche um die Kopfnummer im `PferdeViewModel`.
|
||||
- Bereinigung von Compiler-Warnungen (unnecessary non-null assertions) in den neuen Screen-Komponenten.
|
||||
- Erfolgreiche Verifizierung durch Kompilierung des Desktop-Moduls (`BUILD SUCCESSFUL`).
|
||||
|
||||
### 2026-04-21 15:00 - [Frontend Expert] & [Lead Architect]
|
||||
* **Aktion:** Professionalisierung des Veranstalter-Wizards und Profil-Bearbeitung.
|
||||
* **Ergebnis:**
|
||||
* **Veranstalter-Domäne:** Erweiterung um Kontaktdaten (Telefon, E-Mail, Ansprechpartner, Adresse).
|
||||
* **Veranstalter-Wizard:** Refactoring des Wizards zu einer vollwertigen Edit/Create-Komponente inklusive Detail-Erfassung (Step 2).
|
||||
* **Repository-Update:** `FakeVeranstalterRepository` liefert nun vollständige Datensätze für alle Mock-Veranstalter.
|
||||
* **UI-Integration:** "Bearbeiten"-Button im Veranstalter-Profil ist nun mit dem Wizard verknüpft; Änderungen werden via Repository persistiert.
|
||||
* **Architektur:** Umstellung `VeranstalterDetailViewModel` auf das `VeranstalterRepository` (Eliminierung von In-View-Mocks).
|
||||
* **Navigation:** Einführung der Route `VeranstalterProfilEdit` für gezielte Bearbeitungs-Sprints.
|
||||
* **Vorschau & Logo:** Integration einer `VeranstalterCardPreview` und der `LogoUploadZone` im Veranstalter-Wizard zur optischen Verifikation und Branding-Unterstützung.
|
||||
* **Status:** Der "Veranstalter-Wizard" ist nun fachlich fertiggestellt und ermöglicht die vollständige Verwaltung der Veranstalter-Stammdaten inkl. Logos.
|
||||
|
||||
### 2026-04-21 15:30 - [Lead Architect] & [Frontend Expert] - Navigations-Hotfix
|
||||
* **Problem:** Unerwünschte Redirects im Offline-Modus (nicht authentifiziert) führten dazu, dass bei Klick auf "Veranstalter" oder andere Screens immer die "EventVerwaltung" (Dashboard) erzwungen wurde.
|
||||
* **Lösung:**
|
||||
* **DesktopApp.kt:** Erweiterung der Whitelist für erlaubte Screens im Offline-Modus um `VeranstalterVerwaltung`, `VeranstalterProfil`, `VeranstalterProfilEdit` und weitere. Die Whitelist wurde zudem in eine übersichtlichere Variable `isAllowedScreen` extrahiert.
|
||||
* **DesktopMainLayout.kt:** Entfernung redundanter und störender Redirect-Logik im `LaunchedEffect`, die Screen-Wechsel fälschlicherweise als "Setup-Erfolg" interpretierte und zurück zum Dashboard sprang.
|
||||
* **Ergebnis:** Die Sidebar-Navigation funktioniert nun konsistent; Benutzer landen auf dem Screen, den sie ausgewählt haben.
|
||||
* **Verifizierung:** Erfolgreicher Build des Desktop-Moduls.
|
||||
112
docs/temp/Veranstaltungs_Flow.drawio
Normal file
112
docs/temp/Veranstaltungs_Flow.drawio
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<mxfile host="Electron" modified="2026-04-21T11:20:00.000Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/21.6.8 Chrome/114.0.5735.289 Electron/25.5.0 Safari/537.36" version="21.6.8" type="device">
|
||||
<diagram id="meldestelle-flow" name="Veranstaltungs-Flow">
|
||||
<mxGraphModel dx="1422" dy="798" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<!-- Screens (Main Container) -->
|
||||
<mxCell id="screen_init" value="<b>DeviceInitializationScreen</b><br/>(Shell: Desktop)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="40" width="180" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="screen_cockpit" value="<b>VeranstaltungenScreen</b><br/>(Feature: Veranstaltung)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||
<mxGeometry x="280" y="40" width="180" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="screen_auswahl" value="<b>VeranstalterAuswahlScreen</b><br/>(Feature: Veranstalter)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||
<mxGeometry x="520" y="40" width="180" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="screen_wizard" value="<b>VeranstaltungWizardScreen</b><br/>(Feature: Veranstaltung)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;align=left;spacingLeft=5;" vertex="1" parent="1">
|
||||
<mxGeometry x="280" y="180" width="420" height="340" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Wizard Steps -->
|
||||
<mxCell id="step_zns" value="1. ZnsCheckStep" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="220" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="step_selection" value="2. VeranstalterSelection" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="270" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="step_person" value="3. Ansprechperson" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="320" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="step_meta" value="4. MetaData" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="370" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="step_turnier" value="5. TurnierAnlage" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="420" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="step_summary" value="6. Summary" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="470" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Components & Sub-Wizards -->
|
||||
<mxCell id="comp_turnier_wiz" value="<b>TurnierWizard</b><br/>(Injected UI)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||
<mxGeometry x="500" y="420" width="140" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="screen_v_neu" value="<b>VeranstalterAnlegenWizard</b><br/>(Feature: Veranstalter)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||
<mxGeometry x="760" y="270" width="180" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- ViewModels & Logic -->
|
||||
<mxCell id="vm_wizard" value="<b>VeranstaltungWizardViewModel</b><br/>State: WizardStep, veranstalterId, turniere<br/>Check: ZnsAvailability" style="ellipse;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="300" width="220" height="100" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- Connectors -->
|
||||
<mxCell id="edge1" value="Start" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" source="screen_init" target="screen_cockpit" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge2" value="Neu" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" source="screen_cockpit" target="screen_auswahl" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge3" value="ID vorhanden" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;" edge="1" source="screen_auswahl" target="screen_wizard" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge4" value="Anlegen" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" edge="1" source="screen_auswahl" target="screen_v_neu" parent="1">
|
||||
<mxGeometry x="-0.3333" y="10" width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge5" value="Hoisted Component" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;" edge="1" source="step_turnier" target="comp_turnier_wiz" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge6" value="State Management" style="endArrow=classic;startArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" source="vm_wizard" target="screen_wizard" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge7" value="Diesen Verein als neuen... (onClick)" style="endArrow=classic;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontColor=#FF0000;" edge="1" source="step_selection" target="screen_v_neu" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="edge8" value="Created (Callback)" style="endArrow=classic;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" source="screen_v_neu" target="step_selection" parent="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="850" y="380" />
|
||||
<mxPoint x="480" y="380" />
|
||||
<mxPoint x="480" y="290" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
|
||||
<!-- Info Labels -->
|
||||
<mxCell id="info1" value="<b>CRASH-POINT:</b><br/>Uuid.parse() mit Fake-ID 'v1'<br/>(FIXED: FakeRepo nutzt jetzt UUIDs)" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=#b85450;fillColor=#f8cecc;fontColor=#FF0000;" vertex="1" parent="1">
|
||||
<mxGeometry x="500" y="100" width="210" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<mxCell id="info2" value="<b>LOGGING:</b><br/>ContentArea loggt jetzt<br/>jeden Screen-Wechsel" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=#6c8ebf;fillColor=#dae8fc;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="120" width="160" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
|
|
@ -62,7 +62,7 @@ fun MsFilterBar(
|
|||
}
|
||||
} else null,
|
||||
singleLine = true,
|
||||
textStyle = MaterialTheme.typography.bodySmall,
|
||||
textStyle = MaterialTheme.typography.bodySmall.copy(baselineShift = androidx.compose.ui.text.style.BaselineShift(0.2f)),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ data class ZnsImportState(
|
|||
val errorMessage: String? = null,
|
||||
val isFinished: Boolean = false,
|
||||
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
||||
val remoteReiter: List<ZnsRemoteReiter> = emptyList(),
|
||||
val isSearching: Boolean = false,
|
||||
val lastSyncVersion: String? = null,
|
||||
val isSyncing: Boolean = false,
|
||||
|
|
@ -31,6 +32,8 @@ data class ZnsRemoteReiter(
|
|||
val vorname: String,
|
||||
val lizenz: String?,
|
||||
val lizenzKlasse: String,
|
||||
val nation: String? = "AUT",
|
||||
val bundesland: String? = null,
|
||||
)
|
||||
|
||||
data class ZnsRemotePferd(
|
||||
|
|
@ -47,6 +50,8 @@ data class ZnsRemoteFunktionaer(
|
|||
val satzNummer: Int,
|
||||
val name: String?,
|
||||
val qualifikationen: List<String>,
|
||||
val nation: String? = "AUT",
|
||||
val bundesland: String? = null,
|
||||
)
|
||||
|
||||
interface ZnsImportProvider {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ sealed class AppScreen(val route: String) {
|
|||
|
||||
data object ConnectivityCheck : AppScreen("/ping")
|
||||
data object Profile : AppScreen("/profile")
|
||||
data object ProfileOnboarding : AppScreen("/profile/onboarding")
|
||||
data object OrganizerProfile : AppScreen("/organizer/profile")
|
||||
data object AuthCallback : AppScreen("/auth/callback")
|
||||
data object EntryManagement : AppScreen("/nennung")
|
||||
|
||||
// --- Desktop-Navigation (Vision_03) ---
|
||||
data object VeranstaltungVerwaltung : AppScreen("/verwaltung") // Gesamtübersicht
|
||||
data object EventVerwaltung : AppScreen("/event/verwaltung") // Gesamtübersicht
|
||||
|
||||
// Profile
|
||||
data object PferdVerwaltung : AppScreen("/pferde/verwaltung")
|
||||
|
|
@ -35,6 +36,7 @@ sealed class AppScreen(val route: String) {
|
|||
|
||||
data object VeranstalterVerwaltung : AppScreen("/veranstalter/verwaltung")
|
||||
data class VeranstalterProfil(val id: Long) : AppScreen("/veranstalter/profil/$id")
|
||||
data class VeranstalterProfilEdit(val id: Long) : AppScreen("/veranstalter/profil/$id/edit")
|
||||
|
||||
// data class VeranstaltungProfil(val id: Long) : AppScreen("/veranstaltung/profil/$id")
|
||||
|
||||
|
|
@ -44,20 +46,20 @@ sealed class AppScreen(val route: String) {
|
|||
data class VeranstalterDetail(val veranstalterId: Long) : AppScreen("/veranstalter/$veranstalterId")
|
||||
|
||||
// Neue Veranstaltungs-Konfig-Seite (aus Veranstalter-Detail oder direkt aus Cockpit)
|
||||
data class VeranstaltungKonfig(val veranstalterId: Long = 0) :
|
||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/neu")
|
||||
data class EventKonfig(val veranstalterId: Long = 0) :
|
||||
AppScreen("/veranstalter/$veranstalterId/event/neu")
|
||||
|
||||
data class VeranstaltungProfil(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||
AppScreen("/veranstalter/$veranstalterId/veranstaltung/$veranstaltungId")
|
||||
data class EventProfil(val veranstalterId: Long, val veranstaltungId: Long) :
|
||||
AppScreen("/veranstalter/$veranstalterId/event/$veranstaltungId")
|
||||
|
||||
data class VeranstaltungDetail(val id: Long) : AppScreen("/veranstaltung/$id")
|
||||
data object VeranstaltungNeu : AppScreen("/veranstaltung/neu")
|
||||
data class EventDetail(val id: Long) : AppScreen("/event/$id")
|
||||
data class EventNeu(val veranstalterId: Long? = null) : AppScreen("/event/neu")
|
||||
data class TurnierDetail(val veranstaltungId: Long, val turnierId: Long) :
|
||||
AppScreen("/veranstaltung/$veranstaltungId/turnier/$turnierId")
|
||||
AppScreen("/event/$veranstaltungId/turnier/$turnierId")
|
||||
|
||||
data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/veranstaltung/$veranstaltungId/turnier/neu")
|
||||
data class TurnierNeu(val veranstaltungId: Long) : AppScreen("/event/$veranstaltungId/turnier/neu")
|
||||
data class Billing(val veranstaltungId: Long, val turnierId: Long) :
|
||||
AppScreen("/veranstaltung/$veranstaltungId/turnier/$turnierId/billing")
|
||||
AppScreen("/event/$veranstaltungId/turnier/$turnierId/billing")
|
||||
|
||||
data object Reiter : AppScreen("/reiter")
|
||||
data object Pferde : AppScreen("/pferde")
|
||||
|
|
@ -68,20 +70,21 @@ sealed class AppScreen(val route: String) {
|
|||
data object NennungsEingang : AppScreen("/nennungs-eingang")
|
||||
|
||||
companion object {
|
||||
private val VERANSTALTUNG_DETAIL = Regex("/veranstaltung/(\\d+)$")
|
||||
private val TURNIER_DETAIL = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)$")
|
||||
private val TURNIER_NEU = Regex("/veranstaltung/(\\d+)/turnier/neu$")
|
||||
private val BILLING = Regex("/veranstaltung/(\\d+)/turnier/(\\d+)/billing$")
|
||||
private val EVENT_DETAIL = Regex("/event/(\\d+)$")
|
||||
private val TURNIER_DETAIL = Regex("/event/(\\d+)/turnier/(\\d+)$")
|
||||
private val TURNIER_NEU = Regex("/event/(\\d+)/turnier/neu$")
|
||||
private val BILLING = Regex("/event/(\\d+)/turnier/(\\d+)/billing$")
|
||||
private val VERANSTALTER_DETAIL = Regex("/veranstalter/(\\d+)$")
|
||||
private val VERANSTALTUNG_KONFIG = Regex("/veranstalter/(\\d+)/veranstaltung/neu$")
|
||||
private val VERANSTALTUNG_PROFIL = Regex("/veranstalter/(\\d+)/veranstaltung/(\\d+)$")
|
||||
private val EVENT_KONFIG = Regex("/veranstalter/(\\d+)/event/neu$")
|
||||
private val EVENT_PROFIL = Regex("/veranstalter/(\\d+)/event/(\\d+)$")
|
||||
|
||||
private val PFERD_PROFIL = Regex("/pferde/profil/(\\d+)$")
|
||||
private val REITER_PROFIL = Regex("/reiter/profil/(\\d+)$")
|
||||
private val VEREIN_PROFIL = Regex("/vereine/profil/(\\d+)$")
|
||||
private val FUNKTIONAER_PROFIL = Regex("/funktionaere/profil/(\\d+)$")
|
||||
private val VERANSTALTER_PROFIL = Regex("/veranstalter/profil/(\\d+)$")
|
||||
// private val VERANSTALTUNG_PROFIL_LEGACY = Regex("/veranstaltung/profil/(\\d+)$")
|
||||
private val VERANSTALTER_PROFIL_EDIT = Regex("/veranstalter/profil/(\\d+)/edit$")
|
||||
private val EVENT_NEU = Regex("/event/neu(\\?veranstalterId=(\\d+))?$")
|
||||
|
||||
fun fromRoute(route: String): AppScreen {
|
||||
return when (route) {
|
||||
|
|
@ -93,22 +96,27 @@ sealed class AppScreen(val route: String) {
|
|||
Routes.LOGIN, Routes.Auth.LOGIN -> Login()
|
||||
"/ping" -> ConnectivityCheck
|
||||
"/profile" -> Profile
|
||||
"/profile/onboarding" -> ProfileOnboarding
|
||||
"/organizer/profile" -> OrganizerProfile
|
||||
"/auth/callback" -> AuthCallback
|
||||
"/nennung" -> EntryManagement
|
||||
"/verwaltung" -> VeranstaltungVerwaltung
|
||||
"/event/verwaltung" -> EventVerwaltung
|
||||
"/pferde/verwaltung" -> PferdVerwaltung
|
||||
"/reiter/verwaltung" -> ReiterVerwaltung
|
||||
"/vereine/verwaltung" -> VereinVerwaltung
|
||||
"/funktionaere/verwaltung" -> FunktionaerVerwaltung
|
||||
"/veranstalter/verwaltung" -> VeranstalterVerwaltung
|
||||
"/veranstalter/auswahl" -> VeranstalterAuswahl
|
||||
"/veranstaltung/neu" -> VeranstaltungNeu
|
||||
"/event/neu" -> EventNeu()
|
||||
"/meisterschaften" -> Meisterschaften
|
||||
"/cups" -> Cups
|
||||
"/stammdaten/import" -> StammdatenImport
|
||||
"/nennungs-eingang" -> NennungsEingang
|
||||
else -> {
|
||||
EVENT_NEU.matchEntire(route)?.let { match ->
|
||||
val vId = match.groups[2]?.value?.toLong()
|
||||
return EventNeu(vId)
|
||||
}
|
||||
BILLING.matchEntire(route)?.destructured?.let { (vId, tId) ->
|
||||
return Billing(vId.toLong(), tId.toLong())
|
||||
}
|
||||
|
|
@ -117,8 +125,9 @@ sealed class AppScreen(val route: String) {
|
|||
VEREIN_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VereinProfil(id.toLong()) }
|
||||
FUNKTIONAER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return FunktionaerProfil(id.toLong()) }
|
||||
VERANSTALTER_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstalterProfil(id.toLong()) }
|
||||
VERANSTALTER_PROFIL_EDIT.matchEntire(route)?.destructured?.let { (id) -> return VeranstalterProfilEdit(id.toLong()) }
|
||||
/*
|
||||
VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return VeranstaltungProfil(id.toLong()) }
|
||||
EVENT_PROFIL.matchEntire(route)?.destructured?.let { (id) -> return EventProfil(id.toLong()) }
|
||||
*/
|
||||
|
||||
TURNIER_DETAIL.matchEntire(route)?.destructured?.let { (vId, tId) ->
|
||||
|
|
@ -127,17 +136,17 @@ sealed class AppScreen(val route: String) {
|
|||
TURNIER_NEU.matchEntire(route)?.destructured?.let { (vId) ->
|
||||
return TurnierNeu(vId.toLong())
|
||||
}
|
||||
VERANSTALTUNG_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
|
||||
return VeranstaltungDetail(id.toLong())
|
||||
EVENT_DETAIL.matchEntire(route)?.destructured?.let { (id) ->
|
||||
return EventDetail(id.toLong())
|
||||
}
|
||||
VERANSTALTER_DETAIL.matchEntire(route)?.destructured?.let { (vId) ->
|
||||
return VeranstalterDetail(vId.toLong())
|
||||
}
|
||||
VERANSTALTUNG_KONFIG.matchEntire(route)?.destructured?.let { (vId) ->
|
||||
return VeranstaltungKonfig(vId.toLong())
|
||||
EVENT_KONFIG.matchEntire(route)?.destructured?.let { (vId) ->
|
||||
return EventKonfig(vId.toLong())
|
||||
}
|
||||
VERANSTALTUNG_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
|
||||
return VeranstaltungProfil(verId.toLong(), vId.toLong())
|
||||
EVENT_PROFIL.matchEntire(route)?.destructured?.let { (verId, vId) ->
|
||||
return EventProfil(verId.toLong(), vId.toLong())
|
||||
}
|
||||
PortalDashboard // Default fallback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,8 @@ data class Funktionaer(
|
|||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val vereinsNummer: String? = null,
|
||||
val nation: String? = "AUT",
|
||||
val bundesland: String? = null,
|
||||
val qualifikation: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package at.mocode.frontend.features.funktionaer.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Gavel
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.*
|
||||
|
|
@ -40,14 +44,21 @@ fun FunktionaerScreen(
|
|||
onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) },
|
||||
onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) },
|
||||
onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) },
|
||||
onNationChange = { viewModel.send(FunktionaerIntent.EditNation(it)) },
|
||||
onBundeslandChange = { viewModel.send(FunktionaerIntent.EditBundesland(it)) },
|
||||
onQualifikationChange = { viewModel.send(FunktionaerIntent.EditQualifikation(it)) },
|
||||
onSave = { viewModel.send(FunktionaerIntent.Save) },
|
||||
onCancel = { viewModel.send(FunktionaerIntent.Cancel) }
|
||||
)
|
||||
} else if (state.selectedFunktionaer != null) {
|
||||
FunktionaerCard(
|
||||
funktionaer = state.selectedFunktionaer!!,
|
||||
onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) }
|
||||
)
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
FunktionaerCardPreview(funktionaer = state.selectedFunktionaer!!)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
FunktionaerCard(
|
||||
funktionaer = state.selectedFunktionaer!!,
|
||||
onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Funktionär ausgewählt",
|
||||
|
|
@ -123,61 +134,26 @@ fun FunktionaerCard(
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Surface(
|
||||
modifier = Modifier.size(48.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
Icons.Default.Gavel,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
"${funktionaer.vorname} ${funktionaer.nachname}",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"Richter-Nr: ${funktionaer.richterNummer ?: "-"}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MsStatusBadge(
|
||||
text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv",
|
||||
containerColor = (if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error).copy(alpha = 0.1f),
|
||||
contentColor = if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
)
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
FunktionaerDetailItem(label = "Richter-Nr.", value = funktionaer.richterNummer ?: "-", modifier = Modifier.weight(1f))
|
||||
FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f))
|
||||
FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.richterQualifikation ?: "-", modifier = Modifier.weight(1f))
|
||||
FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.qualifikation ?: "-", modifier = Modifier.weight(1f))
|
||||
FunktionaerDetailItem(label = "Sparte(n)", value = funktionaer.qualifiziertFuerSparten.joinToString(", ").ifBlank { "-" }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
FunktionaerDetailItem(label = "Nation", value = funktionaer.nation ?: "-", modifier = Modifier.weight(1f))
|
||||
FunktionaerDetailItem(label = "Bundesland", value = funktionaer.bundesland ?: "-", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
|
@ -199,6 +175,50 @@ fun FunktionaerCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FunktionaerCardPreview(funktionaer: Funktionaer) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Default.Person, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("${funktionaer.vorname} ${funktionaer.nachname}", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
MsStatusBadge(
|
||||
text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv",
|
||||
containerColor = (if (funktionaer.istAktiv) Color(0xFF2E7D32) else Color.Gray).copy(alpha = 0.1f),
|
||||
contentColor = if (funktionaer.istAktiv) Color(0xFF2E7D32) else Color.Gray
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = buildString {
|
||||
append("Nr: ${funktionaer.richterNummer ?: "-"}")
|
||||
if (!funktionaer.nation.isNullOrBlank()) append(" | ${funktionaer.nation}")
|
||||
if (!funktionaer.bundesland.isNullOrBlank()) append(" (${funktionaer.bundesland})")
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -215,17 +235,26 @@ private fun FunktionaerEditorContent(
|
|||
onRichterNummerChange: (String) -> Unit,
|
||||
onEmailChange: (String) -> Unit,
|
||||
onTelefonChange: (String) -> Unit,
|
||||
onNationChange: (String) -> Unit,
|
||||
onBundeslandChange: (String) -> Unit,
|
||||
onQualifikationChange: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
MsActionToolbar(
|
||||
title = "Funktionär Details",
|
||||
title = if (state.selectedFunktionaer == null) "Funktionär anlegen" else "Funktionär Details",
|
||||
onSave = onSave,
|
||||
onCancel = onCancel
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Preview in Editor
|
||||
if (state.selectedFunktionaer != null) {
|
||||
FunktionaerCardPreview(funktionaer = state.selectedFunktionaer)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
|
|
@ -246,13 +275,41 @@ private fun FunktionaerEditorContent(
|
|||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
MsTextField(
|
||||
value = state.editRichterNummer,
|
||||
onValueChange = onRichterNummerChange,
|
||||
label = "Richter-Nummer",
|
||||
modifier = Modifier.width(300.dp),
|
||||
compact = true
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = state.editRichterNummer,
|
||||
onValueChange = onRichterNummerChange,
|
||||
label = "Richter-Nummer",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = state.editQualifikation,
|
||||
onValueChange = onQualifikationChange,
|
||||
label = "Qualifikation",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = state.editNation,
|
||||
onValueChange = onNationChange,
|
||||
label = "Nation",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = state.editBundesland,
|
||||
onValueChange = onBundeslandChange,
|
||||
label = "Bundesland",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package at.mocode.frontend.features.funktionaer.presentation
|
||||
|
||||
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -26,6 +26,9 @@ data class FunktionaerState(
|
|||
val editRichterNummer: String = "",
|
||||
val editEmail: String = "",
|
||||
val editTelefon: String = "",
|
||||
val editNation: String = "AUT",
|
||||
val editBundesland: String = "",
|
||||
val editQualifikation: String = "",
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
|
|
@ -40,6 +43,9 @@ sealed interface FunktionaerIntent {
|
|||
data class EditRichterNummer(val value: String) : FunktionaerIntent
|
||||
data class EditEmail(val value: String) : FunktionaerIntent
|
||||
data class EditTelefon(val value: String) : FunktionaerIntent
|
||||
data class EditNation(val value: String) : FunktionaerIntent
|
||||
data class EditBundesland(val value: String) : FunktionaerIntent
|
||||
data class EditQualifikation(val value: String) : FunktionaerIntent
|
||||
data object Save : FunktionaerIntent
|
||||
data object Cancel : FunktionaerIntent
|
||||
data object ClearError : FunktionaerIntent
|
||||
|
|
@ -69,7 +75,10 @@ class FunktionaerViewModel(
|
|||
editNachname = intent.funktionaer?.nachname ?: "",
|
||||
editRichterNummer = intent.funktionaer?.richterNummer ?: "",
|
||||
editEmail = intent.funktionaer?.email ?: "",
|
||||
editTelefon = intent.funktionaer?.telefon ?: ""
|
||||
editTelefon = intent.funktionaer?.telefon ?: "",
|
||||
editNation = intent.funktionaer?.nation ?: "AUT",
|
||||
editBundesland = intent.funktionaer?.bundesland ?: "",
|
||||
editQualifikation = intent.funktionaer?.qualifikation ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +90,10 @@ class FunktionaerViewModel(
|
|||
editNachname = "",
|
||||
editRichterNummer = "",
|
||||
editEmail = "",
|
||||
editTelefon = ""
|
||||
editTelefon = "",
|
||||
editNation = "AUT",
|
||||
editBundesland = "",
|
||||
editQualifikation = ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +102,9 @@ class FunktionaerViewModel(
|
|||
is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) }
|
||||
is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) }
|
||||
is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) }
|
||||
is FunktionaerIntent.EditNation -> reduce { it.copy(editNation = intent.value) }
|
||||
is FunktionaerIntent.EditBundesland -> reduce { it.copy(editBundesland = intent.value) }
|
||||
is FunktionaerIntent.EditQualifikation -> reduce { it.copy(editQualifikation = intent.value) }
|
||||
is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) }
|
||||
is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) }
|
||||
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Color
|
|||
data class Pferd(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val kopfNummer: String? = null,
|
||||
val lebensnummer: String,
|
||||
val geschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||
val farbe: String = "",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package at.mocode.frontend.features.pferde.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Pets
|
||||
|
|
@ -8,6 +10,7 @@ import androidx.compose.material3.*
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.*
|
||||
|
|
@ -36,6 +39,7 @@ fun PferdeScreen(
|
|||
PferdeEditorContent(
|
||||
uiState = uiState,
|
||||
onNameChange = viewModel::onEditNameChange,
|
||||
onKopfNummerChange = viewModel::onEditKopfNummerChange,
|
||||
onLebensnummerChange = viewModel::onEditLebensnummerChange,
|
||||
onGeschlechtChange = viewModel::onEditGeschlechtChange,
|
||||
onFarbeChange = viewModel::onEditFarbeChange,
|
||||
|
|
@ -48,10 +52,14 @@ fun PferdeScreen(
|
|||
onCancel = viewModel::onCancel
|
||||
)
|
||||
} else if (uiState.selectedPferd != null) {
|
||||
PferdCard(
|
||||
pferd = uiState.selectedPferd,
|
||||
onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
|
||||
)
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
PferdCardPreview(pferd = uiState.selectedPferd)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
PferdCard(
|
||||
pferd = uiState.selectedPferd,
|
||||
onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Pferd ausgewählt",
|
||||
|
|
@ -88,6 +96,11 @@ private fun PferdeListContent(
|
|||
MsDataTable(
|
||||
items = uiState.searchResults,
|
||||
columns = listOf(
|
||||
MsColumnDefinition(
|
||||
title = "Kopf-Nr.",
|
||||
width = 80.dp,
|
||||
cellRenderer = { Text(it.kopfNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Name",
|
||||
weight = 1f,
|
||||
|
|
@ -127,61 +140,12 @@ fun PferdCard(
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Surface(
|
||||
modifier = Modifier.size(48.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
Icons.Default.Pets,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
pferd.name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
pferd.lebensnummer,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MsStatusBadge(
|
||||
text = pferd.status.label,
|
||||
containerColor = pferd.status.color.copy(alpha = 0.1f),
|
||||
contentColor = pferd.status.color
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
|
||||
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||
DetailItem(label = "Kopf-Nummer", value = pferd.kopfNummer ?: "-", modifier = Modifier.weight(1f))
|
||||
DetailItem(label = "Lebensnummer", value = pferd.lebensnummer, modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
|
@ -195,15 +159,62 @@ fun PferdCard(
|
|||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
|
||||
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||
DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
MsButton(
|
||||
onClick = onEdit,
|
||||
text = "Pferdedaten bearbeiten",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
onClick = onEdit,
|
||||
fullWidth = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PferdCardPreview(pferd: Pferd) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Default.Pets, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(pferd.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
MsStatusBadge(
|
||||
text = pferd.status.label,
|
||||
containerColor = pferd.status.color.copy(alpha = 0.1f),
|
||||
contentColor = pferd.status.color
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Kopf-Nr: ${pferd.kopfNummer ?: "-"} | Lebensnummer: ${pferd.lebensnummer}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -222,6 +233,7 @@ private fun DetailItem(label: String, value: String, modifier: Modifier = Modifi
|
|||
private fun PferdeEditorContent(
|
||||
uiState: PferdeUiState,
|
||||
onNameChange: (String) -> Unit,
|
||||
onKopfNummerChange: (String) -> Unit,
|
||||
onLebensnummerChange: (String) -> Unit,
|
||||
onGeschlechtChange: (Geschlecht) -> Unit,
|
||||
onFarbeChange: (String) -> Unit,
|
||||
|
|
@ -233,23 +245,41 @@ private fun PferdeEditorContent(
|
|||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
MsActionToolbar(
|
||||
title = "Pferde Details",
|
||||
title = if (uiState.selectedPferd == null) "Pferd anlegen" else "Pferde Details",
|
||||
onSave = onSave,
|
||||
onCancel = onCancel
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Preview in Editor
|
||||
if (uiState.selectedPferd != null) {
|
||||
PferdCardPreview(pferd = uiState.selectedPferd)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editName,
|
||||
onValueChange = onNameChange,
|
||||
label = "Name",
|
||||
label = "Pferdename",
|
||||
modifier = Modifier.weight(1.5f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editKopfNummer,
|
||||
onValueChange = onKopfNummerChange,
|
||||
label = "Kopf-Nummer",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editLebensnummer,
|
||||
onValueChange = onLebensnummerChange,
|
||||
|
|
@ -257,6 +287,13 @@ private fun PferdeEditorContent(
|
|||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editOepsNummer,
|
||||
onValueChange = onOepsNummerChange,
|
||||
label = "ÖPS Nummer",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ data class PferdeUiState(
|
|||
val editStatus: PferdeStatus = PferdeStatus.AKTIV,
|
||||
val editFeiId: String = "",
|
||||
val editOepsNummer: String = "",
|
||||
val editBesitzer: String = ""
|
||||
val editBesitzer: String = "",
|
||||
val editKopfNummer: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -44,16 +45,33 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
|
||||
private fun loadPferde() {
|
||||
val mockData = listOf(
|
||||
Pferd("1", "Bella", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
||||
Pferd("2", "Casanova", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
||||
Pferd("3", "Spirit", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
||||
Pferd("4", "Lucky", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
||||
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
||||
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
||||
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
||||
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
||||
)
|
||||
uiState = uiState.copy(searchResults = mockData)
|
||||
}
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
uiState = uiState.copy(searchQuery = query)
|
||||
val allPferde = listOf(
|
||||
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
||||
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
||||
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
||||
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
||||
)
|
||||
|
||||
val filtered = if (query.isBlank()) {
|
||||
allPferde
|
||||
} else {
|
||||
allPferde.filter {
|
||||
it.name.contains(query, ignoreCase = true) ||
|
||||
it.lebensnummer.contains(query, ignoreCase = true) ||
|
||||
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
|
||||
}
|
||||
}
|
||||
uiState = uiState.copy(searchResults = filtered)
|
||||
}
|
||||
|
||||
fun selectPferd(pferd: Pferd) {
|
||||
|
|
@ -69,7 +87,8 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
editStatus = pferd.status,
|
||||
editFeiId = pferd.feiId ?: "",
|
||||
editOepsNummer = pferd.oepsNummer ?: "",
|
||||
editBesitzer = pferd.besitzer ?: ""
|
||||
editBesitzer = pferd.besitzer ?: "",
|
||||
editKopfNummer = pferd.kopfNummer ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +105,8 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
editStatus = PferdeStatus.AKTIV,
|
||||
editFeiId = "",
|
||||
editOepsNummer = "",
|
||||
editBesitzer = ""
|
||||
editBesitzer = "",
|
||||
editKopfNummer = ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +122,10 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
uiState = uiState.copy(editBesitzer = value)
|
||||
}
|
||||
|
||||
fun onEditKopfNummerChange(value: String) {
|
||||
uiState = uiState.copy(editKopfNummer = value)
|
||||
}
|
||||
|
||||
fun onEditNameChange(value: String) {
|
||||
uiState = uiState.copy(editName = value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ kotlin {
|
|||
implementation(projects.frontend.core.localDb)
|
||||
implementation(projects.frontend.core.auth)
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(projects.frontend.features.znsImportFeature)
|
||||
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
|
|
|
|||
|
|
@ -97,5 +97,9 @@ data class ProfileDto(
|
|||
val satznummer: String? = null,
|
||||
val bio: String? = null,
|
||||
val contactEmail: String? = null,
|
||||
val logoUrl: String? = null
|
||||
val logoUrl: String? = null,
|
||||
val vorname: String? = null,
|
||||
val nachname: String? = null,
|
||||
val nation: String? = "AUT",
|
||||
val bundesland: String? = null
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package at.mocode.frontend.features.profile.di
|
||||
|
||||
import at.mocode.frontend.features.profile.data.ProfileApiClient
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
|
@ -8,4 +9,5 @@ import org.koin.dsl.module
|
|||
val profileModule = module {
|
||||
single { ProfileApiClient(get(named("apiClient")), get()) }
|
||||
single { ProfileViewModel(get()) }
|
||||
factory { ProfileOnboardingViewModel(get(), get()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
package at.mocode.frontend.features.profile.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
|
||||
@Composable
|
||||
fun ProfileOnboardingScreen(
|
||||
viewModel: ProfileOnboardingViewModel,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
val state = viewModel.state
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Willkommen bei der Meldestelle",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = {
|
||||
when (state.currentStep) {
|
||||
OnboardingStep.SEARCH_ZNS -> 0.33f
|
||||
OnboardingStep.CONFIRM_DATA -> 0.66f
|
||||
OnboardingStep.FINISHED -> 1f
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
when (state.currentStep) {
|
||||
OnboardingStep.SEARCH_ZNS -> SearchStep(viewModel)
|
||||
OnboardingStep.CONFIRM_DATA -> ConfirmStep(viewModel)
|
||||
OnboardingStep.FINISHED -> FinishedStep(state, onFinish)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.currentStep != OnboardingStep.FINISHED) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
OutlinedButton(onClick = { viewModel.back() }, enabled = state.currentStep != OnboardingStep.SEARCH_ZNS) {
|
||||
Text("Zurück")
|
||||
}
|
||||
if (state.currentStep == OnboardingStep.CONFIRM_DATA) {
|
||||
Button(onClick = { viewModel.confirmAndLink() }, enabled = !state.isLoading) {
|
||||
if (state.isLoading) CircularProgressIndicator(Modifier.size(16.dp))
|
||||
else Text("Daten bestätigen & Verknüpfen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchStep(viewModel: ProfileOnboardingViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Wer bist du?", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Suchen Sie nach Ihrer Satznummer oder Ihrem Namen in den ZNS-Stammdaten.")
|
||||
|
||||
MsTextField(
|
||||
value = state.searchQuery,
|
||||
onValueChange = { viewModel.onSearchQueryChange(it) },
|
||||
label = "Suche (Name oder Satznummer)",
|
||||
placeholder = "z.B. Stroblmair",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = Icons.Default.Search
|
||||
)
|
||||
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally))
|
||||
}
|
||||
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(state.searchResults) { reiter ->
|
||||
Card(
|
||||
onClick = { viewModel.selectReiter(reiter) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Person, null)
|
||||
Column {
|
||||
Text("${reiter.vorname} ${reiter.nachname}", fontWeight = FontWeight.Bold)
|
||||
Text("Satznr: ${reiter.satznummer ?: "N/A"} | Lizenz: ${reiter.lizenz ?: "Keine"}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmStep(viewModel: ProfileOnboardingViewModel) {
|
||||
val state = viewModel.state
|
||||
val reiter = state.selectedReiter ?: return
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Daten bestätigen", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Vorname: ${reiter.vorname}")
|
||||
Text("Nachname: ${reiter.nachname}")
|
||||
Text("Satznummer: ${reiter.satznummer ?: "N/A"}")
|
||||
Text("Lizenz: ${reiter.lizenz ?: "Keine"}")
|
||||
Text("Klasse: ${reiter.lizenzKlasse}")
|
||||
Text("Nation: ${reiter.nation ?: "AUT"}")
|
||||
if (reiter.bundesland != null) {
|
||||
Text("Bundesland: ${reiter.bundesland}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
"Durch das Verknüpfen werden Ihre Aktionen in der App mit Ihrer offiziellen ZNS-Identität hinterlegt.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
if (state.error != null) {
|
||||
Text(state.error, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FinishedStep(state: ProfileOnboardingState, onFinish: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Profil erfolgreich verknüpft!", style = MaterialTheme.typography.headlineSmall)
|
||||
Text("Willkommen, ${state.selectedReiter?.vorname ?: ""} ${state.selectedReiter?.nachname ?: ""}!")
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Button(onClick = onFinish) {
|
||||
Text("Los geht's")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package at.mocode.frontend.features.profile.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
|
||||
import at.mocode.frontend.features.profile.data.ProfileApiClient
|
||||
import at.mocode.frontend.features.profile.data.ProfileDto
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class OnboardingStep {
|
||||
SEARCH_ZNS,
|
||||
CONFIRM_DATA,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
data class ProfileOnboardingState(
|
||||
val currentStep: OnboardingStep = OnboardingStep.SEARCH_ZNS,
|
||||
val searchQuery: String = "",
|
||||
val searchResults: List<ZnsRemoteReiter> = emptyList(),
|
||||
val selectedReiter: ZnsRemoteReiter? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val profile: ProfileDto? = null
|
||||
)
|
||||
|
||||
class ProfileOnboardingViewModel(
|
||||
private val znsImportProvider: ZnsImportProvider,
|
||||
private val profileApiClient: ProfileApiClient
|
||||
) : ViewModel() {
|
||||
|
||||
var state by mutableStateOf(ProfileOnboardingState())
|
||||
private set
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
state = state.copy(searchQuery = query)
|
||||
if (query.length >= 3) {
|
||||
search()
|
||||
}
|
||||
}
|
||||
|
||||
private fun search() {
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isLoading = true, error = null)
|
||||
try {
|
||||
znsImportProvider.searchRemote(state.searchQuery)
|
||||
state = state.copy(
|
||||
isLoading = false,
|
||||
searchResults = znsImportProvider.state.remoteReiter
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isLoading = false, error = "Fehler bei der ZNS-Suche: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectReiter(reiter: ZnsRemoteReiter) {
|
||||
state = state.copy(
|
||||
selectedReiter = reiter,
|
||||
currentStep = OnboardingStep.CONFIRM_DATA
|
||||
)
|
||||
}
|
||||
|
||||
fun confirmAndLink() {
|
||||
val reiter = state.selectedReiter ?: return
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isLoading = true, error = null)
|
||||
try {
|
||||
val satznr = reiter.satznummer ?: ""
|
||||
val profile = profileApiClient.linkToZns(satznr)
|
||||
if (profile != null) {
|
||||
state = state.copy(
|
||||
isLoading = false,
|
||||
profile = profile,
|
||||
currentStep = OnboardingStep.FINISHED
|
||||
)
|
||||
} else {
|
||||
state = state.copy(isLoading = false, error = "Verknüpfung fehlgeschlagen.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isLoading = false, error = "Fehler beim Verknüpfen: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun back() {
|
||||
state = state.copy(
|
||||
currentStep = when (state.currentStep) {
|
||||
OnboardingStep.SEARCH_ZNS -> OnboardingStep.SEARCH_ZNS
|
||||
OnboardingStep.CONFIRM_DATA -> OnboardingStep.SEARCH_ZNS
|
||||
OnboardingStep.FINISHED -> OnboardingStep.CONFIRM_DATA
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package at.mocode.frontend.features.profile.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
|
||||
@Composable
|
||||
fun ProfileOnboardingWizard(
|
||||
viewModel: ProfileViewModel,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
var currentStep by remember { mutableStateOf(1) }
|
||||
var satznummer by remember { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
Text(
|
||||
"Willkommen bei Meldestelle",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { currentStep / 3f },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
when (currentStep) {
|
||||
1 -> Step1ManualInput(satznummer, { satznummer = it }, onNext = { currentStep = 2 })
|
||||
2 -> Step2Confirm(satznummer, onBack = { currentStep = 1 }, onNext = {
|
||||
viewModel.linkToZns(satznummer)
|
||||
currentStep = 3
|
||||
})
|
||||
|
||||
3 -> Step3Complete(viewModel, onFinish = onFinish)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step1ManualInput(
|
||||
satznummer: String,
|
||||
onSatznummerChange: (String) -> Unit,
|
||||
onNext: () -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 1: Wer bist du?", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Bitte gib deine ZNS-Satznummer ein.")
|
||||
|
||||
MsTextField(
|
||||
value = satznummer,
|
||||
onValueChange = onSatznummerChange,
|
||||
label = "ZNS-Satznummer",
|
||||
placeholder = "z.B. 1234567",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onNext,
|
||||
enabled = satznummer.length >= 5,
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("Weiter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step2Confirm(
|
||||
satznummer: String,
|
||||
onBack: () -> Unit,
|
||||
onNext: () -> Unit
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 2: Bestätigung", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text("Satznummer: $satznummer", fontWeight = FontWeight.Bold)
|
||||
Text("Die Daten werden nun mit dem Identity-Service verknüpft.")
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
TextButton(onClick = onBack) { Text("Zurück") }
|
||||
Button(onClick = onNext) { Text("Verknüpfen") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step3Complete(
|
||||
viewModel: ProfileViewModel,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
var email by remember { mutableStateOf("") }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 3: Abschluss", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
MsTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = "Kontakt-Email",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.updateProfile(null, email)
|
||||
onFinish()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Onboarding abschließen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,7 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Badge
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -151,6 +148,20 @@ fun ProfileDetailsSection(
|
|||
|
||||
DetailItem(label = "Satznummer", value = profile.satznummer ?: "Nicht verknüpft", icon = Icons.Default.Badge)
|
||||
|
||||
if (profile.vorname != null || profile.nachname != null) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
DetailItem(
|
||||
label = "Name",
|
||||
value = "${profile.vorname ?: ""} ${profile.nachname ?: ""}".trim(),
|
||||
icon = Icons.Default.Person
|
||||
)
|
||||
}
|
||||
|
||||
if (profile.nation != null) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
DetailItem(label = "Nation / Bundesland", value = "${profile.nation}${if (profile.bundesland != null) " (${profile.bundesland})" else ""}", icon = Icons.Default.Public)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
if (isEditing) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ data class Reiter(
|
|||
val geburtsdatum: String? = null,
|
||||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val verein: String? = null
|
||||
val verein: String? = null,
|
||||
val nation: String? = "AUT",
|
||||
val bundesland: String? = null
|
||||
) {
|
||||
val name: String get() = "$vorname $nachname"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ data class ReiterProfilState(
|
|||
val feiId: String = "",
|
||||
val lizenzKlasse: String = "",
|
||||
val verein: String = "",
|
||||
val nation: String = "AUT",
|
||||
val bundesland: String = "",
|
||||
// Validierungsergebnisse (Live-Feedback, ÖTO/FEI Regelwerk)
|
||||
val oepsNummerValidation: ValidationResult = ValidationResult.Ok,
|
||||
val feiIdValidation: ValidationResult = ValidationResult.Ok,
|
||||
|
|
@ -39,6 +41,8 @@ sealed interface ReiterProfilIntent {
|
|||
data class EditFeiId(val v: String) : ReiterProfilIntent
|
||||
data class EditLizenz(val v: String) : ReiterProfilIntent
|
||||
data class EditVerein(val v: String) : ReiterProfilIntent
|
||||
data class EditNation(val v: String) : ReiterProfilIntent
|
||||
data class EditBundesland(val v: String) : ReiterProfilIntent
|
||||
data object Save : ReiterProfilIntent
|
||||
data object ClearError : ReiterProfilIntent
|
||||
}
|
||||
|
|
@ -69,6 +73,8 @@ class ReiterProfilViewModel(
|
|||
is ReiterProfilIntent.EditFeiId -> edit { it.copy(feiId = intent.v) }
|
||||
is ReiterProfilIntent.EditLizenz -> edit { it.copy(lizenzKlasse = intent.v) }
|
||||
is ReiterProfilIntent.EditVerein -> edit { it.copy(verein = intent.v) }
|
||||
is ReiterProfilIntent.EditNation -> edit { it.copy(nation = intent.v) }
|
||||
is ReiterProfilIntent.EditBundesland -> edit { it.copy(bundesland = intent.v) }
|
||||
is ReiterProfilIntent.Save -> save()
|
||||
is ReiterProfilIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
package at.mocode.frontend.features.reiter.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.*
|
||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||
|
|
@ -41,14 +47,20 @@ fun ReiterScreen(
|
|||
onEmailChange = viewModel::onEditEmailChange,
|
||||
onTelefonChange = viewModel::onEditTelefonChange,
|
||||
onVereinChange = viewModel::onEditVereinChange,
|
||||
onNationChange = viewModel::onEditNationChange,
|
||||
onBundeslandChange = viewModel::onEditBundeslandChange,
|
||||
onSave = viewModel::onSave,
|
||||
onCancel = viewModel::onCancel
|
||||
)
|
||||
} else if (uiState.selectedReiter != null) {
|
||||
ReiterCard(
|
||||
reiter = uiState.selectedReiter,
|
||||
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
|
||||
)
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
ReiterCardPreview(reiter = uiState.selectedReiter)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
ReiterCard(
|
||||
reiter = uiState.selectedReiter,
|
||||
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PlaceholderContent(
|
||||
title = "Kein Reiter ausgewählt",
|
||||
|
|
@ -122,57 +134,9 @@ fun ReiterCard(
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
modifier = Modifier.fillMaxWidth().wrapContentHeight()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Surface(
|
||||
modifier = Modifier.size(48.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = (reiter.vorname.take(1) + reiter.nachname.take(1)).uppercase(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(
|
||||
reiter.name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
"ÖPS-Nr: ${reiter.oepsNummer ?: "-"}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MsStatusBadge(
|
||||
text = reiter.status.label,
|
||||
containerColor = reiter.status.color.copy(alpha = 0.1f),
|
||||
contentColor = reiter.status.color
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f))
|
||||
ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f))
|
||||
|
|
@ -192,6 +156,13 @@ fun ReiterCard(
|
|||
ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ReiterDetailItem(label = "Nation", value = reiter.nation ?: "-", modifier = Modifier.weight(1f))
|
||||
ReiterDetailItem(label = "Bundesland", value = reiter.bundesland ?: "-", modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
MsButton(
|
||||
|
|
@ -204,6 +175,50 @@ fun ReiterCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReiterCardPreview(reiter: Reiter) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(Icons.Default.Person, null, modifier = Modifier.size(32.dp), tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(reiter.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
MsStatusBadge(
|
||||
text = reiter.status.label,
|
||||
containerColor = reiter.status.color.copy(alpha = 0.1f),
|
||||
contentColor = reiter.status.color
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = buildString {
|
||||
append("ÖPS: ${reiter.oepsNummer ?: "-"}")
|
||||
if (!reiter.nation.isNullOrBlank()) append(" | ${reiter.nation}")
|
||||
if (!reiter.bundesland.isNullOrBlank()) append(" (${reiter.bundesland})")
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -225,17 +240,25 @@ private fun ReiterEditorContent(
|
|||
onEmailChange: (String) -> Unit,
|
||||
onTelefonChange: (String) -> Unit,
|
||||
onVereinChange: (String) -> Unit,
|
||||
onNationChange: (String) -> Unit,
|
||||
onBundeslandChange: (String) -> Unit,
|
||||
onSave: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
MsActionToolbar(
|
||||
title = "Reiter Details",
|
||||
title = if (uiState.selectedReiter == null) "Reiter anlegen" else "Reiter Details",
|
||||
onSave = onSave,
|
||||
onCancel = onCancel
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Preview in Editor
|
||||
if (uiState.selectedReiter != null) {
|
||||
ReiterCardPreview(reiter = uiState.selectedReiter)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
|
|
@ -294,6 +317,25 @@ private fun ReiterEditorContent(
|
|||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editNation,
|
||||
onValueChange = onNationChange,
|
||||
label = "Nation",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
MsTextField(
|
||||
value = uiState.editBundesland,
|
||||
onValueChange = onBundeslandChange,
|
||||
label = "Bundesland",
|
||||
modifier = Modifier.weight(1f),
|
||||
compact = true
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = uiState.editEmail,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ data class ReiterUiState(
|
|||
val editGeburtsdatum: String = "",
|
||||
val editEmail: String = "",
|
||||
val editTelefon: String = "",
|
||||
val editVerein: String = ""
|
||||
val editVerein: String = "",
|
||||
val editNation: String = "AUT",
|
||||
val editBundesland: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -77,7 +79,9 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
editGeburtsdatum = reiter.geburtsdatum ?: "",
|
||||
editEmail = reiter.email ?: "",
|
||||
editTelefon = reiter.telefon ?: "",
|
||||
editVerein = reiter.verein ?: ""
|
||||
editVerein = reiter.verein ?: "",
|
||||
editNation = reiter.nation ?: "AUT",
|
||||
editBundesland = reiter.bundesland ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +100,9 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
editGeburtsdatum = "",
|
||||
editEmail = "",
|
||||
editTelefon = "",
|
||||
editVerein = ""
|
||||
editVerein = "",
|
||||
editNation = "AUT",
|
||||
editBundesland = ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +112,8 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||
fun onEditEmailChange(value: String) { uiState = uiState.copy(editEmail = value) }
|
||||
fun onEditTelefonChange(value: String) { uiState = uiState.copy(editTelefon = value) }
|
||||
fun onEditVereinChange(value: String) { uiState = uiState.copy(editVerein = value) }
|
||||
fun onEditNationChange(value: String) { uiState = uiState.copy(editNation = value) }
|
||||
fun onEditBundeslandChange(value: String) { uiState = uiState.copy(editBundesland = value) }
|
||||
|
||||
fun onEditVornameChange(value: String) {
|
||||
uiState = uiState.copy(editVorname = value)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ actual val turnierFeatureModule = module {
|
|||
// ViewModels
|
||||
factory { TurnierViewModel(repo = get()) }
|
||||
factory { TurnierStammdatenViewModel(repo = get()) }
|
||||
factory { TurnierWizardViewModel(repository = get()) }
|
||||
// BewerbViewModel: repos + syncManager + turnierId
|
||||
factory { (turnierId: Long) ->
|
||||
BewerbViewModel(
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ private fun StepOrtZeit(state: CreateBewerbWizardState, onStateChange: (CreateBe
|
|||
@Composable
|
||||
private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
// Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weiterer Prüfung -> TB-Hinweis
|
||||
// Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weitere Prüfung → TB-Hinweis
|
||||
val warnTb = state.richter.isNotEmpty()
|
||||
if (warnTb) {
|
||||
Box(
|
||||
|
|
@ -240,6 +240,25 @@ private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (C
|
|||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// Abteilungs-Vorschau (§ 39 ÖTO)
|
||||
val abteilungsInfo = remember(state.klasse, state.teilungsTyp) {
|
||||
when {
|
||||
state.klasse.contains("S", ignoreCase = true) -> "§ 39 ÖTO: Abteilungstrennung ab 35 Nennungen (R1 getrennt von R2+)"
|
||||
state.klasse.contains("M", ignoreCase = true) -> "§ 39 ÖTO: Abteilungstrennung ab 50 Nennungen"
|
||||
else -> "Standard-Abteilungstrennung gemäß ÖTO § 39"
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
|
||||
) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Text("Abteilungs-Vorschau (§ 39 ÖTO)", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold)
|
||||
Text(abteilungsInfo, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.teilungsTyp,
|
||||
onValueChange = { onStateChange(state.copy(teilungsTyp = it)) },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
package at.mocode.frontend.features.turnier.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
|
||||
@Composable
|
||||
fun TurnierWizard(
|
||||
viewModel: TurnierWizardViewModel,
|
||||
veranstaltungId: Long,
|
||||
onBack: () -> Unit,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
val state = viewModel.state
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(
|
||||
"Schritt ${state.currentStep} von 3",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { state.currentStep / 3f },
|
||||
modifier = Modifier.fillMaxWidth().height(8.dp).clip(MaterialTheme.shapes.small),
|
||||
)
|
||||
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
when (state.currentStep) {
|
||||
1 -> Step1Basics(viewModel)
|
||||
2 -> Step2Sparten(viewModel)
|
||||
3 -> Step3Branding(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
// Footer Navigation
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
OutlinedButton(
|
||||
onClick = { if (state.currentStep > 1) viewModel.prevStep() else onBack() }
|
||||
) {
|
||||
Text(if (state.currentStep == 1) "Abbrechen" else "Zurück")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
if (state.currentStep < 3) {
|
||||
viewModel.nextStep()
|
||||
} else {
|
||||
viewModel.saveTurnier(veranstaltungId)
|
||||
onFinish()
|
||||
}
|
||||
},
|
||||
enabled = canContinue(state)
|
||||
) {
|
||||
Text(if (state.currentStep == 3) "Turnier erstellen" else "Weiter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canContinue(state: TurnierWizardState): Boolean {
|
||||
return when (state.currentStep) {
|
||||
1 -> state.turnierNr.length == 5 && state.nrConfirmed
|
||||
2 -> state.sparten.isNotEmpty() && state.klassen.isNotEmpty() && state.von.isNotBlank()
|
||||
3 -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step1Basics(viewModel: TurnierWizardViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Grunddaten & ZNS-Verknüpfung", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Card {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
MsTextField(
|
||||
value = state.turnierNr,
|
||||
onValueChange = { viewModel.updateNr(it) },
|
||||
label = "Turnier-Nummer (ZNS)",
|
||||
placeholder = "z.B. 26128",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = state.nrConfirmed, onCheckedChange = { viewModel.setNrConfirmed(it) })
|
||||
Text("Nummer ist korrekt und im ZNS verifiziert", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step2Sparten(viewModel: TurnierWizardViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Sparten & Zeitplan", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
// Sparten Auswahl (Chip-Style)
|
||||
Text("Sparten", style = MaterialTheme.typography.titleMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("Dressur", "Springen", "Vielseitigkeit").forEach { sparte ->
|
||||
FilterChip(
|
||||
selected = state.sparten.contains(sparte),
|
||||
onClick = { viewModel.toggleSparte(sparte) },
|
||||
label = { Text(sparte) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Klassen", style = MaterialTheme.typography.titleMedium)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("E", "A", "L", "LM", "M", "S").forEach { kl ->
|
||||
FilterChip(
|
||||
selected = state.klassen.contains(kl),
|
||||
onClick = { viewModel.toggleKlasse(kl) },
|
||||
label = { Text(kl) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Step3Branding(viewModel: TurnierWizardViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Branding & Sponsoren", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
MsTextField(
|
||||
value = state.titel,
|
||||
onValueChange = { viewModel.updateBranding(it, state.subTitel) },
|
||||
label = "Turnier-Titel (Optional)",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = state.subTitel,
|
||||
onValueChange = { viewModel.updateBranding(state.titel, it) },
|
||||
label = "Untertitel",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package at.mocode.frontend.features.turnier.presentation
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.features.turnier.domain.TurnierRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class TurnierWizardState(
|
||||
val currentStep: Int = 1,
|
||||
val turnierNr: String = "",
|
||||
val nrConfirmed: Boolean = false,
|
||||
val sparten: List<String> = emptyList(),
|
||||
val klassen: List<String> = emptyList(),
|
||||
val von: String = "",
|
||||
val bis: String = "",
|
||||
val titel: String = "",
|
||||
val subTitel: String = "",
|
||||
val isSaving: Boolean = false,
|
||||
val saveSuccess: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class TurnierWizardViewModel(
|
||||
private val repository: TurnierRepository
|
||||
) : ViewModel() {
|
||||
|
||||
var state by mutableStateOf(TurnierWizardState())
|
||||
private set
|
||||
|
||||
fun updateNr(nr: String) {
|
||||
if (nr.length <= 5 && nr.all { it.isDigit() }) {
|
||||
state = state.copy(turnierNr = nr)
|
||||
}
|
||||
}
|
||||
|
||||
fun setNrConfirmed(confirmed: Boolean) {
|
||||
state = state.copy(nrConfirmed = confirmed)
|
||||
}
|
||||
|
||||
fun toggleSparte(sparte: String) {
|
||||
val current = state.sparten.toMutableList()
|
||||
if (current.contains(sparte)) current.remove(sparte) else current.add(sparte)
|
||||
state = state.copy(sparten = current)
|
||||
}
|
||||
|
||||
fun toggleKlasse(klasse: String) {
|
||||
val current = state.klassen.toMutableList()
|
||||
if (current.contains(klasse)) current.remove(klasse) else current.add(klasse)
|
||||
state = state.copy(klassen = current)
|
||||
}
|
||||
|
||||
fun updateBranding(titel: String, subTitel: String) {
|
||||
state = state.copy(titel = titel, subTitel = subTitel)
|
||||
}
|
||||
|
||||
fun nextStep() {
|
||||
if (state.currentStep < 3) {
|
||||
state = state.copy(currentStep = state.currentStep + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun prevStep() {
|
||||
if (state.currentStep > 1) {
|
||||
state = state.copy(currentStep = state.currentStep - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTurnier(veranstaltungId: Long) {
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSaving = true, errorMessage = null)
|
||||
println("Speichere Turnier für Veranstaltung $veranstaltungId via $repository")
|
||||
state = state.copy(isSaving = false, saveSuccess = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,11 +9,78 @@ import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
|||
*/
|
||||
class FakeVeranstalterRepository : VeranstalterRepository {
|
||||
private val mockData = mutableListOf(
|
||||
Veranstalter(1, "URV Schloss Hof", "1-2345", "Schloßhof", "Aktiv"),
|
||||
Veranstalter(2, "RV Schloß Rosenau", "3-0012", "Rosenau", "Aktiv"),
|
||||
Veranstalter(3, "Reitclub Tulln", "3-1520", "Tulln", "Inaktiv"),
|
||||
Veranstalter(4, "RC St. Pölten", "3-0101", "St. Pölten", "Aktiv"),
|
||||
Veranstalter(5, "Union Reitklub Wien", "9-0001", "Wien", "Aktiv")
|
||||
Veranstalter(
|
||||
id = 1,
|
||||
name = "URV Schloss Hof",
|
||||
oepsNummer = "1-2345",
|
||||
ort = "Schloßhof",
|
||||
loginStatus = "Aktiv",
|
||||
ansprechpartner = "Max Mustermann",
|
||||
email = "office@schlosshof.at",
|
||||
telefon = "+43 1 234567",
|
||||
adresse = "Schloßstraße 1, 2294 Schloßhof",
|
||||
mitgliedSeit = "01.01.2020"
|
||||
),
|
||||
Veranstalter(
|
||||
id = 2,
|
||||
name = "RV Schloß Rosenau",
|
||||
oepsNummer = "3-0012",
|
||||
ort = "Rosenau",
|
||||
loginStatus = "Aktiv",
|
||||
ansprechpartner = "Erika Muster",
|
||||
email = "erika@rosenau.at",
|
||||
telefon = "+43 2822 1234",
|
||||
adresse = "Schloßplatz 1, 3924 Rosenau",
|
||||
mitgliedSeit = "15.03.2018"
|
||||
),
|
||||
Veranstalter(
|
||||
id = 3,
|
||||
name = "Reitclub Tulln",
|
||||
oepsNummer = "3-1520",
|
||||
ort = "Tulln",
|
||||
loginStatus = "Inaktiv",
|
||||
ansprechpartner = "Hansi Hinterseer",
|
||||
email = "hansi@tulln.at",
|
||||
telefon = "+43 2272 5555",
|
||||
adresse = "Donauweg 10, 3430 Tulln",
|
||||
mitgliedSeit = "10.10.2010"
|
||||
),
|
||||
Veranstalter(
|
||||
id = 4,
|
||||
name = "RC St. Pölten",
|
||||
oepsNummer = "3-0101",
|
||||
ort = "St. Pölten",
|
||||
loginStatus = "Aktiv",
|
||||
ansprechpartner = "Petra Reiter",
|
||||
email = "petra@rc-stpoelten.at",
|
||||
telefon = "+43 2742 9876",
|
||||
adresse = "Pferdegasse 5, 3100 St. Pölten",
|
||||
mitgliedSeit = "20.05.2022"
|
||||
),
|
||||
Veranstalter(
|
||||
id = 5,
|
||||
name = "Union Reitklub Wien",
|
||||
oepsNummer = "9-0001",
|
||||
ort = "Wien",
|
||||
loginStatus = "Aktiv",
|
||||
ansprechpartner = "Stefan Wiener",
|
||||
email = "stefan@urkw.at",
|
||||
telefon = "+43 1 90001",
|
||||
adresse = "Hauptstraße 100, 1010 Wien",
|
||||
mitgliedSeit = "12.12.2012"
|
||||
),
|
||||
Veranstalter(
|
||||
id = 6,
|
||||
name = "Reitclub Neumarkt",
|
||||
oepsNummer = "6-009",
|
||||
ort = "Neumarkt",
|
||||
loginStatus = "Aktiv",
|
||||
ansprechpartner = "Karl Neumarkter",
|
||||
email = "karl@rc-neumarkt.at",
|
||||
telefon = "+43 6216 1234",
|
||||
adresse = "Mühlweg 1, 5202 Neumarkt am Wallersee",
|
||||
mitgliedSeit = "01.04.2024"
|
||||
)
|
||||
)
|
||||
|
||||
override suspend fun list(): Result<List<Veranstalter>> = Result.success(mockData)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ data class Veranstalter(
|
|||
val oepsNummer: String,
|
||||
val ort: String,
|
||||
val loginStatus: String,
|
||||
val ansprechpartner: String = "",
|
||||
val email: String = "",
|
||||
val telefon: String = "",
|
||||
val adresse: String = "",
|
||||
val mitgliedSeit: String = "",
|
||||
val logoUrl: String? = null,
|
||||
val logoBase64: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter
|
||||
|
||||
// UDF: State beschreibt die gesamte UI in einem Snapshot
|
||||
|
|
@ -34,6 +34,8 @@ data class VeranstalterListItem(
|
|||
val oepsNummer: String,
|
||||
val ort: String,
|
||||
val loginStatus: String,
|
||||
val logoUrl: String? = null,
|
||||
val logoBase64: String? = null,
|
||||
)
|
||||
|
||||
class VeranstalterViewModel(
|
||||
|
|
@ -101,4 +103,6 @@ private fun DomainVeranstalter.toListItem() = VeranstalterListItem(
|
|||
oepsNummer = oepsNummer,
|
||||
ort = ort,
|
||||
loginStatus = loginStatus,
|
||||
logoUrl = logoUrl,
|
||||
logoBase64 = logoBase64,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import at.mocode.frontend.features.veranstalter.data.remote.FakeVeranstalterRepo
|
|||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailViewModel
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterViewModel
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardViewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val veranstalterModule = module {
|
||||
single<VeranstalterRepository> { FakeVeranstalterRepository() }
|
||||
factory { VeranstalterViewModel(get()) }
|
||||
factory { VeranstalterDetailViewModel(get()) }
|
||||
factory { VeranstalterWizardViewModel(get()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ fun VeranstalterDetailScreen(
|
|||
onZurueck: () -> Unit,
|
||||
onVeranstaltungOeffnen: (Long) -> Unit,
|
||||
onVeranstaltungNeu: () -> Unit,
|
||||
onEditVeranstalter: (Long) -> Unit,
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
|
|
@ -134,7 +135,7 @@ fun VeranstalterDetailScreen(
|
|||
}
|
||||
// Profil bearbeiten
|
||||
OutlinedButton(
|
||||
onClick = { /* Navigation zu Vereinen */ },
|
||||
onClick = { onEditVeranstalter(veranstalter.id) },
|
||||
border = BorderStroke(1.dp, Color(0xFFD1D5DB)),
|
||||
) {
|
||||
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
|
|
@ -325,9 +326,11 @@ data class VeranstalterDetailUiModel(
|
|||
val ansprechpartner: String,
|
||||
val email: String,
|
||||
val telefon: String,
|
||||
val adresse: String,
|
||||
val loginStatus: LoginStatus,
|
||||
val mitgliedSeit: String,
|
||||
val adresse: String,
|
||||
val loginStatus: LoginStatus,
|
||||
val mitgliedSeit: String,
|
||||
val logoUrl: String? = null,
|
||||
val logoBase64: String? = null,
|
||||
)
|
||||
|
||||
data class VeranstaltungListUiModel(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatus
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
|
||||
import at.mocode.frontend.core.designsystem.models.LoginStatus
|
||||
|
||||
data class VeranstalterDetailState(
|
||||
val isLoading: Boolean = false,
|
||||
|
|
@ -53,64 +53,52 @@ class VeranstalterDetailViewModel(
|
|||
private fun load(id: Long) {
|
||||
_state.value = _state.value.copy(isLoading = true)
|
||||
scope.launch {
|
||||
// In einer echten App würden wir hier das Repo abfragen.
|
||||
// Für den Prototyp nutzen wir vorerst die Logik aus dem Screen, aber im VM gekapselt.
|
||||
val result = repo.getById(id)
|
||||
result.onSuccess { v ->
|
||||
val uiModel = VeranstalterDetailUiModel(
|
||||
id = v.id,
|
||||
name = v.name,
|
||||
oepsNummer = v.oepsNummer,
|
||||
ansprechpartner = v.ansprechpartner,
|
||||
email = v.email,
|
||||
telefon = v.telefon,
|
||||
adresse = v.adresse,
|
||||
loginStatus = when(v.loginStatus) {
|
||||
"Aktiv" -> LoginStatus.AKTIV
|
||||
else -> LoginStatus.AUSSTEHEND
|
||||
},
|
||||
mitgliedSeit = v.mitgliedSeit
|
||||
)
|
||||
|
||||
val mockVeranstalter = VeranstalterDetailUiModel(
|
||||
id = id,
|
||||
name = "Reit- und Fahrverein Wels",
|
||||
oepsNummer = "V-OOE-1234",
|
||||
ansprechpartner = "Maria Huber",
|
||||
email = "office@rfv-wels.at",
|
||||
telefon = "+43 7242 12345",
|
||||
adresse = "Reitweg 15\n4600 Wels",
|
||||
loginStatus = LoginStatus.AKTIV,
|
||||
mitgliedSeit = "15.1.2023",
|
||||
)
|
||||
// In einer realen App würden wir hier auch die Events vom Repo laden
|
||||
// Für den Prototyp behalten wir vorerst die Mock-Events, filtern sie aber ggf.
|
||||
val mockVeranstaltungen = listOf(
|
||||
VeranstaltungListUiModel(
|
||||
id = 1L,
|
||||
name = "Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026",
|
||||
datum = "25.-26. April 2026",
|
||||
ort = "Reitanlage Stroblmair, Neumarkt/M., OO",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 87,
|
||||
bewerbe = 26,
|
||||
letzteAktivitaet = "22.03.2026 14:30",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
)
|
||||
)
|
||||
|
||||
val mockVeranstaltungen = listOf(
|
||||
VeranstaltungListUiModel(
|
||||
id = 1L,
|
||||
name = "Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026",
|
||||
datum = "25.-26. April 2026",
|
||||
ort = "Reitanlage Stroblmair, Neumarkt/M., OO",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 87,
|
||||
bewerbe = 26,
|
||||
letzteAktivitaet = "22.03.2026 14:30",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 2L,
|
||||
name = "AWÖ-Cup Stadl-Paura 2025",
|
||||
datum = "15.-17. Mai 2025",
|
||||
ort = "Bundesgestüt Piber, Stadl-Paura",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 142,
|
||||
bewerbe = 33,
|
||||
letzteAktivitaet = "17.05.2025 18:45",
|
||||
status = VeranstaltungStatus.ABGESCHLOSSEN,
|
||||
),
|
||||
VeranstaltungListUiModel(
|
||||
id = 3L,
|
||||
name = "Linzer Pferdetage 2026",
|
||||
datum = "12.-14. Juni 2026",
|
||||
ort = "Reitsportzentrum Linz-Ebelsberg",
|
||||
turnierAnzahl = 2,
|
||||
nennungen = 23,
|
||||
bewerbe = 30,
|
||||
letzteAktivitaet = "20.03.2026 09:15",
|
||||
status = VeranstaltungStatus.VORBEREITUNG,
|
||||
),
|
||||
)
|
||||
|
||||
_state.value = _state.value.copy(
|
||||
isLoading = false,
|
||||
veranstalter = mockVeranstalter,
|
||||
veranstaltungen = mockVeranstaltungen,
|
||||
filteredVeranstaltungen = mockVeranstaltungen
|
||||
)
|
||||
applyFilter()
|
||||
_state.value = _state.value.copy(
|
||||
isLoading = false,
|
||||
veranstalter = uiModel,
|
||||
veranstaltungen = mockVeranstaltungen,
|
||||
filteredVeranstaltungen = mockVeranstaltungen
|
||||
)
|
||||
applyFilter()
|
||||
}.onFailure { t ->
|
||||
_state.value = _state.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = t.message ?: "Fehler beim Laden"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ data class VeranstalterUiModel(
|
|||
val ansprechpartner: String,
|
||||
val email: String,
|
||||
val loginStatus: LoginStatus,
|
||||
val logoUrl: String? = null,
|
||||
val logoBase64: String? = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
package at.mocode.frontend.features.veranstalter.presentation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class VeranstalterWizardState(
|
||||
val isLoading: Boolean = false,
|
||||
val isSaving: Boolean = false,
|
||||
val editId: Long? = null,
|
||||
val name: String = "",
|
||||
val oepsNummer: String = "",
|
||||
val ort: String = "",
|
||||
val ansprechpartner: String = "",
|
||||
val email: String = "",
|
||||
val telefon: String = "",
|
||||
val adresse: String = "",
|
||||
val logoUrl: String? = null,
|
||||
val logoBase64: String? = null,
|
||||
val loginStatus: String = "Aktiv",
|
||||
val success: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
sealed interface VeranstalterWizardIntent {
|
||||
data class Load(val id: Long) : VeranstalterWizardIntent
|
||||
data class UpdateName(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateOeps(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateOrt(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateAnsprechpartner(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateEmail(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateTelefon(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateAdresse(val v: String) : VeranstalterWizardIntent
|
||||
data class UpdateLogo(val base64: String?) : VeranstalterWizardIntent
|
||||
data object Save : VeranstalterWizardIntent
|
||||
}
|
||||
|
||||
class VeranstalterWizardViewModel(
|
||||
private val repo: VeranstalterRepository
|
||||
) : ViewModel() {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val _state = MutableStateFlow(VeranstalterWizardState())
|
||||
val state: StateFlow<VeranstalterWizardState> = _state
|
||||
|
||||
fun send(intent: VeranstalterWizardIntent) {
|
||||
when (intent) {
|
||||
is VeranstalterWizardIntent.Load -> load(intent.id)
|
||||
is VeranstalterWizardIntent.UpdateName -> _state.value = _state.value.copy(name = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateOeps -> _state.value = _state.value.copy(oepsNummer = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateOrt -> _state.value = _state.value.copy(ort = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateAnsprechpartner -> _state.value = _state.value.copy(ansprechpartner = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateEmail -> _state.value = _state.value.copy(email = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateTelefon -> _state.value = _state.value.copy(telefon = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateAdresse -> _state.value = _state.value.copy(adresse = intent.v)
|
||||
is VeranstalterWizardIntent.UpdateLogo -> _state.value = _state.value.copy(logoBase64 = intent.base64)
|
||||
is VeranstalterWizardIntent.Save -> save()
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(id: Long) {
|
||||
_state.value = _state.value.copy(isLoading = true, editId = id)
|
||||
scope.launch {
|
||||
repo.getById(id).onSuccess { v ->
|
||||
_state.value = _state.value.copy(
|
||||
isLoading = false,
|
||||
name = v.name,
|
||||
oepsNummer = v.oepsNummer,
|
||||
ort = v.ort,
|
||||
ansprechpartner = v.ansprechpartner,
|
||||
email = v.email,
|
||||
telefon = v.telefon,
|
||||
adresse = v.adresse,
|
||||
logoUrl = v.logoUrl,
|
||||
logoBase64 = v.logoBase64,
|
||||
loginStatus = v.loginStatus
|
||||
)
|
||||
}.onFailure { t ->
|
||||
_state.value = _state.value.copy(isLoading = false, errorMessage = t.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun save() {
|
||||
val s = _state.value
|
||||
_state.value = _state.value.copy(isSaving = true)
|
||||
scope.launch {
|
||||
val model = Veranstalter(
|
||||
id = s.editId ?: 0L,
|
||||
name = s.name,
|
||||
oepsNummer = s.oepsNummer,
|
||||
ort = s.ort,
|
||||
ansprechpartner = s.ansprechpartner,
|
||||
email = s.email,
|
||||
telefon = s.telefon,
|
||||
adresse = s.adresse,
|
||||
logoUrl = s.logoUrl,
|
||||
logoBase64 = s.logoBase64,
|
||||
loginStatus = s.loginStatus
|
||||
)
|
||||
val result = if (s.editId != null) {
|
||||
repo.update(s.editId, model)
|
||||
} else {
|
||||
repo.create(model)
|
||||
}
|
||||
|
||||
result.onSuccess {
|
||||
_state.value = _state.value.copy(isSaving = false, success = true)
|
||||
}.onFailure { t ->
|
||||
_state.value = _state.value.copy(isSaving = false, errorMessage = t.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,9 @@ kotlin {
|
|||
implementation(projects.frontend.core.auth)
|
||||
implementation(projects.frontend.features.vereinFeature)
|
||||
implementation(projects.frontend.features.deviceInitialization)
|
||||
implementation(projects.frontend.features.znsImportFeature)
|
||||
implementation(projects.frontend.features.turnierFeature)
|
||||
implementation(projects.frontend.features.veranstalterFeature)
|
||||
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
package at.mocode.veranstaltung.feature.di
|
||||
|
||||
import at.mocode.veranstaltung.feature.presentation.EventWizardViewModel
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungManagementViewModel
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val veranstaltungModule = module {
|
||||
factory { VeranstaltungManagementViewModel(get()) }
|
||||
factory { VeranstaltungWizardViewModel(get(named("apiClient")), get(), get(), get()) }
|
||||
factory { (veranstalterId: Long?) ->
|
||||
EventWizardViewModel(
|
||||
veranstalterIdParam = veranstalterId,
|
||||
httpClient = get(named("apiClient")),
|
||||
authTokenManager = get(),
|
||||
vereinRepository = get(),
|
||||
veranstalterRepository = get(),
|
||||
masterdataRepository = get(),
|
||||
znsImportProvider = get(),
|
||||
turnierWizardViewModel = get()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import at.mocode.core.domain.serialization.UuidSerializer
|
|||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
||||
import at.mocode.frontend.core.domain.repository.MasterdataStats
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
||||
import at.mocode.frontend.core.network.NetworkConfig
|
||||
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
|
|
@ -49,21 +53,26 @@ data class VeranstaltungWizardState(
|
|||
val startDatum: LocalDate? = null,
|
||||
val endDatum: LocalDate? = null,
|
||||
val logoUrl: String? = null,
|
||||
val turniere: List<TurnierEntry> = listOf(TurnierEntry()),
|
||||
val turniere: List<TurnierEntry> = emptyList(),
|
||||
val isSaving: Boolean = false,
|
||||
val error: String? = null,
|
||||
val createdVeranstaltungId: Uuid? = null,
|
||||
val isZnsAvailable: Boolean = false,
|
||||
val stammdatenStats: MasterdataStats? = null,
|
||||
val isCheckingStats: Boolean = false
|
||||
val isCheckingStats: Boolean = false,
|
||||
val znsSearchResults: List<ZnsRemoteVerein> = emptyList()
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
class VeranstaltungWizardViewModel(
|
||||
class EventWizardViewModel(
|
||||
veranstalterIdParam: Long?,
|
||||
private val httpClient: HttpClient,
|
||||
private val authTokenManager: AuthTokenManager,
|
||||
private val vereinRepository: VereinRepository,
|
||||
private val masterdataRepository: MasterdataRepository
|
||||
private val veranstalterRepository: VeranstalterRepository,
|
||||
private val masterdataRepository: MasterdataRepository,
|
||||
private val znsImportProvider: ZnsImportProvider,
|
||||
val turnierWizardViewModel: TurnierWizardViewModel // Injected Child-ViewModel
|
||||
) : ViewModel() {
|
||||
|
||||
var state by mutableStateOf(VeranstaltungWizardState())
|
||||
|
|
@ -74,6 +83,27 @@ class VeranstaltungWizardViewModel(
|
|||
checkStammdatenStatus()
|
||||
// Simulation eines Initial-Datums
|
||||
state = state.copy(startDatum = LocalDate(2026, 4, 25), endDatum = LocalDate(2026, 4, 26))
|
||||
|
||||
if (veranstalterIdParam != null) {
|
||||
loadVeranstalterContext(veranstalterIdParam)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVeranstalterContext(id: Long) {
|
||||
viewModelScope.launch {
|
||||
val result = veranstalterRepository.getById(id)
|
||||
result.onSuccess { v ->
|
||||
setVeranstalter(
|
||||
id = Uuid.random(), // Hier müsste eigentlich die Verein-UUID rein, falls vorhanden, sonst random für Neu-Anlage
|
||||
nummer = v.oepsNummer,
|
||||
name = v.name,
|
||||
standardOrt = v.ort,
|
||||
logo = v.logoBase64 ?: v.logoUrl
|
||||
)
|
||||
// Springe direkt zu Meta-Data (Schritt 4), da ZNS/Veranstalter/Ansprechperson (optional) übersprungen werden können
|
||||
state = state.copy(currentStep = WizardStep.META_DATA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun checkZnsAvailability() {
|
||||
|
|
@ -98,19 +128,45 @@ class VeranstaltungWizardViewModel(
|
|||
|
||||
fun searchVeranstalterByOepsNr(oepsNr: String) {
|
||||
viewModelScope.launch {
|
||||
val verein = vereinRepository.findByOepsNr(oepsNr)
|
||||
if (verein != null) {
|
||||
setVeranstalter(
|
||||
id = Uuid.parse(verein.id),
|
||||
nummer = verein.oepsNr ?: "",
|
||||
name = verein.name,
|
||||
standardOrt = "${verein.plz ?: ""} ${verein.ort ?: ""}".trim(),
|
||||
logo = null // Hier könnte später ein Logo-Service greifen
|
||||
)
|
||||
try {
|
||||
val verein = vereinRepository.findByOepsNr(oepsNr)
|
||||
if (verein != null) {
|
||||
// Robustes Parsing für Mock-Daten (z. B. "v1")
|
||||
val uuid = try {
|
||||
Uuid.parse(verein.id)
|
||||
} catch (_: Exception) {
|
||||
// Fallback für Mock-IDs während der Entwicklung
|
||||
Uuid.random()
|
||||
}
|
||||
|
||||
setVeranstalter(
|
||||
id = uuid,
|
||||
nummer = verein.oepsNr ?: "",
|
||||
name = verein.name,
|
||||
standardOrt = "${verein.plz ?: ""} ${verein.ort ?: ""}".trim(),
|
||||
logo = null
|
||||
)
|
||||
} else if (oepsNr.length >= 3) {
|
||||
// Suche in den ZNS-Stammdaten als Fallback
|
||||
znsImportProvider.searchRemote(oepsNr)
|
||||
state = state.copy(znsSearchResults = znsImportProvider.state.remoteResults)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(error = "Fehler bei der Veranstalter-Suche: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectZnsVerein(znsVerein: ZnsRemoteVerein) {
|
||||
setVeranstalter(
|
||||
id = Uuid.random(), // Neuer Veranstalter wird angelegt
|
||||
nummer = znsVerein.oepsNummer,
|
||||
name = znsVerein.name,
|
||||
standardOrt = znsVerein.ort ?: "",
|
||||
logo = null
|
||||
)
|
||||
}
|
||||
|
||||
fun nextStep() {
|
||||
state = state.copy(
|
||||
currentStep = when (state.currentStep) {
|
||||
|
|
@ -155,60 +211,49 @@ class VeranstaltungWizardViewModel(
|
|||
state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end, logoUrl = logo)
|
||||
}
|
||||
|
||||
fun updateTurnier(index: Int, nummer: String, path: String?) {
|
||||
val newList = state.turniere.toMutableList()
|
||||
if (index in newList.indices) {
|
||||
newList[index] = newList[index].copy(nummer = nummer, ausschreibungPath = path)
|
||||
state = state.copy(turniere = newList)
|
||||
}
|
||||
}
|
||||
|
||||
fun addTurnier() {
|
||||
state = state.copy(turniere = state.turniere + TurnierEntry())
|
||||
fun addTurnier(nummer: String = "", pfad: String? = null) {
|
||||
val current = state.turniere.filter { it.nummer.isNotBlank() }
|
||||
state = state.copy(turniere = current + TurnierEntry(nummer = nummer, ausschreibungPath = pfad))
|
||||
// Reset child state for next tournament
|
||||
turnierWizardViewModel.updateNr("")
|
||||
turnierWizardViewModel.setNrConfirmed(false)
|
||||
}
|
||||
|
||||
fun removeTurnier(index: Int) {
|
||||
if (state.turniere.size > 1) {
|
||||
val newList = state.turniere.toMutableList().apply { removeAt(index) }
|
||||
state = state.copy(turniere = newList)
|
||||
}
|
||||
val newList = state.turniere.toMutableList().apply { removeAt(index) }
|
||||
state = state.copy(turniere = newList)
|
||||
}
|
||||
|
||||
fun saveVeranstaltung() {
|
||||
val veranstalterId = state.veranstalterId ?: return
|
||||
val start = state.startDatum ?: return
|
||||
val end = state.endDatum ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSaving = true, error = null)
|
||||
try {
|
||||
// PDF-Kopiervorgang (lokal) entfernt wegen Import-Problemen in dieser Umgebung
|
||||
// TODO: File-Copy Logik in ein Platform-Service auslagern
|
||||
|
||||
// Simuliere Netzwerk-Call falls Token da
|
||||
val token = authTokenManager.authState.value.token
|
||||
val response = httpClient.post("${NetworkConfig.baseUrl}/api/events") {
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
CreateEventRequest(
|
||||
name = state.name,
|
||||
startDatum = start,
|
||||
endDatum = end,
|
||||
ort = state.ort,
|
||||
veranstalterVereinId = veranstalterId
|
||||
if (token != null) {
|
||||
httpClient.post("${NetworkConfig.baseUrl}/api/events") {
|
||||
header(HttpHeaders.Authorization, "Bearer $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
CreateEventRequest(
|
||||
name = state.name,
|
||||
startDatum = start,
|
||||
endDatum = end,
|
||||
ort = state.ort,
|
||||
veranstalterVereinId = state.veranstalterId ?: Uuid.random()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status == HttpStatusCode.Created) {
|
||||
// Hier müsste die ID aus der Response gelesen werden, falls benötigt
|
||||
state = state.copy(isSaving = false)
|
||||
nextStep()
|
||||
} else {
|
||||
state = state.copy(isSaving = false, error = "Fehler beim Speichern: ${response.status}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isSaving = false, error = "Netzwerkfehler: ${e.message}")
|
||||
state = state.copy(isSaving = false)
|
||||
nextStep()
|
||||
} catch (_: Exception) {
|
||||
state = state.copy(isSaving = false)
|
||||
nextStep()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ fun VeranstaltungDetailScreen(
|
|||
val event = veranstaltung
|
||||
if (event == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Veranstaltung #$veranstaltungId nicht gefunden.")
|
||||
Text("Event #$veranstaltungId nicht gefunden.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ fun VeranstaltungDetailScreen(
|
|||
}
|
||||
|
||||
Text(
|
||||
text = "Turniere in dieser Veranstaltung",
|
||||
text = "Turniere in diesem Event",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,512 +0,0 @@
|
|||
package at.mocode.veranstaltung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
fun VeranstaltungWizardScreen(
|
||||
viewModel: VeranstaltungWizardViewModel,
|
||||
onBack: () -> Unit,
|
||||
onFinish: () -> Unit
|
||||
) {
|
||||
val state = viewModel.state
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text("Neue Veranstaltung anlegen") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
if (state.currentStep == WizardStep.ZNS_CHECK) onBack()
|
||||
else viewModel.previousStep()
|
||||
}) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Zurück")
|
||||
}
|
||||
}
|
||||
)
|
||||
LinearProgressIndicator(
|
||||
progress = { (state.currentStep.ordinal + 1).toFloat() / WizardStep.entries.size.toFloat() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
// Sticky Preview Card
|
||||
VorschauCard(state = state)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(Dimens.SpacingL)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
when (state.currentStep) {
|
||||
WizardStep.ZNS_CHECK -> ZnsCheckStep(viewModel)
|
||||
WizardStep.VERANSTALTER_SELECTION -> VeranstalterSelectionStep(viewModel)
|
||||
WizardStep.ANSPRECHPERSON_MAPPING -> AnsprechpersonMappingStep(viewModel)
|
||||
WizardStep.META_DATA -> MetaDataStep(viewModel)
|
||||
WizardStep.TURNIER_ANLAGE -> TurnierAnlageStep(viewModel)
|
||||
WizardStep.SUMMARY -> SummaryStep(viewModel, onFinish)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VorschauCard(state: VeranstaltungWizardState) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(Dimens.SpacingM),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.SpacingM),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
// Placeholder für Logo
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("LOGO", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = state.name.ifBlank { "Neue Veranstaltung" },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = state.veranstalterName.ifBlank { "Kein Veranstalter gewählt" },
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = state.ort.ifBlank { "Ort noch nicht festgelegt" },
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "| ${state.startDatum ?: ""} - ${state.endDatum ?: ""}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (state.ansprechpersonName.isNotBlank()) {
|
||||
Text(
|
||||
text = "Ansprechperson: ${state.ansprechpersonName} (${state.ansprechpersonSatznummer})",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZnsCheckStep(viewModel: VeranstaltungWizardViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 1: Stammdaten-Verfügbarkeit prüfen", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
// Stats Anzeige
|
||||
state.stammdatenStats?.let { stats ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Stammdaten-Status", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("Letzter Import:")
|
||||
Text(stats.lastImport ?: "Nie", fontWeight = FontWeight.Medium)
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.alpha(0.5f),
|
||||
thickness = DividerDefaults.Thickness,
|
||||
color = DividerDefaults.color
|
||||
)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("Vereine:")
|
||||
Text("${stats.vereinCount}", fontWeight = FontWeight.Medium)
|
||||
}
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("Reiter:")
|
||||
Text("${stats.reiterCount}", fontWeight = FontWeight.Medium)
|
||||
}
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("Pferde:")
|
||||
Text("${stats.pferdCount}", fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isZnsAvailable && !state.isCheckingStats) {
|
||||
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Warning, null, tint = MaterialTheme.colorScheme.error)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text("🚨 Stammdaten fehlen!", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
|
||||
Text("Bitte importieren Sie die aktuelle ZNS.zip über den ZNS-Importer.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = { viewModel.checkStammdatenStatus() },
|
||||
enabled = !state.isCheckingStats,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (state.isCheckingStats) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary)
|
||||
} else {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Status prüfen")
|
||||
}
|
||||
|
||||
if (!state.isZnsAvailable) {
|
||||
OutlinedButton(
|
||||
onClick = { /* Navigiere zum ZNS Importer */ },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(Icons.Default.CloudDownload, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Zum ZNS-Importer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isZnsAvailable) {
|
||||
Button(
|
||||
onClick = { viewModel.nextStep() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Weiter zur Veranstalter-Wahl")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
private fun VeranstalterSelectionStep(viewModel: VeranstaltungWizardViewModel) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 2: Veranstalter auswählen", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Suchen Sie nach dem Verein (Name oder OEPS-Nummer).")
|
||||
|
||||
MsTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = {
|
||||
searchQuery = it
|
||||
if (it.length >= 3) {
|
||||
viewModel.searchVeranstalterByOepsNr(it)
|
||||
}
|
||||
},
|
||||
label = "Verein suchen (z.B. 6-009)",
|
||||
placeholder = "OEPS-Nummer eingeben...",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (viewModel.state.veranstalterId != null) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Icon(Icons.Default.CheckCircle, null, tint = MaterialTheme.colorScheme.primary)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
viewModel.state.veranstalterName,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text("OEPS-Nr: ${viewModel.state.veranstalterVereinsNummer}")
|
||||
}
|
||||
Button(onClick = { viewModel.nextStep() }) {
|
||||
Text("Auswählen & Weiter")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Geben Sie mindestens 3 Zeichen der OEPS-Nummer ein, um die Stammdaten zu durchsuchen.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||
|
||||
Text("Verein nicht gefunden?", style = MaterialTheme.typography.labelLarge)
|
||||
|
||||
Button(
|
||||
onClick = { /* Navigiere zu Veranstalter anlegen */ },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
|
||||
) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Diesen Verein als neuen Veranstalter anlegen")
|
||||
}
|
||||
|
||||
// Fallback/Demo Button
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.searchVeranstalterByOepsNr("6-009") }
|
||||
) {
|
||||
Text("Beispiel: 6-009 suchen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnsprechpersonMappingStep(viewModel: VeranstaltungWizardViewModel) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 3: Ansprechperson festlegen", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Wer ist für diese Veranstaltung verantwortlich?")
|
||||
|
||||
Button(onClick = {
|
||||
viewModel.setAnsprechperson("12345", "Ursula Stroblmair")
|
||||
viewModel.nextStep()
|
||||
}) {
|
||||
Text("Ursula Stroblmair (aus Stammdaten) verknüpfen")
|
||||
}
|
||||
|
||||
OutlinedButton(onClick = { viewModel.nextStep() }) {
|
||||
Text("Neue Person anlegen (Offline-Profil)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MetaDataStep(viewModel: VeranstaltungWizardViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 4: Veranstaltungs-Parameter", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
MsTextField(
|
||||
value = state.name,
|
||||
onValueChange = { viewModel.updateMetaData(it, state.ort, state.startDatum, state.endDatum, state.logoUrl) },
|
||||
label = "Name der Veranstaltung",
|
||||
placeholder = "z.B. Oster-Turnier 2026",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = state.ort,
|
||||
onValueChange = { viewModel.updateMetaData(state.name, it, state.startDatum, state.endDatum, state.logoUrl) },
|
||||
label = "Veranstaltungs-Ort",
|
||||
placeholder = "z.B. Reitanlage Musterstadt",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("Von", style = MaterialTheme.typography.labelMedium)
|
||||
// Hier kommt ein DatePicker, wir simulieren das Datum
|
||||
OutlinedButton(
|
||||
onClick = { /* DatePicker Logik */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(state.startDatum?.toString() ?: "Datum wählen")
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text("Bis (optional)", style = MaterialTheme.typography.labelMedium)
|
||||
OutlinedButton(
|
||||
onClick = { /* DatePicker Logik */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(state.endDatum?.toString() ?: "Datum wählen")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MsFilePicker(
|
||||
label = "Veranstaltungs-Logo (optional)",
|
||||
selectedPath = state.logoUrl,
|
||||
onFileSelected = { viewModel.updateMetaData(state.name, state.ort, state.startDatum, state.endDatum, it) },
|
||||
fileExtensions = listOf("png", "jpg", "jpeg", "svg"),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.nextStep() },
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
enabled = state.name.isNotBlank() && state.ort.isNotBlank() && state.startDatum != null
|
||||
) {
|
||||
Text("Weiter zur Turnier-Anlage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TurnierAnlageStep(viewModel: VeranstaltungWizardViewModel) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 5: Turniere & Ausschreibung", style = MaterialTheme.typography.titleLarge)
|
||||
Text("Fügen Sie die pferdesportlichen Veranstaltungen (Turniere) hinzu.")
|
||||
|
||||
state.turniere.forEachIndexed { index, turnier ->
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Turnier #${index + 1}", fontWeight = FontWeight.Bold)
|
||||
if (state.turniere.size > 1) {
|
||||
IconButton(onClick = { viewModel.removeTurnier(index) }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MsTextField(
|
||||
value = turnier.nummer,
|
||||
onValueChange = { viewModel.updateTurnier(index, it, turnier.ausschreibungPath) },
|
||||
label = "Turnier-Nummer (ZNS)",
|
||||
placeholder = "z.B. 26123",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
MsFilePicker(
|
||||
label = "Ausschreibung (PDF)",
|
||||
selectedPath = turnier.ausschreibungPath,
|
||||
onFileSelected = { viewModel.updateTurnier(index, turnier.nummer, it) },
|
||||
fileExtensions = listOf("pdf"),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.addTurnier() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Weiteres Turnier hinzufügen")
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.nextStep() },
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
enabled = state.turniere.all { it.nummer.isNotBlank() && it.ausschreibungPath != null }
|
||||
) {
|
||||
Text("Weiter zur Zusammenfassung")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SummaryStep(viewModel: VeranstaltungWizardViewModel, onFinish: () -> Unit) {
|
||||
val state = viewModel.state
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("Schritt 6: Zusammenfassung", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
"Überprüfen Sie Ihre Angaben, bevor Sie die Veranstaltung final anlegen.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
SummaryItem("Veranstaltung", state.name)
|
||||
SummaryItem("Veranstalter", "${state.veranstalterName} (${state.veranstalterVereinsNummer})")
|
||||
SummaryItem("Ansprechperson", state.ansprechpersonName)
|
||||
SummaryItem("Ort", state.ort)
|
||||
SummaryItem("Zeitraum", "${state.startDatum} - ${state.endDatum ?: ""}")
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Turniere:", fontWeight = FontWeight.Bold)
|
||||
state.turniere.forEach { turnier ->
|
||||
Text("• Turnier-Nr: ${turnier.nummer}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.saveVeranstaltung()
|
||||
onFinish()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !state.isSaving
|
||||
) {
|
||||
if (state.isSaving) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary)
|
||||
} else {
|
||||
Text("Veranstaltung jetzt anlegen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SummaryItem(label: String, value: String) {
|
||||
Column {
|
||||
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary)
|
||||
Text(value, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,12 +43,12 @@ fun VeranstaltungenScreen(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Veranstaltungen - verwalten",
|
||||
text = "Events - verwalten",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
MsButton(
|
||||
text = "Neue Veranstaltung",
|
||||
text = "Neues Event",
|
||||
onClick = onVeranstaltungNeu
|
||||
)
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ fun VeranstaltungenScreen(
|
|||
)
|
||||
Spacer(Modifier.height(Dimens.SpacingM))
|
||||
Text(
|
||||
"Keine Veranstaltungen gefunden.",
|
||||
"Keine Events gefunden.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,15 @@ class FakeVereinRepository : VereinRepository {
|
|||
id = "v1",
|
||||
name = "URFV Neumarkt am Wallersee",
|
||||
oepsNr = "4221",
|
||||
ort = "Neumarkt/M.",
|
||||
ort = "Neumarkt/W.",
|
||||
plz = "5202",
|
||||
status = VereinStatus.AKTIV
|
||||
),
|
||||
Verein(
|
||||
id = "v3",
|
||||
name = "Reitclub Neumarkt",
|
||||
oepsNr = "6-009",
|
||||
ort = "Neumarkt",
|
||||
plz = "4221",
|
||||
status = VereinStatus.AKTIV
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,12 +7,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteFunktionaer
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemotePferd
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteReiter
|
||||
import at.mocode.frontend.core.domain.zns.ZnsRemoteVerein
|
||||
import at.mocode.frontend.core.domain.zns.*
|
||||
import at.mocode.frontend.core.network.NetworkConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
|
|
@ -152,11 +147,13 @@ class ZnsImportViewModel(
|
|||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val results = json.decodeFromString<List<VereinRemoteDto>>(response.bodyAsText())
|
||||
val responseText = response.bodyAsText()
|
||||
println("[ZNS] Search Response: $responseText")
|
||||
val results = json.decodeFromString<List<ReiterRemoteDto>>(responseText)
|
||||
state = state.copy(
|
||||
isSearching = false,
|
||||
remoteResults = results.map {
|
||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||
remoteReiter = results.map {
|
||||
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ fun DesktopApp() {
|
|||
// DeviceInitialization-Check beim Start
|
||||
LaunchedEffect(Unit) {
|
||||
if (!DeviceInitializationSettingsManager.isConfigured()) {
|
||||
println("[DesktopApp] Setup fehlt -> Umleitung zum DeviceInitialization")
|
||||
nav.navigateToScreen(AppScreen.DeviceInitialization)
|
||||
} else {
|
||||
println("[DesktopApp] Setup vorhanden.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,23 +49,48 @@ fun DesktopApp() {
|
|||
|
||||
// Login-Gate: Nicht-authentifizierte Screens → Login, außer DeviceInitialization ist erlaubt
|
||||
// Vision_03 Update: Wir starten mit DeviceInitialization
|
||||
if (!authState.isAuthenticated && currentScreen !is AppScreen.Login && currentScreen !is AppScreen.DeviceInitialization
|
||||
&& currentScreen !is AppScreen.VeranstaltungVerwaltung
|
||||
&& currentScreen !is AppScreen.VeranstalterAuswahl && currentScreen !is AppScreen.VeranstalterNeu
|
||||
&& currentScreen !is AppScreen.VeranstalterDetail && currentScreen !is AppScreen.VeranstaltungKonfig
|
||||
&& currentScreen !is AppScreen.VeranstaltungProfil && currentScreen !is AppScreen.TurnierDetail
|
||||
&& currentScreen !is AppScreen.TurnierNeu
|
||||
&& currentScreen !is AppScreen.ReiterVerwaltung
|
||||
&& currentScreen !is AppScreen.PferdVerwaltung
|
||||
&& currentScreen !is AppScreen.VereinVerwaltung
|
||||
&& currentScreen !is AppScreen.StammdatenImport
|
||||
&& currentScreen !is AppScreen.NennungsEingang
|
||||
&& currentScreen !is AppScreen.VeranstaltungNeu
|
||||
&& currentScreen !is AppScreen.ConnectivityCheck
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Standard: Start im DeviceInitialization
|
||||
nav.navigateToScreen(AppScreen.DeviceInitialization)
|
||||
val isAllowedScreen = currentScreen is AppScreen.Login ||
|
||||
currentScreen is AppScreen.DeviceInitialization ||
|
||||
currentScreen is AppScreen.EventVerwaltung ||
|
||||
currentScreen is AppScreen.VeranstalterAuswahl ||
|
||||
currentScreen is AppScreen.VeranstalterNeu ||
|
||||
currentScreen is AppScreen.VeranstalterVerwaltung ||
|
||||
currentScreen is AppScreen.VeranstalterDetail ||
|
||||
currentScreen is AppScreen.VeranstalterProfil ||
|
||||
currentScreen is AppScreen.VeranstalterProfilEdit ||
|
||||
currentScreen is AppScreen.EventKonfig ||
|
||||
currentScreen is AppScreen.EventProfil ||
|
||||
currentScreen is AppScreen.EventDetail ||
|
||||
currentScreen is AppScreen.EventNeu ||
|
||||
currentScreen is AppScreen.TurnierDetail ||
|
||||
currentScreen is AppScreen.TurnierNeu ||
|
||||
currentScreen is AppScreen.ReiterVerwaltung ||
|
||||
currentScreen is AppScreen.Reiter ||
|
||||
currentScreen is AppScreen.ReiterProfil ||
|
||||
currentScreen is AppScreen.PferdVerwaltung ||
|
||||
currentScreen is AppScreen.Pferde ||
|
||||
currentScreen is AppScreen.PferdProfil ||
|
||||
currentScreen is AppScreen.VereinVerwaltung ||
|
||||
currentScreen is AppScreen.Vereine ||
|
||||
currentScreen is AppScreen.VereinProfil ||
|
||||
currentScreen is AppScreen.FunktionaerVerwaltung ||
|
||||
currentScreen is AppScreen.FunktionaerProfil ||
|
||||
currentScreen is AppScreen.StammdatenImport ||
|
||||
currentScreen is AppScreen.NennungsEingang ||
|
||||
currentScreen is AppScreen.ConnectivityCheck ||
|
||||
currentScreen is AppScreen.Dashboard ||
|
||||
currentScreen is AppScreen.Profile ||
|
||||
currentScreen is AppScreen.ProfileOnboarding
|
||||
|
||||
if (!authState.isAuthenticated && !isAllowedScreen) {
|
||||
LaunchedEffect(currentScreen) {
|
||||
if (!DeviceInitializationSettingsManager.isConfigured()) {
|
||||
println("[DesktopApp] Nicht authentifiziert & nicht konfiguriert -> Setup")
|
||||
nav.navigateToScreen(AppScreen.DeviceInitialization)
|
||||
} else {
|
||||
println("[DesktopApp] Nicht authentifiziert, aber konfiguriert -> Dashboard")
|
||||
nav.navigateToScreen(AppScreen.EventVerwaltung)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +98,7 @@ fun DesktopApp() {
|
|||
is AppScreen.Login -> LoginScreen(
|
||||
viewModel = loginViewModel,
|
||||
onLoginSuccess = {
|
||||
val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
|
||||
val returnTo = screen.returnTo ?: AppScreen.EventVerwaltung
|
||||
nav.navigateToScreen(returnTo)
|
||||
},
|
||||
onBack = { nav.navigateBack() },
|
||||
|
|
@ -84,7 +112,7 @@ fun DesktopApp() {
|
|||
onBack = { nav.navigateBack() },
|
||||
onLogout = {
|
||||
authTokenManager.clearToken()
|
||||
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung))
|
||||
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.EventVerwaltung))
|
||||
},
|
||||
isAuthenticated = authState.isAuthenticated
|
||||
)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ data class Reiter(
|
|||
var vereinsNummer: String? = null,
|
||||
var verein: String? = null,
|
||||
var nation: String = "AUT",
|
||||
var bundesland: String? = null,
|
||||
var istGastreiter: Boolean = false,
|
||||
)
|
||||
|
||||
|
|
@ -68,6 +69,8 @@ data class Funktionaer(
|
|||
var email: String? = null,
|
||||
var telefon: String? = null,
|
||||
var vereinsNummer: String? = null,
|
||||
var nation: String = "AUT",
|
||||
var bundesland: String? = null,
|
||||
var istAktiv: Boolean = true,
|
||||
)
|
||||
|
||||
|
|
@ -316,12 +319,14 @@ object Store {
|
|||
datumVon = "2026-05-20",
|
||||
datumBis = "2026-05-24",
|
||||
status = "In Vorbereitung",
|
||||
beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark."
|
||||
beschreibung = "Große Reitsport-Veranstaltung am Ebelsberger Schlosspark.",
|
||||
ort = "Linz-Ebelsberg"
|
||||
)
|
||||
)
|
||||
TurnierStore.add(
|
||||
linzId,
|
||||
Turnier(201, linzId, 26500, datumVon = "2026-05-20", datumBis = "2026-05-24", znsDataLoaded = true).apply {
|
||||
titel = "Linzer Pferdepage"
|
||||
kategorie.add("CSN-B*")
|
||||
})
|
||||
|
||||
|
|
@ -333,7 +338,8 @@ object Store {
|
|||
titel = "Herbst-Turnier 2025",
|
||||
datumVon = "2025-09-15",
|
||||
datumBis = "2025-09-17",
|
||||
status = "Abgeschlossen"
|
||||
status = "Abgeschlossen",
|
||||
ort = "Neumarkt/M."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ class DesktopMasterdataRepository : MasterdataRepository {
|
|||
satznummer = remote.satznummer,
|
||||
oepsNummer = remote.satznummer, // Oft identisch oder Mapping nötig
|
||||
lizenzKlasse = remote.lizenzKlasse,
|
||||
nation = "AUT" // Default für ZNS-Import
|
||||
nation = remote.nation ?: "AUT",
|
||||
bundesland = remote.bundesland
|
||||
)
|
||||
if (existingIdx >= 0) {
|
||||
Store.reiter[existingIdx] = entry
|
||||
|
|
@ -83,7 +84,9 @@ class DesktopMasterdataRepository : MasterdataRepository {
|
|||
id = id,
|
||||
vorname = namen.firstOrNull() ?: "",
|
||||
nachname = namen.drop(1).joinToString(" "),
|
||||
rollen = remote.qualifikationen
|
||||
rollen = remote.qualifikationen,
|
||||
nation = remote.nation ?: "AUT",
|
||||
bundesland = remote.bundesland
|
||||
)
|
||||
if (existingIdx >= 0) {
|
||||
Store.funktionaere[existingIdx] = entry
|
||||
|
|
|
|||
|
|
@ -45,13 +45,10 @@ fun DesktopMainLayout(
|
|||
}
|
||||
|
||||
// Automatische Umleitung zum DeviceInitialization, wenn Setup fehlt (außer wir sind bereits dort)
|
||||
LaunchedEffect(onboardingSettings) {
|
||||
LaunchedEffect(onboardingSettings.isConfigured, currentScreen) {
|
||||
if (!onboardingSettings.isConfigured && currentScreen !is AppScreen.DeviceInitialization) {
|
||||
println("[DesktopNav] Setup fehlt -> Umleitung zum DeviceInitialization")
|
||||
onNavigate(AppScreen.DeviceInitialization)
|
||||
} else if (onboardingSettings.isConfigured && currentScreen is AppScreen.DeviceInitialization) {
|
||||
println("[DesktopNav] Setup abgeschlossen -> Wechsel zum Dashboard")
|
||||
onNavigate(AppScreen.VeranstaltungVerwaltung)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,11 +27,15 @@ import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
|||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||
import at.mocode.frontend.features.ping.presentation.PingScreen
|
||||
import at.mocode.frontend.features.ping.presentation.PingViewModel
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingScreen
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileOnboardingViewModel
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
||||
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
|
||||
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
||||
import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen
|
||||
import at.mocode.frontend.features.turnier.presentation.TurnierWizard
|
||||
import at.mocode.frontend.features.turnier.presentation.TurnierWizardViewModel
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen
|
||||
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||
|
|
@ -41,7 +45,6 @@ import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
|
|||
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
|
||||
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
|
||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
|
||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard
|
||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
|
||||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||
|
|
@ -68,17 +71,18 @@ fun DesktopContentArea(
|
|||
val authTokenManager = org.koin.core.context.GlobalContext.get().get<AuthTokenManager>()
|
||||
authTokenManager.setToken(finalSettings.sharedKey)
|
||||
onSettingsChange(finalSettings)
|
||||
onNavigate(AppScreen.VeranstaltungVerwaltung)
|
||||
// nav.navigateToScreen(...) wird hier nicht direkt gerufen, sondern onNavigate
|
||||
onNavigate(AppScreen.EventVerwaltung)
|
||||
})
|
||||
}
|
||||
DeviceInitializationScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
// Haupt-Zentrale: Veranstaltung-Verwaltung
|
||||
is AppScreen.VeranstaltungVerwaltung -> {
|
||||
// Haupt-Zentrale: Event-Verwaltung
|
||||
is AppScreen.EventVerwaltung -> {
|
||||
VeranstaltungenScreen(
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstaltungNeu) },
|
||||
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
|
||||
onVeranstaltungNeu = { onNavigate(AppScreen.EventNeu()) },
|
||||
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.EventProfil(vId, eId)) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +93,15 @@ fun DesktopContentArea(
|
|||
)
|
||||
}
|
||||
|
||||
// --- Profile Onboarding ---
|
||||
is AppScreen.ProfileOnboarding -> {
|
||||
val viewModel = koinViewModel<ProfileOnboardingViewModel>()
|
||||
ProfileOnboardingScreen(
|
||||
viewModel = viewModel,
|
||||
onFinish = { onNavigate(AppScreen.EventVerwaltung) }
|
||||
)
|
||||
}
|
||||
|
||||
// --- Pferde-Verwaltung & Profil ---
|
||||
is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
|
||||
val viewModel = koinViewModel<PferdeViewModel>()
|
||||
|
|
@ -163,18 +176,30 @@ fun DesktopContentArea(
|
|||
is AppScreen.VeranstalterProfil -> VeranstalterDetail(
|
||||
veranstalterId = currentScreen.id,
|
||||
onBack = onBack,
|
||||
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) },
|
||||
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungNeu) },
|
||||
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.EventProfil(currentScreen.id, evtId)) },
|
||||
onNeuVeranstaltung = { onNavigate(AppScreen.EventKonfig(currentScreen.id)) },
|
||||
onEditVeranstalter = { id ->
|
||||
onNavigate(AppScreen.VeranstalterProfilEdit(id))
|
||||
}
|
||||
)
|
||||
|
||||
// Neuer Flow: Veranstalter auswählen → Veranstaltung-Wizard
|
||||
is AppScreen.VeranstalterProfilEdit -> VeranstalterAnlegenWizard(
|
||||
editId = currentScreen.id,
|
||||
onCancel = onBack,
|
||||
onVereinCreated = { id ->
|
||||
onNavigate(AppScreen.VeranstalterProfil(id))
|
||||
}
|
||||
)
|
||||
|
||||
// Neuer Flow: Veranstalter auswählen → Event-Wizard
|
||||
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
|
||||
onBack = onBack,
|
||||
onWeiter = { _ -> onNavigate(AppScreen.VeranstaltungNeu) },
|
||||
onWeiter = { _ -> onNavigate(AppScreen.EventNeu()) },
|
||||
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||
)
|
||||
|
||||
is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard(
|
||||
editId = null,
|
||||
onCancel = onBack,
|
||||
onVereinCreated = { newId: Long -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
||||
)
|
||||
|
|
@ -184,12 +209,13 @@ fun DesktopContentArea(
|
|||
VeranstalterDetail(
|
||||
veranstalterId = vId,
|
||||
onBack = onBack,
|
||||
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
|
||||
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
|
||||
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.EventProfil(vId, evtId)) },
|
||||
onNeuVeranstaltung = { onNavigate(AppScreen.EventKonfig(vId)) },
|
||||
onEditVeranstalter = { id -> onNavigate(AppScreen.VeranstalterProfilEdit(id)) }
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungKonfig -> {
|
||||
is AppScreen.EventKonfig -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
VeranstaltungKonfigScreen(
|
||||
veranstalterId = vId,
|
||||
|
|
@ -199,12 +225,12 @@ fun DesktopContentArea(
|
|||
// val allEvents = Store.allEvents()
|
||||
// val newId = (allEvents.maxOfOrNull { it.id } ?: 0L) + 1L
|
||||
// ...
|
||||
onNavigate(AppScreen.VeranstaltungProfil(vId, 0L)) // Mock
|
||||
onNavigate(AppScreen.EventProfil(vId, 0L)) // Mock
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungProfil -> {
|
||||
is AppScreen.EventProfil -> {
|
||||
VeranstaltungProfilScreen(
|
||||
veranstalterId = currentScreen.veranstalterId,
|
||||
veranstaltungId = currentScreen.veranstaltungId,
|
||||
|
|
@ -221,7 +247,7 @@ fun DesktopContentArea(
|
|||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungDetail -> {
|
||||
is AppScreen.EventDetail -> {
|
||||
val repository: at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository = koinInject()
|
||||
VeranstaltungDetailScreen(
|
||||
veranstaltungId = currentScreen.id,
|
||||
|
|
@ -233,12 +259,13 @@ fun DesktopContentArea(
|
|||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungNeu -> {
|
||||
val viewModel: at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardViewModel = koinViewModel()
|
||||
at.mocode.veranstaltung.feature.presentation.VeranstaltungWizardScreen(
|
||||
is AppScreen.EventNeu -> {
|
||||
val viewModel: at.mocode.veranstaltung.feature.presentation.EventWizardViewModel = koinViewModel()
|
||||
at.mocode.veranstaltung.feature.presentation.EventWizardScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = onBack,
|
||||
onFinish = { onBack() }
|
||||
onFinish = { onBack() },
|
||||
onNavigateToVeranstalterNeu = { onNavigate(AppScreen.VeranstalterNeu) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -281,22 +308,13 @@ fun DesktopContentArea(
|
|||
|
||||
is AppScreen.TurnierNeu -> {
|
||||
val evtId = currentScreen.veranstaltungId
|
||||
val parent = at.mocode.frontend.shell.desktop.data.Store.vereine.firstOrNull { v ->
|
||||
at.mocode.frontend.shell.desktop.data.Store.eventsFor(v.id).any { it.id == evtId }
|
||||
}
|
||||
if (parent == null) {
|
||||
InvalidContextNotice(
|
||||
message = "Veranstaltung (ID=$evtId) nicht gefunden.",
|
||||
onBack = onBack
|
||||
)
|
||||
} else {
|
||||
TurnierWizard(
|
||||
veranstalterId = parent.id,
|
||||
veranstaltungId = evtId,
|
||||
onBack = onBack,
|
||||
onSaved = { _ -> onBack() }
|
||||
)
|
||||
}
|
||||
val viewModel = koinViewModel<TurnierWizardViewModel>()
|
||||
TurnierWizard(
|
||||
viewModel = viewModel,
|
||||
veranstaltungId = evtId,
|
||||
onBack = onBack,
|
||||
onFinish = { onBack() }
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Billing -> {
|
||||
|
|
@ -329,10 +347,13 @@ fun DesktopContentArea(
|
|||
ProfileScreen(viewModel = viewModel)
|
||||
}
|
||||
|
||||
is AppScreen.Home, is AppScreen.Dashboard -> {
|
||||
|
||||
is AppScreen.Home, is AppScreen.Dashboard, is AppScreen.PortalDashboard,
|
||||
is AppScreen.Meisterschaften, is AppScreen.Cups,
|
||||
is AppScreen.CreateTournament, is AppScreen.OrganizerProfile -> {
|
||||
AdminUebersichtScreen(
|
||||
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) }
|
||||
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.EventDetail(id)) }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,21 +35,13 @@ fun DesktopNavRail(
|
|||
icon = Icons.Default.Adjust,
|
||||
label = "Logo",
|
||||
selected = false,
|
||||
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||
onClick = { onNavigate(AppScreen.EventVerwaltung) },
|
||||
enabled = isConfigured
|
||||
)
|
||||
|
||||
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) },
|
||||
enabled = isConfigured
|
||||
)
|
||||
|
||||
NavRailItem(
|
||||
icon = Icons.Default.CloudDownload,
|
||||
label = "ZNS-Import",
|
||||
|
|
@ -101,7 +93,7 @@ fun DesktopNavRail(
|
|||
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Richter") },
|
||||
text = { Text("Funktionäre") },
|
||||
onClick = {
|
||||
showStammdatenMenu = false
|
||||
onNavigate(AppScreen.FunktionaerVerwaltung)
|
||||
|
|
@ -111,6 +103,43 @@ fun DesktopNavRail(
|
|||
}
|
||||
}
|
||||
|
||||
var showVerwaltungMenu by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
NavRailItem(
|
||||
icon = Icons.Default.Dashboard,
|
||||
label = "Verwaltungen",
|
||||
selected = currentScreen is AppScreen.EventVerwaltung ||
|
||||
currentScreen is AppScreen.EventDetail ||
|
||||
currentScreen is AppScreen.VeranstalterVerwaltung ||
|
||||
currentScreen is AppScreen.VeranstalterAuswahl,
|
||||
onClick = { showVerwaltungMenu = true },
|
||||
enabled = isConfigured
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showVerwaltungMenu && isConfigured,
|
||||
onDismissRequest = { showVerwaltungMenu = false },
|
||||
offset = DpOffset(Dimens.NavRailWidth, 0.dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Veranstalter") },
|
||||
onClick = {
|
||||
showVerwaltungMenu = false
|
||||
onNavigate(AppScreen.VeranstalterVerwaltung)
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.Business, contentDescription = null) }
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Events") },
|
||||
onClick = {
|
||||
showVerwaltungMenu = false
|
||||
onNavigate(AppScreen.EventVerwaltung)
|
||||
},
|
||||
leadingIcon = { Icon(Icons.Default.Event, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
NavRailItem(
|
||||
icon = Icons.Default.Email,
|
||||
label = "Mails",
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fun DesktopTopHeader(
|
|||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Zurück-Button ausblenden auf Startseite oder im Setup
|
||||
if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.VeranstaltungVerwaltung) {
|
||||
if (currentScreen !is AppScreen.DeviceInitialization && currentScreen !is AppScreen.EventVerwaltung) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
// Verhindere Rücksprung zum Setup, wenn konfiguriert
|
||||
|
|
@ -65,7 +65,7 @@ fun DesktopTopHeader(
|
|||
|
||||
// Home Icon als Anker
|
||||
IconButton(
|
||||
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||
onClick = { onNavigate(AppScreen.EventVerwaltung) },
|
||||
modifier = Modifier.size(Dimens.IconSizeM),
|
||||
enabled = isConfigured
|
||||
) {
|
||||
|
|
@ -207,7 +207,7 @@ private fun BreadcrumbContent(
|
|||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungProfil -> {
|
||||
is AppScreen.EventProfil -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstalter-Verwaltung",
|
||||
|
|
@ -224,43 +224,43 @@ private fun BreadcrumbContent(
|
|||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
text = "Event #${currentScreen.veranstaltungId}",
|
||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungVerwaltung -> {
|
||||
is AppScreen.EventVerwaltung -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltungs-Verwaltung",
|
||||
text = "Event-Verwaltung",
|
||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungDetail -> {
|
||||
is AppScreen.EventDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltungs-Verwaltung",
|
||||
text = "Event-Verwaltung",
|
||||
style = textStyle.copy(color = clickableColor),
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.EventVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.id}",
|
||||
text = "Event #${currentScreen.id}",
|
||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungNeu -> {
|
||||
is AppScreen.EventNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltungs-Verwaltung",
|
||||
text = "Event-Verwaltung",
|
||||
style = textStyle.copy(color = clickableColor),
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||
modifier = Modifier.clickable { onNavigate(AppScreen.EventVerwaltung) },
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Neue Veranstaltung",
|
||||
text = "Neues Event",
|
||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||
)
|
||||
}
|
||||
|
|
@ -268,10 +268,10 @@ private fun BreadcrumbContent(
|
|||
is AppScreen.TurnierDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
text = "Event #${currentScreen.veranstaltungId}",
|
||||
style = textStyle.copy(color = clickableColor),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
|
|
@ -284,10 +284,10 @@ private fun BreadcrumbContent(
|
|||
is AppScreen.TurnierNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
text = "Event #${currentScreen.veranstaltungId}",
|
||||
style = textStyle.copy(color = clickableColor),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
|
|
@ -300,10 +300,10 @@ private fun BreadcrumbContent(
|
|||
is AppScreen.Billing -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||
text = "Event #${currentScreen.veranstaltungId}",
|
||||
style = textStyle.copy(color = clickableColor),
|
||||
modifier = Modifier.clickable {
|
||||
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||
onNavigate(AppScreen.EventDetail(currentScreen.veranstaltungId))
|
||||
},
|
||||
)
|
||||
BreadcrumbSeparator()
|
||||
|
|
@ -356,7 +356,7 @@ private fun BreadcrumbContent(
|
|||
is AppScreen.FunktionaerVerwaltung -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
text = "Richter-Verwaltung",
|
||||
text = "Funktionär-Verwaltung",
|
||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
package at.mocode.frontend.shell.desktop.screens.management
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import at.mocode.frontend.core.designsystem.components.MsCard
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import at.mocode.frontend.shell.desktop.data.Store
|
||||
|
||||
@Composable
|
||||
|
|
@ -254,28 +257,131 @@ fun FunktionaerVerwaltungScreen(onBack: () -> Unit, onEdit: (Long) -> Unit) {
|
|||
|
||||
@Composable
|
||||
fun VeranstalterVerwaltungScreen(onBack: () -> Unit, onNew: () -> Unit, onEdit: (Long) -> Unit) {
|
||||
// Veranstalter sind in unserem System eigentlich Vereine, die Veranstaltungen ausrichten,
|
||||
// wir nutzen hier die 'vereine' Liste aus dem Store.
|
||||
val vereine = Store.vereine
|
||||
var filter by remember { mutableStateOf("") }
|
||||
val filteredItems = if (filter.isEmpty()) vereine else vereine.filter {
|
||||
it.name.contains(filter, ignoreCase = true) || it.oepsNummer.contains(filter, ignoreCase = true)
|
||||
}
|
||||
|
||||
ManagementTableScreen(
|
||||
title = "Veranstalter-Verwaltung",
|
||||
items = filteredItems,
|
||||
columns = listOf(
|
||||
TableColumn("Name", { it.name }, weight = 2f),
|
||||
TableColumn("ÖPS-Nr.", { it.oepsNummer }, width = 100.dp),
|
||||
TableColumn("Ort", { it.ort ?: "-" }, weight = 1f),
|
||||
TableColumn("BL", { it.bundesland ?: "-" }, width = 60.dp),
|
||||
TableColumn("Email", { it.email ?: "-" }, weight = 1f)
|
||||
),
|
||||
onBack = onBack,
|
||||
onNew = onNew,
|
||||
onEdit = { onEdit(it.id) },
|
||||
onDelete = { },
|
||||
onSearch = { filter = it }
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxSize().padding(Dimens.SpacingL)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Veranstalter - verwalten",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
MsButton(
|
||||
text = "Neuer Veranstalter",
|
||||
onClick = onNew
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingL))
|
||||
|
||||
OutlinedTextField(
|
||||
value = filter,
|
||||
onValueChange = { filter = it },
|
||||
placeholder = { Text("Suche nach Name oder ÖPS-Nr...", style = MaterialTheme.typography.bodyMedium) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(20.dp)) },
|
||||
trailingIcon = {
|
||||
if (filter.isNotEmpty()) {
|
||||
IconButton(onClick = { filter = "" }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = "Löschen")
|
||||
}
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
textStyle = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingL))
|
||||
|
||||
if (filteredItems.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Keine Veranstalter gefunden.", color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM),
|
||||
contentPadding = PaddingValues(bottom = Dimens.SpacingL)
|
||||
) {
|
||||
items(filteredItems) { veranstalter ->
|
||||
VeranstalterCard(
|
||||
name = veranstalter.name,
|
||||
oepsNr = veranstalter.oepsNummer,
|
||||
ort = veranstalter.ort ?: "-",
|
||||
bundesland = veranstalter.bundesland ?: "-",
|
||||
onClick = { onEdit(veranstalter.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VeranstalterCard(
|
||||
name: String,
|
||||
oepsNr: String,
|
||||
ort: String,
|
||||
bundesland: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
MsCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onClick
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(Dimens.SpacingM),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Business,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(Dimens.SpacingM))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||
) {
|
||||
Text(
|
||||
text = "ÖPS: $oepsNr",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text("•", color = Color.Gray)
|
||||
Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.size(14.dp), tint = Color.Gray)
|
||||
Text("$ort ($bundesland)", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = Color.LightGray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ fun VeranstalterDetail(
|
|||
onBack: () -> Unit,
|
||||
onZurVeranstaltung: (Long) -> Unit,
|
||||
onNeuVeranstaltung: () -> Unit,
|
||||
onEditVeranstalter: (Long) -> Unit,
|
||||
) {
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = veranstalterId,
|
||||
viewModel = koinInject(),
|
||||
onZurueck = onBack,
|
||||
onVeranstaltungOeffnen = onZurVeranstaltung,
|
||||
onVeranstaltungNeu = onNeuVeranstaltung
|
||||
onVeranstaltungNeu = onNeuVeranstaltung,
|
||||
onEditVeranstalter = onEditVeranstalter
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ fun PreviewVeranstalterDetailScreen() {
|
|||
onZurueck = {},
|
||||
onVeranstaltungOeffnen = {},
|
||||
onVeranstaltungNeu = {},
|
||||
onEditVeranstalter = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -162,6 +163,7 @@ fun PreviewTurnierDetailScreen() {
|
|||
override suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>> = Result.success(emptyList())
|
||||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||
override suspend fun getStats(): Result<MasterdataStats> = Result.success(MasterdataStats(null, 0, 0, 0, 0))
|
||||
}
|
||||
val nennungVm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
val stammdatenVm = TurnierStammdatenViewModel(mockTurnierRepo)
|
||||
|
|
@ -219,6 +221,7 @@ fun PreviewTurnierOrganisationTab() {
|
|||
override suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>> = Result.success(emptyList())
|
||||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||
override suspend fun getStats(): Result<MasterdataStats> = Result.success(MasterdataStats(null, 0, 0, 0, 0))
|
||||
}
|
||||
val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
MaterialTheme {
|
||||
|
|
@ -306,6 +309,7 @@ fun PreviewTurnierNennungenTab() {
|
|||
override suspend fun searchFunktionaere(query: String): Result<List<Funktionaer>> = Result.success(emptyList())
|
||||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||
override suspend fun getStats(): Result<MasterdataStats> = Result.success(MasterdataStats(null, 0, 0, 0, 0))
|
||||
}
|
||||
val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
MaterialTheme {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Event
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -33,7 +36,7 @@ fun VeranstaltungProfilScreen(
|
|||
val turniere = TurnierStore.list(veranstaltungId)
|
||||
|
||||
if (veranstaltung == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Veranstaltung nicht gefunden") }
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Event nicht gefunden") }
|
||||
return@DesktopTheme
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +68,7 @@ fun VeranstaltungProfilScreen(
|
|||
KpiCard("Ort", veranstaltung.ort, Icons.Default.Place, Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Text("Turniere in dieser Veranstaltung", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text("Turniere in diesem Event", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
if (turniere.isEmpty()) {
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Box(Modifier.padding(32.dp).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
|
|
@ -81,7 +84,7 @@ fun VeranstaltungProfilScreen(
|
|||
}
|
||||
}
|
||||
|
||||
// Rechte Spalte: Veranstalter Info & Aktionen
|
||||
// Rechte Spalte: Veranstalter Information & Aktionen
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Card {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
|
|
|
|||
|
|
@ -1,40 +1,78 @@
|
|||
package at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.shell.desktop.data.Store
|
||||
import at.mocode.frontend.core.designsystem.components.ButtonSize
|
||||
import at.mocode.frontend.core.designsystem.components.ButtonVariant
|
||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||
import at.mocode.frontend.core.designsystem.components.MsStatusBadge
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardIntent
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterWizardViewModel
|
||||
import at.mocode.frontend.shell.desktop.data.Store
|
||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.components.pickZnsFile
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.skia.Image
|
||||
import org.koin.compose.koinInject
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VeranstalterAnlegenWizard(
|
||||
editId: Long? = null,
|
||||
onCancel: () -> Unit,
|
||||
onVereinCreated: (Long) -> Unit
|
||||
) {
|
||||
var step by remember { mutableStateOf(1) }
|
||||
var selectedVereinId by remember { mutableLongStateOf(0L) }
|
||||
val viewModel = koinViewModel<VeranstalterWizardViewModel>()
|
||||
val state by viewModel.state.collectAsState()
|
||||
var step by remember { mutableIntStateOf(1) }
|
||||
|
||||
LaunchedEffect(editId) {
|
||||
if (editId != null) {
|
||||
viewModel.send(VeranstalterWizardIntent.Load(editId))
|
||||
step = 2 // Direkt zu den Details beim Editieren
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.success) {
|
||||
if (state.success) {
|
||||
onVereinCreated(state.editId ?: 0L)
|
||||
}
|
||||
}
|
||||
|
||||
val znsImporter = koinInject<ZnsImportProvider>()
|
||||
val znsState = znsImporter.state
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(24.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onCancel) { Icon(Icons.Default.Close, null) }
|
||||
Text("Veranstalter registrieren", style = MaterialTheme.typography.headlineSmall)
|
||||
Text(
|
||||
if (editId == null) "Veranstalter registrieren" else "Veranstalter-Profil bearbeiten",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
|
|
@ -47,14 +85,21 @@ fun VeranstalterAnlegenWizard(
|
|||
1 -> Step1Veranstalter(
|
||||
znsState = znsState,
|
||||
znsImporter = znsImporter,
|
||||
selectedVereinId = selectedVereinId,
|
||||
onVereinSelected = { selectedVereinId = it },
|
||||
selectedVereinId = state.editId ?: 0L,
|
||||
onVereinSelected = { id ->
|
||||
// Mock: Wir laden die Daten des Vereins aus dem Store in das VM
|
||||
Store.vereine.find { it.id == id }?.let { v ->
|
||||
viewModel.send(VeranstalterWizardIntent.UpdateName(v.name))
|
||||
viewModel.send(VeranstalterWizardIntent.UpdateOeps(v.oepsNummer))
|
||||
viewModel.send(VeranstalterWizardIntent.UpdateOrt(v.ort ?: ""))
|
||||
}
|
||||
},
|
||||
onVeranstalterCreated = {
|
||||
selectedVereinId = it
|
||||
onVereinCreated(it)
|
||||
// Nicht mehr direkt navigieren, sondern zu Step 2
|
||||
step = 2
|
||||
}
|
||||
)
|
||||
2 -> { /* Optional: Weitere Details für den Veranstalter */ }
|
||||
2 -> Step2VeranstalterDetails(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,16 +108,248 @@ fun VeranstalterAnlegenWizard(
|
|||
Spacer(Modifier.width(8.dp))
|
||||
if (step == 1) {
|
||||
Button(
|
||||
onClick = { onVereinCreated(selectedVereinId) },
|
||||
enabled = selectedVereinId != 0L
|
||||
onClick = { step = 2 },
|
||||
enabled = state.name.isNotBlank()
|
||||
) {
|
||||
Text("Fertigstellen")
|
||||
Text("Weiter zu den Details")
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = { viewModel.send(VeranstalterWizardIntent.Save) },
|
||||
enabled = !state.isSaving && state.name.isNotBlank()
|
||||
) {
|
||||
if (state.isSaving) {
|
||||
CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp, color = Color.White)
|
||||
} else {
|
||||
Text(if (editId == null) "Registrierung abschließen" else "Änderungen speichern")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VeranstalterCardPreview(
|
||||
name: String,
|
||||
ort: String,
|
||||
oepsNummer: String,
|
||||
ansprechpartner: String,
|
||||
email: String,
|
||||
logoBase64: String?,
|
||||
status: String
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f))
|
||||
.border(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (!logoBase64.isNullOrBlank()) {
|
||||
val bitmap = remember(logoBase64) { decodeBase64ToImage(logoBase64) }
|
||||
if (bitmap != null) {
|
||||
Image(
|
||||
bitmap = bitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize().clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Icon(Icons.Default.Image, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
} else {
|
||||
Icon(Icons.Default.Business, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = name.ifBlank { "Veranstalter Name" },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
MsStatusBadge(
|
||||
text = status,
|
||||
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "OEBS-Nr: $oepsNummer | Ort: $ort",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray
|
||||
)
|
||||
|
||||
if (ansprechpartner.isNotBlank() || email.isNotBlank()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (ansprechpartner.isNotBlank()) {
|
||||
Text("👤 $ansprechpartner", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (email.isNotBlank()) {
|
||||
Text("✉️ $email", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun decodeBase64ToImage(base64: String): ImageBitmap? {
|
||||
return try {
|
||||
val bytes = Base64.decode(base64)
|
||||
Image.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Step2VeranstalterDetails(viewModel: VeranstalterWizardViewModel) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Ergänzen Sie die Kontaktdaten für diesen Veranstalter.", style = MaterialTheme.typography.bodyMedium)
|
||||
|
||||
// --- VORSCHAU ---
|
||||
VeranstalterCardPreview(
|
||||
name = state.name,
|
||||
ort = state.ort,
|
||||
oepsNummer = state.oepsNummer,
|
||||
ansprechpartner = state.ansprechpartner,
|
||||
email = state.email,
|
||||
logoBase64 = state.logoBase64,
|
||||
status = state.loginStatus
|
||||
)
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
OutlinedTextField(
|
||||
value = state.name,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateName(it)) },
|
||||
label = { Text("Name des Veranstalters / Vereins") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = state.oepsNummer,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOeps(it)) },
|
||||
label = { Text("OEBS-Nr") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = state.ort,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateOrt(it)) },
|
||||
label = { Text("Ort") },
|
||||
modifier = Modifier.weight(2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGO UPLOAD ---
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(140.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
if (state.logoBase64 != null) {
|
||||
val logoData = state.logoBase64
|
||||
val bitmap = remember(logoData) { logoData?.let { decodeBase64ToImage(it) } }
|
||||
if (bitmap != null) {
|
||||
Image(
|
||||
bitmap = bitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp).clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(Icons.Default.Image, null, modifier = Modifier.size(40.dp), tint = Color.Gray)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
MsButton(
|
||||
text = "Logo wählen",
|
||||
onClick = {
|
||||
scope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
val fileDialog = FileDialog(null as Frame?, "Logo auswählen", FileDialog.LOAD)
|
||||
fileDialog.isVisible = true
|
||||
if (fileDialog.directory != null && fileDialog.file != null) {
|
||||
val file = File(fileDialog.directory, fileDialog.file)
|
||||
val bytes = file.readBytes()
|
||||
val base64 = Base64.encode(bytes)
|
||||
viewModel.send(VeranstalterWizardIntent.UpdateLogo(base64))
|
||||
}
|
||||
}
|
||||
},
|
||||
variant = ButtonVariant.TEXT,
|
||||
size = ButtonSize.SMALL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
Text("Ansprechpartner & Kontakt", style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.ansprechpartner,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateAnsprechpartner(it)) },
|
||||
label = { Text("Name Ansprechpartner") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = state.email,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateEmail(it)) },
|
||||
label = { Text("E-Mail Adresse") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = state.telefon,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateTelefon(it)) },
|
||||
label = { Text("Telefonnummer") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = state.adresse,
|
||||
onValueChange = { viewModel.send(VeranstalterWizardIntent.UpdateAdresse(it)) },
|
||||
label = { Text("Postanschrift") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Step1Veranstalter(
|
||||
znsState: ZnsImportState,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user