Compare commits
5 Commits
5887ac7b6c
...
edfe05cbe3
| Author | SHA1 | Date | |
|---|---|---|---|
| edfe05cbe3 | |||
| 6feb139a46 | |||
| b94e0f2d9d | |||
| 8806d11e3c | |||
| a1bf93342e |
41
docs/04_Agents/Journal/2026-04-20_Session-Log_Onboarding.md
Normal file
41
docs/04_Agents/Journal/2026-04-20_Session-Log_Onboarding.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
type: Journal
|
||||||
|
status: FINAL
|
||||||
|
owner: Curator
|
||||||
|
date: 2026-04-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session Log – Finalisierung Start-Sequenz & Layout (Phase 13)
|
||||||
|
|
||||||
|
## 🏗️ Status-Update
|
||||||
|
Die Nachmittags-Session am 20. April 2026 wurde erfolgreich abgeschlossen. Die gesamte Start-Sequenz, die Infrastruktur-Integration und das globale Layout wurden nach dem **ADR-0024 Plug-and-Play Pattern** finalisiert.
|
||||||
|
|
||||||
|
## 🛠️ Umfang & Änderungen
|
||||||
|
|
||||||
|
### 1. Onboarding & Geräte-Initialisierung
|
||||||
|
- **Sidebar-Blocking:** Fachliche Module sind deaktiviert, bis das Gerät initialisiert ist.
|
||||||
|
- **Client-Datensicherheit:** Der `backupPath` ist für alle Rollen verpflichtend (Lokale Datensouveränität).
|
||||||
|
- **Navigations-Fix:** Login-Sackgasse behoben (`navigateBack` implementiert).
|
||||||
|
- **Setup-Workflow:** Nahtloser Übergang zur Veranstaltungs-Verwaltung nach Abschluss ohne Neustart.
|
||||||
|
|
||||||
|
### 2. Infrastruktur & Sicherheit
|
||||||
|
- **Lifecycle-Awareness:** `ConnectivityTracker` und `DiscoveryService` starten reaktiv erst nach erfolgreicher Initialisierung.
|
||||||
|
- **Plug-and-Play:** Dienste bleiben inaktiv, solange kein Gerätename/Key vorliegt (Ressourcenschonung).
|
||||||
|
|
||||||
|
### 3. Globale Navigation & Layout (Vision_03)
|
||||||
|
- **Top-Bar:** Integration eines pulsierenden WebSocket-Sync-Indikators (Echtzeit-Peer-Count).
|
||||||
|
- **Breadcrumbs:** Konsistentes MD3-Styling mit Home-Anker und navigierbaren Pfaden für alle Bounded Contexts.
|
||||||
|
- **Navigation-Rail:** Optimierung auf MD3-Standards (Ripple-Effekt, Surface-Indikatoren, Tooltips).
|
||||||
|
- **Footer:** Umstellung auf High-Density Layout (28dp Höhe, optimierte Schriftgrößen) für maximale Informationsdichte.
|
||||||
|
- **Refactoring:** `DesktopMainLayout.kt` von 1274 Zeilen auf ca. 95 Zeilen reduziert.
|
||||||
|
- **Modul-Aufsplittung:** Extraktion der Sub-Komponenten in `NavRail.kt`, `TopHeader.kt`, `ContentArea.kt` und `FooterBar.kt` im Paket `at.mocode.frontend.shell.desktop.screens.layout.components`.
|
||||||
|
- **API-Bereinigung:** Beseitigung ungenutzter Properties (z.B. `TopBarTextColor`) und Korrektur veralteter API-Signaturen in den Screen-Injektionen.
|
||||||
|
- **Fehlerbehebung:** Beseitigung von Kompilerfehlern in `NavRail.kt` (Tooltip-Positionierung) und Bereinigung ungenutzter Parameter in `ContentArea.kt`.
|
||||||
|
|
||||||
|
## 📐 Architektur-Check (ADR-0024)
|
||||||
|
- **Self-Contained:** Feature-Module verwalten ihren State; Shell reagiert auf Events.
|
||||||
|
- **Reaktivität:** UI reagiert sofort auf Konfigurationsänderungen (`settings.json`).
|
||||||
|
- **Offline-First:** Visuelles Feedback über lokale DB, LAN-Peers und Cloud-Sync ist jederzeit präsent.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokumentation erstellt durch den Curator im Rahmen des "Meldestelle"-Protokolls.*
|
||||||
44
docs/99_Journal/2026-04-19_Ping_Service_Fixes.md
Normal file
44
docs/99_Journal/2026-04-19_Ping_Service_Fixes.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Journal: Behebung von 500er Fehlern im Ping-Service & Security-Fixes
|
||||||
|
|
||||||
|
**Datum:** 19. April 2026
|
||||||
|
**Status:** Abgeschlossen
|
||||||
|
**Agent:** 🏗️ [Lead Architect] | 🧐 [QA Specialist] | 🧹 [Curator]
|
||||||
|
|
||||||
|
## 🎯 Zielsetzung
|
||||||
|
Nach der gestrigen Umstrukturierung traten beim `ping-service` HTTP 500 Fehler bei autorisierten API-Aufrufen auf. Ziel war die Identifikation der Ursachen (Security, Parameter-Mapping, Resilience) und deren Behebung sowie die Aktualisierung der Testsuite.
|
||||||
|
|
||||||
|
## 🛠️ Durchgeführte Änderungen
|
||||||
|
|
||||||
|
### 1. Security-Mapping & Rollen-Korrektur
|
||||||
|
* Im `PingController` wurden die `@PreAuthorize`-Annotationen korrigiert. Da der `KeycloakRoleConverter` das Präfix `ROLE_` hartkodiert hinzufügt und die Rollen in Großbuchstaben umwandelt, wurden die Prüfungen von `MELD_USER` auf `ROLE_MELD_USER` (bzw. `ROLE_MELD_ADMIN`) angepasst.
|
||||||
|
* Der Import der `AccessDeniedException` im `PingExceptionHandler` wurde auf die korrekte Spring Security Klasse fixiert, um 403-Fehler sauber zu fangen und nicht in 500er zu verwandeln.
|
||||||
|
|
||||||
|
### 2. API-Konsistenz & Parameter-Mapping
|
||||||
|
* Der Query-Parameter für `/api/ping/sync` wurde explizit auf `lastSyncTimestamp` gemappt, um mit dem Postman-Aufruf und den Anforderungen des Frontends konsistent zu sein.
|
||||||
|
|
||||||
|
### 3. Resilience4j & Coroutines
|
||||||
|
* Die Bibliothek `resilience4j-kotlin` wurde in die `libs.versions.toml` aufgenommen und im `ping-service` eingebunden. Dies stellt sicher, dass der `@CircuitBreaker` korrekt mit Kotlin `suspend` Funktionen zusammenarbeitet und Exceptions nicht unkontrolliert durchschlagen.
|
||||||
|
|
||||||
|
### 4. Test-Aktualisierung
|
||||||
|
* `PingControllerTest.kt` wurde angepasst, um den neuen Parameter-Namen `lastSyncTimestamp` zu verwenden.
|
||||||
|
* Alle 5 Tests im `PingControllerTest` verlaufen nun erfolgreich.
|
||||||
|
|
||||||
|
## ✅ Verifizierung
|
||||||
|
* `./gradlew :backend:services:ping:ping-service:test`: **ERFOLGREICH** (5/5 Tests passed)
|
||||||
|
* Manueller Check der Parameter-Namen gegen Postman-Anforderungen: **ERFOLGREICH**
|
||||||
|
* Verifizierung des Rollen-Mappings im `KeycloakRoleConverter` gegen Controller-Annotationen: **KONSISTENT**
|
||||||
|
|
||||||
|
## 🧹 Fazit
|
||||||
|
Die "letzte Meile" der Service-Kommunikation ist nun stabil. Durch das verbesserte Exception-Handling und die korrekte Resilience-Integration liefert der Service nun aussagekräftige HTTP-Statuscodes statt generischer 500er Fehler.
|
||||||
|
|
||||||
|
### Nachtrag 20:30
|
||||||
|
* **Networking-Fix:** `GlobalSecurityConfig` angepasst, um `jwk-set-uri` primär aus Spring-Properties oder Environment-Variables zu lesen. Default auf `localhost:8180` für IDE-Betrieb korrigiert, um `UnknownHostException: keycloak` zu vermeiden.
|
||||||
|
* **Exception-Handling:** `PingExceptionHandler` um generischen `Exception`-Handler erweitert, um auch Security-Initialisierungsfehler (wie JWT-Decoder-Probleme) sauber abzufangen und zu loggen.
|
||||||
|
|
||||||
|
### Nachtrag 21:25
|
||||||
|
* **Re-Fix Circuit Breaker Fallback:** Nachdem ein fehlerhafter Zwischenversuch (möglicherweise durch einen anderen Agenten) die `suspend`-Markierung wieder eingeführt hatte, wurde diese nun final entfernt. Die Signatur `fallbackPing(simulate: Boolean, ex: Throwable)` ohne `suspend` ist die einzig stabile Variante für Resilience4j in Kombination mit Spring Boot 3 AOP-Proxies und Kotlin Coroutines. Tests bestätigen die strukturelle Korrektheit.
|
||||||
|
|
||||||
|
### Nachtrag 21:45
|
||||||
|
* **Stochastic Simulation:** Die Zufallskomponente (`Random.nextDouble() < 0.6`) wurde in der `enhancedPing`-Methode wieder eingeführt.
|
||||||
|
* **Logik:** Wenn `simulate=true` übergeben wird, tritt der Fehler nun nur noch in ca. 60% der Fälle auf, was ein realistisches "intermittierendes" Fehlerszenario für den Circuit Breaker Test darstellt. In den restlichen 40% wird die Anfrage trotz Simulationsmodus erfolgreich verarbeitet.
|
||||||
|
* **Logging:** Zusätzliches Log-Statement für den "Lucky Pass" Fall hinzugefügt, um die Simulationstransparenz in der Konsole zu wahren.
|
||||||
|
|
@ -21,11 +21,8 @@ class ConnectivityTracker : KoinComponent {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
|
||||||
init {
|
fun startTracking() {
|
||||||
startTracking()
|
if (scope.isActive && _isOnline.value) return // Bereits aktiv (Dummy-Check)
|
||||||
}
|
|
||||||
|
|
||||||
private fun startTracking() {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
_isOnline.value = checkConnection()
|
_isOnline.value = checkConnection()
|
||||||
|
|
|
||||||
|
|
@ -72,33 +72,26 @@ actual fun DeviceInitializationConfig(
|
||||||
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
||||||
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
imeAction = if (settings.networkRole == NetworkRole.MASTER) ImeAction.Next else ImeAction.Done,
|
imeAction = ImeAction.Next,
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onNext = { focusManager.moveFocus(FocusDirection.Next) },
|
onNext = { focusManager.moveFocus(FocusDirection.Next) }
|
||||||
onDone = {
|
|
||||||
if (DeviceInitializationValidator.canContinue(settings)) {
|
|
||||||
viewModel.completeInitialization()
|
|
||||||
} else {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
modifier = Modifier.focusRequester(sharedKeyFocus),
|
modifier = Modifier.focusRequester(sharedKeyFocus),
|
||||||
trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||||
onTrailingIconClick = { passwordVisible = !passwordVisible }
|
onTrailingIconClick = { passwordVisible = !passwordVisible }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (settings.networkRole == NetworkRole.MASTER) {
|
MsFilePicker(
|
||||||
MsFilePicker(
|
label = "Backup-Verzeichnis (Pfad)",
|
||||||
label = "Backup-Verzeichnis (Pfad)",
|
selectedPath = settings.backupPath,
|
||||||
selectedPath = settings.backupPath,
|
onFileSelected = { selectedPath ->
|
||||||
onFileSelected = { selectedPath ->
|
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
|
||||||
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
|
},
|
||||||
},
|
directoryOnly = true,
|
||||||
directoryOnly = true,
|
modifier = Modifier.focusRequester(backupPathFocus)
|
||||||
modifier = Modifier.focusRequester(backupPathFocus)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
if (settings.networkRole == NetworkRole.MASTER) {
|
||||||
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
||||||
Slider(
|
Slider(
|
||||||
value = settings.syncInterval.toFloat(),
|
value = settings.syncInterval.toFloat(),
|
||||||
|
|
@ -106,7 +99,19 @@ actual fun DeviceInitializationConfig(
|
||||||
valueRange = 1f..60f,
|
valueRange = 1f..60f,
|
||||||
steps = 59
|
steps = 59
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Button zum Abschließen für Clients, da diese keinen Slider/Clients haben
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.completeInitialization() },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = DeviceInitializationValidator.canContinue(settings)
|
||||||
|
) {
|
||||||
|
Text("Konfiguration abschließen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.networkRole == NetworkRole.MASTER) {
|
||||||
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)
|
||||||
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,14 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import at.mocode.frontend.shell.desktop.navigation.DesktopNavigationPort
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.layout.DesktopMainLayout
|
|
||||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||||
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
import at.mocode.frontend.core.auth.presentation.LoginScreen
|
||||||
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
import at.mocode.frontend.core.auth.presentation.LoginViewModel
|
||||||
import at.mocode.frontend.core.designsystem.theme.AppTheme
|
import at.mocode.frontend.core.designsystem.theme.AppTheme
|
||||||
import at.mocode.frontend.core.navigation.AppScreen
|
import at.mocode.frontend.core.navigation.AppScreen
|
||||||
import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager
|
import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager
|
||||||
|
import at.mocode.frontend.shell.desktop.navigation.DesktopNavigationPort
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.layout.DesktopMainLayout
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ fun DesktopApp() {
|
||||||
val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
|
val returnTo = screen.returnTo ?: AppScreen.VeranstaltungVerwaltung
|
||||||
nav.navigateToScreen(returnTo)
|
nav.navigateToScreen(returnTo)
|
||||||
},
|
},
|
||||||
onBack = { /* Desktop hat keine PortalDashboard-Page */ },
|
onBack = { nav.navigateBack() },
|
||||||
)
|
)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|
@ -85,6 +85,7 @@ fun DesktopApp() {
|
||||||
authTokenManager.clearToken()
|
authTokenManager.clearToken()
|
||||||
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung))
|
nav.navigateToScreen(AppScreen.Login(returnTo = AppScreen.VeranstaltungVerwaltung))
|
||||||
},
|
},
|
||||||
|
isAuthenticated = authState.isAuthenticated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,363 @@
|
||||||
|
package at.mocode.frontend.shell.desktop.screens.layout.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||||
|
import at.mocode.frontend.core.navigation.AppScreen
|
||||||
|
import at.mocode.frontend.features.billing.presentation.BillingScreen
|
||||||
|
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
||||||
|
import at.mocode.frontend.features.device.initialization.data.local.DeviceInitializationSettingsManager
|
||||||
|
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||||
|
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen
|
||||||
|
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerIntent
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerScreen
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerViewModel
|
||||||
|
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
|
||||||
|
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||||
|
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.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.veranstalter.presentation.VeranstaltungKonfigScreen
|
||||||
|
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
||||||
|
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||||
|
import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterAuswahl
|
||||||
|
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.VeranstaltungVerwaltung
|
||||||
|
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
|
||||||
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DesktopContentArea(
|
||||||
|
currentScreen: AppScreen,
|
||||||
|
onNavigate: (AppScreen) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSettingsChange: (DeviceInitializationSettings) -> Unit,
|
||||||
|
) {
|
||||||
|
when (currentScreen) {
|
||||||
|
// DeviceInitialization (Geräte-Setup)
|
||||||
|
is AppScreen.DeviceInitialization -> {
|
||||||
|
println("[Screen] Rendering DeviceInitialization")
|
||||||
|
val viewModel = koinViewModel<DeviceInitializationViewModel> {
|
||||||
|
parametersOf({ finalSettings: DeviceInitializationSettings ->
|
||||||
|
DeviceInitializationSettingsManager.saveSettings(finalSettings)
|
||||||
|
// Vision_04: Sicherheitsschlüssel als Token setzen, damit Cloud-Suche funktioniert
|
||||||
|
val authTokenManager = org.koin.core.context.GlobalContext.get().get<AuthTokenManager>()
|
||||||
|
authTokenManager.setToken(finalSettings.sharedKey)
|
||||||
|
onSettingsChange(finalSettings)
|
||||||
|
onNavigate(AppScreen.VeranstaltungVerwaltung)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
DeviceInitializationScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Haupt-Zentrale: Veranstaltung-Verwaltung
|
||||||
|
is AppScreen.VeranstaltungVerwaltung -> {
|
||||||
|
VeranstaltungVerwaltung(
|
||||||
|
onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) },
|
||||||
|
onNewVeranstaltung = {
|
||||||
|
// Wenn wir direkt aus der Übersicht kommen, erst Veranstalter wählen lassen
|
||||||
|
onNavigate(AppScreen.VeranstalterAuswahl)
|
||||||
|
},
|
||||||
|
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
|
||||||
|
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
|
||||||
|
onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) },
|
||||||
|
onNavigateToFunktionaere = { onNavigate(AppScreen.FunktionaerVerwaltung) },
|
||||||
|
onNavigateToVeranstalter = { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
|
onNavigateToZnsImport = { onNavigate(AppScreen.StammdatenImport) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ZNS Importer ---
|
||||||
|
is AppScreen.StammdatenImport -> {
|
||||||
|
StammdatenImportScreen(
|
||||||
|
onBack = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pferde-Verwaltung & Profil ---
|
||||||
|
is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
|
||||||
|
val viewModel = koinViewModel<PferdeViewModel>()
|
||||||
|
PferdeScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.PferdProfil -> {
|
||||||
|
val viewModel = koinViewModel<PferdeViewModel>()
|
||||||
|
// In der aktuellen Ausbaustufe wählen wir das Pferd im ViewModel aus
|
||||||
|
LaunchedEffect(currentScreen.id) {
|
||||||
|
// Mock: Wir suchen das Pferd in den Suchergebnissen
|
||||||
|
viewModel.uiState.searchResults.find { it.id == currentScreen.id.toString() }?.let {
|
||||||
|
viewModel.selectPferd(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PferdeScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reiter-Verwaltung & Profil ---
|
||||||
|
is AppScreen.Reiter, is AppScreen.ReiterVerwaltung -> {
|
||||||
|
val viewModel = koinViewModel<ReiterViewModel>()
|
||||||
|
ReiterScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.ReiterProfil -> {
|
||||||
|
val viewModel = koinViewModel<ReiterViewModel>()
|
||||||
|
LaunchedEffect(currentScreen.id) {
|
||||||
|
viewModel.uiState.searchResults.find { it.id == currentScreen.id.toString() }?.let {
|
||||||
|
viewModel.selectReiter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReiterScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Verein-Verwaltung & Profil ---
|
||||||
|
is AppScreen.Vereine, is AppScreen.VereinVerwaltung -> {
|
||||||
|
println("[Screen] Rendering VereinVerwaltung (VereinScreen)")
|
||||||
|
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||||
|
VereinScreen(viewModel = vereinViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VereinProfil -> {
|
||||||
|
println("[Screen] Rendering VereinProfil #${currentScreen.id}")
|
||||||
|
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||||
|
// Mock: Selektion im ViewModel (falls unterstützt)
|
||||||
|
VereinScreen(viewModel = vereinViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Funktionaer-Verwaltung & Profil ---
|
||||||
|
is AppScreen.FunktionaerVerwaltung -> {
|
||||||
|
val viewModel = koinViewModel<FunktionaerViewModel>()
|
||||||
|
FunktionaerScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.FunktionaerProfil -> {
|
||||||
|
val viewModel = koinViewModel<FunktionaerViewModel>()
|
||||||
|
LaunchedEffect(currentScreen.id) {
|
||||||
|
viewModel.state.value.list.find { it.id == currentScreen.id }?.let {
|
||||||
|
viewModel.send(FunktionaerIntent.Select(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FunktionaerScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Veranstalter-Verwaltung & Profil ---
|
||||||
|
is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
|
||||||
|
onBack = onBack,
|
||||||
|
onNew = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
|
onEdit = { onNavigate(AppScreen.VeranstalterProfil(it)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
is AppScreen.VeranstalterProfil -> VeranstalterDetail(
|
||||||
|
veranstalterId = currentScreen.id,
|
||||||
|
onBack = onBack,
|
||||||
|
onZurVeranstaltung = { evtId: Long -> onNavigate(AppScreen.VeranstaltungProfil(currentScreen.id, evtId)) },
|
||||||
|
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(currentScreen.id)) },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Neuer Flow: Veranstalter auswählen → Detail → Veranstaltung-Übersicht
|
||||||
|
is AppScreen.VeranstalterAuswahl -> VeranstalterAuswahl(
|
||||||
|
onBack = onBack,
|
||||||
|
onWeiter = { veranstalterId -> onNavigate(AppScreen.VeranstalterDetail(veranstalterId)) },
|
||||||
|
onNeu = { onNavigate(AppScreen.VeranstalterNeu) },
|
||||||
|
)
|
||||||
|
|
||||||
|
is AppScreen.VeranstalterNeu -> VeranstalterAnlegenWizard(
|
||||||
|
onCancel = onBack,
|
||||||
|
onVereinCreated = { newId: Long -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
is AppScreen.VeranstalterDetail -> {
|
||||||
|
val vId = currentScreen.veranstalterId
|
||||||
|
VeranstalterDetail(
|
||||||
|
veranstalterId = vId,
|
||||||
|
onBack = onBack,
|
||||||
|
onZurVeranstaltung = { evtId -> onNavigate(AppScreen.VeranstaltungProfil(vId, evtId)) },
|
||||||
|
onNeuVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig(vId)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungKonfig -> {
|
||||||
|
val vId = currentScreen.veranstalterId
|
||||||
|
VeranstaltungKonfigScreen(
|
||||||
|
veranstalterId = vId,
|
||||||
|
onAbbrechen = onBack,
|
||||||
|
onSpeichern = { _, _, _ ->
|
||||||
|
// In-Memory Store Simulation
|
||||||
|
// val allEvents = Store.allEvents()
|
||||||
|
// val newId = (allEvents.maxOfOrNull { it.id } ?: 0L) + 1L
|
||||||
|
// ...
|
||||||
|
onNavigate(AppScreen.VeranstaltungProfil(vId, 0L)) // Mock
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungProfil -> {
|
||||||
|
VeranstaltungProfilScreen(
|
||||||
|
veranstalterId = currentScreen.veranstalterId,
|
||||||
|
veranstaltungId = currentScreen.veranstaltungId,
|
||||||
|
onBack = onBack,
|
||||||
|
onTurnierOpen = { tId ->
|
||||||
|
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, tId))
|
||||||
|
},
|
||||||
|
onTurnierNeu = {
|
||||||
|
onNavigate(AppScreen.TurnierNeu(currentScreen.veranstaltungId))
|
||||||
|
},
|
||||||
|
onNavigateToVeranstalterProfil = { verId ->
|
||||||
|
onNavigate(AppScreen.VeranstalterProfil(verId))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungDetail -> {
|
||||||
|
VeranstaltungDetailScreen(
|
||||||
|
veranstaltungId = currentScreen.id,
|
||||||
|
onBack = onBack,
|
||||||
|
onTurnierOeffnen = { tId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tId)) },
|
||||||
|
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungNeu -> {
|
||||||
|
VeranstaltungNeuScreen(
|
||||||
|
onBack = onBack,
|
||||||
|
onSave = { onBack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.TurnierDetail -> {
|
||||||
|
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 {
|
||||||
|
val veranstaltung =
|
||||||
|
at.mocode.frontend.shell.desktop.data.Store.eventsFor(parent.id).firstOrNull { it.id == evtId }
|
||||||
|
// bewerbViewModel: BewerbViewModel, nennungViewModel: TurnierNennungViewModel, stammdatenViewModel: TurnierStammdatenViewModel
|
||||||
|
val bewerbViewModel: at.mocode.frontend.features.turnier.presentation.BewerbViewModel =
|
||||||
|
org.koin.compose.koinInject { parametersOf(currentScreen.turnierId) }
|
||||||
|
val nennungViewModel: at.mocode.frontend.features.turnier.presentation.TurnierNennungViewModel =
|
||||||
|
org.koin.compose.koinInject { parametersOf(currentScreen.turnierId) }
|
||||||
|
val stammdatenViewModel: at.mocode.frontend.features.turnier.presentation.TurnierStammdatenViewModel =
|
||||||
|
org.koin.compose.koinInject()
|
||||||
|
|
||||||
|
TurnierDetailScreen(
|
||||||
|
veranstaltungId = evtId,
|
||||||
|
turnierId = currentScreen.turnierId,
|
||||||
|
bewerbViewModel = bewerbViewModel,
|
||||||
|
nennungViewModel = nennungViewModel,
|
||||||
|
stammdatenViewModel = stammdatenViewModel,
|
||||||
|
eventVon = veranstaltung?.datumVon,
|
||||||
|
eventBis = veranstaltung?.datumBis,
|
||||||
|
eventOrt = veranstaltung?.ort,
|
||||||
|
veranstalterName = parent.name,
|
||||||
|
veranstalterOrt = parent.ort,
|
||||||
|
veranstalterLogoUrl = veranstaltung?.logoUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Billing -> {
|
||||||
|
val billingViewModel = koinViewModel<BillingViewModel>()
|
||||||
|
BillingScreen(
|
||||||
|
viewModel = billingViewModel,
|
||||||
|
veranstaltungId = currentScreen.veranstaltungId,
|
||||||
|
onBack = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.NennungsEingang -> {
|
||||||
|
NennungsEingangScreen(
|
||||||
|
onBack = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.EntryManagement -> {
|
||||||
|
val viewModel = koinViewModel<NennungViewModel>()
|
||||||
|
NennungManagementScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.ConnectivityCheck -> {
|
||||||
|
val viewModel = koinViewModel<PingViewModel>()
|
||||||
|
PingScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Profile -> {
|
||||||
|
val viewModel = koinViewModel<ProfileViewModel>()
|
||||||
|
ProfileScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Home, is AppScreen.Dashboard -> {
|
||||||
|
AdminUebersichtScreen(
|
||||||
|
onVeranstalterAuswahl = { onNavigate(AppScreen.VeranstalterAuswahl) },
|
||||||
|
onVeranstaltungOeffnen = { id -> onNavigate(AppScreen.VeranstaltungDetail(id)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
InvalidContextNotice(
|
||||||
|
message = "Dieser Screen (${currentScreen::class.simpleName}) ist noch nicht für Desktop optimiert.",
|
||||||
|
onBack = onBack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InvalidContextNotice(message: String, onBack: () -> Unit) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(32.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(message, color = Color(0xFFB91C1C), fontSize = 14.sp)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Button(onClick = onBack) { Text("Zur Auswahl") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package at.mocode.frontend.shell.desktop.screens.layout.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CloudDone
|
||||||
|
import androidx.compose.material.icons.filled.CloudOff
|
||||||
|
import androidx.compose.material.icons.filled.Dataset
|
||||||
|
import androidx.compose.material.icons.filled.Lan
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||||
|
import at.mocode.frontend.core.network.ConnectivityTracker
|
||||||
|
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
|
||||||
|
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DesktopFooterBar(
|
||||||
|
settings: DeviceInitializationSettings,
|
||||||
|
onSetupClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val connectivityTracker = koinInject<ConnectivityTracker>()
|
||||||
|
val discoveryService = koinInject<NetworkDiscoveryService>()
|
||||||
|
val znsImporter = koinInject<ZnsImportProvider>()
|
||||||
|
|
||||||
|
val online by connectivityTracker.isOnline.collectAsState()
|
||||||
|
val znsState = znsImporter.state
|
||||||
|
val discoveredServices = remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
|
||||||
|
val deviceName = settings.deviceName.ifBlank { "Unbekannt" }
|
||||||
|
|
||||||
|
// Periodisches Update der LAN-Geräte (mDNS)
|
||||||
|
LaunchedEffect(settings.isConfigured, settings.deviceName) {
|
||||||
|
if (settings.isConfigured && settings.deviceName.isNotBlank()) {
|
||||||
|
discoveryService.startDiscovery()
|
||||||
|
connectivityTracker.startTracking()
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
discoveredServices.value = discoveryService.getDiscoveredServices()
|
||||||
|
delay(5000.milliseconds)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
discoveryService.stopDiscovery()
|
||||||
|
connectivityTracker.stopTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
modifier = Modifier.clickable(onClick = onSetupClick)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(28.dp)
|
||||||
|
.padding(horizontal = Dimens.SpacingS),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||||
|
) {
|
||||||
|
// Status: Cloud Sync
|
||||||
|
StatusIndicator(
|
||||||
|
icon = if (online) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
||||||
|
label = if (online) "Cloud OK" else "Lokal",
|
||||||
|
color = if (online) AppColors.Success else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
|
||||||
|
VerticalDivider(modifier = Modifier.height(12.dp), color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
|
||||||
|
// Status: LAN Devices (mDNS)
|
||||||
|
val deviceCount = discoveredServices.value.size
|
||||||
|
StatusIndicator(
|
||||||
|
icon = Icons.Filled.Lan,
|
||||||
|
label = if (deviceCount > 0) "$deviceName ($deviceCount Peers)" else "$deviceName (Solitär)",
|
||||||
|
color = if (deviceCount > 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
|
||||||
|
VerticalDivider(modifier = Modifier.height(12.dp), color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
|
||||||
|
// Status: ZNS Stammdaten
|
||||||
|
val lastSync = znsState.lastSyncVersion
|
||||||
|
val znsLabel = if (lastSync != null) "ZNS: $lastSync" else "ZNS: Kein Sync"
|
||||||
|
StatusIndicator(
|
||||||
|
icon = Icons.Default.Dataset,
|
||||||
|
label = znsLabel,
|
||||||
|
color = if (lastSync != null) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = "v2.4.0-rc1 | Desktop-Alpha",
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StatusIndicator(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
color: Color
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier.size(14.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelSmall.copy(fontSize = 10.sp),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
package at.mocode.frontend.shell.desktop.screens.layout.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
import at.mocode.frontend.core.navigation.AppScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DesktopNavRail(
|
||||||
|
currentScreen: AppScreen,
|
||||||
|
onNavigate: (AppScreen) -> Unit,
|
||||||
|
isConfigured: Boolean
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxHeight().width(Dimens.NavRailWidth),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxHeight().padding(vertical = Dimens.SpacingM),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)
|
||||||
|
) {
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.Adjust,
|
||||||
|
label = "Logo",
|
||||||
|
selected = false,
|
||||||
|
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
selected = currentScreen is AppScreen.StammdatenImport,
|
||||||
|
onClick = { onNavigate(AppScreen.StammdatenImport) },
|
||||||
|
enabled = isConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
var showStammdatenMenu by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
label = "Stammdaten",
|
||||||
|
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung ||
|
||||||
|
currentScreen is AppScreen.Reiter || currentScreen is AppScreen.ReiterVerwaltung ||
|
||||||
|
currentScreen is AppScreen.Pferde || currentScreen is AppScreen.PferdVerwaltung ||
|
||||||
|
currentScreen is AppScreen.FunktionaerVerwaltung,
|
||||||
|
onClick = { showStammdatenMenu = true },
|
||||||
|
enabled = isConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showStammdatenMenu && isConfigured,
|
||||||
|
onDismissRequest = { showStammdatenMenu = false },
|
||||||
|
offset = DpOffset(Dimens.NavRailWidth, 0.dp)
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Vereine") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Vereine)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.People, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Reiter") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Reiter)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Pferde") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Pferde)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Richter") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.FunktionaerVerwaltung)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Gavel, contentDescription = null) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.Email,
|
||||||
|
label = "Mails",
|
||||||
|
selected = currentScreen is AppScreen.NennungsEingang,
|
||||||
|
onClick = { onNavigate(AppScreen.NennungsEingang) },
|
||||||
|
enabled = isConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.AppRegistration,
|
||||||
|
label = "Setup",
|
||||||
|
selected = currentScreen is AppScreen.DeviceInitialization,
|
||||||
|
onClick = { onNavigate(AppScreen.DeviceInitialization) },
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.WifiTethering,
|
||||||
|
label = "Connectivity",
|
||||||
|
selected = currentScreen is AppScreen.ConnectivityCheck,
|
||||||
|
onClick = { onNavigate(AppScreen.ConnectivityCheck) },
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun NavRailItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
enabled: Boolean = true
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val contentColor = if (!enabled) {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
|
||||||
|
} else if (selected) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||||
|
positioning = TooltipAnchorPosition.Above,
|
||||||
|
spacingBetweenTooltipAndAnchor = 4.dp
|
||||||
|
),
|
||||||
|
tooltip = {
|
||||||
|
PlainTooltip(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberTooltipState()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(Dimens.NavRailWidth)
|
||||||
|
.clickable(
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = ripple(bounded = false, radius = 24.dp),
|
||||||
|
onClick = onClick
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = label,
|
||||||
|
tint = contentColor,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,400 @@
|
||||||
|
package at.mocode.frontend.shell.desktop.screens.layout.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||||
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.theme.AppColors
|
||||||
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
import at.mocode.frontend.core.navigation.AppScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DesktopTopHeader(
|
||||||
|
currentScreen: AppScreen,
|
||||||
|
onNavigate: (AppScreen) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
isAuthenticated: Boolean,
|
||||||
|
connectedPeersCount: Int = 0
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(Dimens.TopBarHeight),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(horizontal = Dimens.SpacingL),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (currentScreen !is AppScreen.DeviceInitialization) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onBack,
|
||||||
|
modifier = Modifier.size(Dimens.IconSizeM)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Zurück",
|
||||||
|
modifier = Modifier.size(Dimens.IconSizeM),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(Dimens.SpacingS))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home Icon als Anker
|
||||||
|
IconButton(
|
||||||
|
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||||
|
modifier = Modifier.size(Dimens.IconSizeM)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Home,
|
||||||
|
contentDescription = "Home",
|
||||||
|
modifier = Modifier.size(Dimens.IconSizeM),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breadcrumb-Segmente
|
||||||
|
BreadcrumbContent(currentScreen, onNavigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||||
|
) {
|
||||||
|
// Sync-Status Indikator
|
||||||
|
val syncColor = if (connectedPeersCount > 0) AppColors.Success else MaterialTheme.colorScheme.outline
|
||||||
|
val syncText = if (connectedPeersCount > 0) "$connectedPeersCount Peer(s)" else "Offline"
|
||||||
|
|
||||||
|
val infiniteTransition = rememberInfiniteTransition()
|
||||||
|
val pulseAlpha by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 1f,
|
||||||
|
targetValue = 0.4f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Reverse
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(syncColor.copy(alpha = 0.1f), MaterialTheme.shapes.small)
|
||||||
|
.padding(horizontal = Dimens.SpacingS, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(8.dp),
|
||||||
|
shape = MaterialTheme.shapes.extraSmall,
|
||||||
|
color = if (connectedPeersCount > 0) syncColor.copy(alpha = pulseAlpha) else syncColor
|
||||||
|
) {}
|
||||||
|
Text(
|
||||||
|
text = syncText,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = syncColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier.height(16.dp),
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profil / Logout Bereich
|
||||||
|
if (isAuthenticated) {
|
||||||
|
Text(
|
||||||
|
text = "Administrator",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
IconButton(onClick = onLogout) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Logout,
|
||||||
|
contentDescription = "Abmelden",
|
||||||
|
modifier = Modifier.size(Dimens.IconSizeM),
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Gast",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
IconButton(onClick = { onNavigate(AppScreen.Login()) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Person,
|
||||||
|
contentDescription = "Anmelden",
|
||||||
|
modifier = Modifier.size(Dimens.IconSizeM),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BreadcrumbContent(
|
||||||
|
currentScreen: AppScreen,
|
||||||
|
onNavigate: (AppScreen) -> Unit
|
||||||
|
) {
|
||||||
|
val textStyle = MaterialTheme.typography.bodyMedium
|
||||||
|
val clickableColor = MaterialTheme.colorScheme.primary
|
||||||
|
val activeColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
when (currentScreen) {
|
||||||
|
is AppScreen.VeranstalterAuswahl -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter auswählen",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstalterNeu -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter-Verwaltung",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Neuer Veranstalter",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstalterDetail -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter-Verwaltung",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungProfil -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter-Verwaltung",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstalterVerwaltung) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstalter #${currentScreen.veranstalterId}",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstalterDetail(currentScreen.veranstalterId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungVerwaltung -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltungs-Verwaltung",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungDetail -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltungs-Verwaltung",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.id}",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.VeranstaltungNeu -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltungs-Verwaltung",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable { onNavigate(AppScreen.VeranstaltungVerwaltung) },
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Neue Veranstaltung",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.TurnierDetail -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Turnier #${currentScreen.turnierId}",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.TurnierNeu -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Neues Turnier",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Billing -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Veranstaltung #${currentScreen.veranstaltungId}",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.VeranstaltungDetail(currentScreen.veranstaltungId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Turnier #${currentScreen.turnierId}",
|
||||||
|
style = textStyle.copy(color = clickableColor),
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onNavigate(AppScreen.TurnierDetail(currentScreen.veranstaltungId, currentScreen.turnierId))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Abrechnung",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.StammdatenImport -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "ZNS Stammdaten Import",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Vereine -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Vereine",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Reiter -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Reiter",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Pferde -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Pferde",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.FunktionaerVerwaltung -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Richter-Verwaltung",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.NennungsEingang -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Nennungs-Eingang",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.ConnectivityCheck -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Connectivity Check",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.DeviceInitialization -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Geräte-Initialisierung",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AppScreen.Login -> {
|
||||||
|
BreadcrumbSeparator()
|
||||||
|
Text(
|
||||||
|
text = "Administrator-Login",
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold, color = activeColor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Generischer Fall für ungelistete Screens
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BreadcrumbSeparator() {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp).padding(horizontal = 4.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user