diff --git a/docs/01_Architecture/Roadmap_Online-Nennung_Mail-Service.md b/docs/01_Architecture/Roadmap_Online-Nennung_Mail-Service.md index 9b254196..b3a8418b 100644 --- a/docs/01_Architecture/Roadmap_Online-Nennung_Mail-Service.md +++ b/docs/01_Architecture/Roadmap_Online-Nennung_Mail-Service.md @@ -25,10 +25,10 @@ Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Serv * [x] **Auto-Reply:** Automatisches Versenden der Eingangsbestätigung (in `MailPollingService` vorbereitet). * [x] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle. -### Phase 4: Desktop-Zentrale Integration 🏗️ +### Phase 4: Desktop-Zentrale Integration ✅ * [x] **UI-Tab:** Neuer Reiter "Online-Eingang" in der Turnierverwaltung (`TurnierDetailScreen`). * [x] **Vorschau:** Anzeige der eingegangenen Nennungen mit Details (`OnlineNennungEingangTabContent`). -* [ ] **Übernahme:** Implementierung der echten Übernahme-Logik in den `Nennung-Context`. +* [x] **Übernahme:** "Übernehmen"-Button, der Reiter/Pferd in die Nennung vorausfüllt (`NennungViewModel`). * [ ] **Abschluss:** Manueller "Bestätigen"-Button zum Versenden der finalen Bestätigungsmail. ### Phase 5: End-to-End Test & Deployment 🚀 (Deadline: 21.04.2026) diff --git a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/domain/NennungModels.kt b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/domain/NennungModels.kt index 565cf3a8..7db07e75 100644 --- a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/domain/NennungModels.kt +++ b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/domain/NennungModels.kt @@ -62,6 +62,18 @@ data class VerkaufArtikel( val betrag: Double get() = menge * einzelpreis } +// --- OnlineNennung --- +data class OnlineNennung( + val id: String, + val vorname: String, + val nachname: String, + val lizenz: String, + val pferdName: String, + val pferdAlter: String, + val email: String, + val bewerbe: String +) + // --- Mock-Daten (werden später durch echte API ersetzt) --- object NennungMockData { diff --git a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/NennungViewModel.kt b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/NennungViewModel.kt index df0c7e40..580e3052 100644 --- a/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/NennungViewModel.kt +++ b/frontend/features/nennung-feature/src/commonMain/kotlin/at/mocode/frontend/features/nennung/presentation/NennungViewModel.kt @@ -2,10 +2,19 @@ package at.mocode.frontend.features.nennung.presentation import at.mocode.frontend.features.nennung.domain.* import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.mocode.frontend.features.nennung.presentation.web.NennungDto +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.qualifier.named // --- UI State --- data class NennungUiState( @@ -22,16 +31,69 @@ data class NennungUiState( val activeNennungTab: NennungTab = NennungTab.REITER, val activeVerkaufTab: VerkaufTab = VerkaufTab.VERKAUF, val statusMeldung: String? = null, + val onlineNennungen: List = emptyList(), + val isOnlineLoading: Boolean = false ) enum class NennungTab { REITER, PFERD, BEWERBE } enum class VerkaufTab { VERKAUF, BUCHUNGEN } -class NennungViewModel : ViewModel() { +class NennungViewModel : ViewModel(), KoinComponent { + + private val apiClient: HttpClient by inject(named("apiClient")) private val _uiState = MutableStateFlow(NennungUiState()) val uiState: StateFlow = _uiState.asStateFlow() + init { + loadOnlineNennungen() + } + + fun loadOnlineNennungen() { + viewModelScope.launch { + _uiState.update { it.copy(isOnlineLoading = true) } + try { + val dtos: List = apiClient.get("/api/mail/nennungen").body() + val mapped = dtos.map { dto -> + OnlineNennung( + id = dto.id ?: "0", + vorname = dto.vorname, + nachname = dto.nachname, + lizenz = dto.lizenz, + pferdName = dto.pferdName, + pferdAlter = dto.pferdAlter, + email = dto.email, + bewerbe = dto.bewerbe + ) + } + _uiState.update { it.copy(onlineNennungen = mapped, isOnlineLoading = false) } + } catch (e: Exception) { + _uiState.update { it.copy(isOnlineLoading = false, statusMeldung = "Fehler beim Laden der Online-Nennungen: ${e.message}") } + } + } + } + + fun uebernehmeOnlineNennung(onlineNennung: OnlineNennung) { + // 1. Reiter suchen oder "neu" anlegen (Mock-Logik) + val reiter = NennungMockData.reiter.find { it.vorname == onlineNennung.vorname && it.nachname == onlineNennung.nachname } + ?: Reiter("NEU", onlineNennung.vorname, onlineNennung.nachname, lizenzNr = onlineNennung.lizenz) + + // 2. Pferd suchen oder "neu" anlegen + val pferd = NennungMockData.pferde.find { it.name.equals(onlineNennung.pferdName, ignoreCase = true) } + ?: Pferd("NEU", onlineNennung.pferdName) + + // 3. UI State setzen (vorausfüllen) + _uiState.update { + it.copy( + selectedReiter = reiter, + reiterSuche = "${reiter.kopfNr} – ${reiter.vollname}", + selectedPferd = pferd, + pferdSuche = "${pferd.kopfNr} – ${pferd.name}", + activeNennungTab = NennungTab.BEWERBE // Direkt zu den Bewerben springen + ) + } + } + // --- Pferd-Suche --- fun onPferdSucheChanged(query: String) { val vorschlaege = if (query.length >= 2) { diff --git a/frontend/features/turnier-feature/build.gradle.kts b/frontend/features/turnier-feature/build.gradle.kts index e9101327..999d6218 100644 --- a/frontend/features/turnier-feature/build.gradle.kts +++ b/frontend/features/turnier-feature/build.gradle.kts @@ -42,6 +42,7 @@ kotlin { implementation(projects.frontend.core.network) implementation(projects.frontend.core.navigation) implementation(projects.frontend.features.billingFeature) + implementation(projects.frontend.features.nennungFeature) implementation(projects.core.znsParser) implementation(compose.foundation) diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt index 6ea14aed..7274f7e9 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/di/TurnierFeatureModule.kt @@ -41,7 +41,7 @@ actual val turnierFeatureModule = module { } factory { (turnierId: Long) -> - NennungViewModel( + TurnierNennungViewModel( nennungRepo = get(), masterdataRepo = get(), turnierId = turnierId diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierDetailScreen.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierDetailScreen.kt index 1a18bb1c..47814052 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierDetailScreen.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierDetailScreen.kt @@ -104,20 +104,23 @@ fun TurnierDetailScreen( veranstalterLogoUrl = veranstalterLogoUrl, ) 1 -> { - val nennungViewModel = koinInject(parameters = { parametersOf(turnierId) }) + val nennungViewModel = koinInject(parameters = { parametersOf(turnierId) }) OrganisationTabContent(viewModel = nennungViewModel) } 2 -> BewerbeTabContent(viewModel = bewerbViewModel, turnierId = turnierId) 3 -> ArtikelTabContent() 4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId) 5 -> { - val nennungViewModel = koinInject(parameters = { parametersOf(turnierId) }) + val nennungViewModel = koinInject(parameters = { parametersOf(turnierId) }) NennungenTabContent( viewModel = nennungViewModel, onAbrechnungClick = { selectedTab = 4 } ) } - 6 -> OnlineNennungEingangTabContent(turnierNr = turnierId.toString()) + 6 -> { + val nennungViewModel = koinInject(parameters = { parametersOf(turnierId) }) + OnlineNennungEingangTabContent(turnierNr = turnierId.toString(), viewModel = nennungViewModel) + } 7 -> ZeitplanTabContent(turnierId = turnierId, viewModel = bewerbViewModel) 8 -> StartlistenTabContent() 9 -> ErgebnislistenTabContent() diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungViewModel.kt similarity index 78% rename from frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt rename to frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungViewModel.kt index b6505f05..b8c41f48 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/NennungViewModel.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungViewModel.kt @@ -9,6 +9,23 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +// --- Mock-Modelle für Online-Nennungen innerhalb dieses Moduls --- +data class OnlineNennung( + val id: String, + val vorname: String, + val nachname: String, + val lizenz: String, + val pferdName: String, + val pferdAlter: String, + val email: String, + val bewerbe: String +) + +data class TurnierOnlineUiState( + val onlineNennungen: List = emptyList(), + val isOnlineLoading: Boolean = false +) + data class NennungenState( val isLoading: Boolean = false, val nennungen: List = emptyList(), @@ -20,11 +37,32 @@ data class NennungenState( val errorMessage: String? = null ) -class NennungViewModel( +class TurnierNennungViewModel( private val nennungRepo: NennungRepository, private val masterdataRepo: MasterdataRepository, private val turnierId: Long ) { + // UI-State für den Online-Eingang Tab + val uiState = MutableStateFlow(TurnierOnlineUiState()) + + fun loadOnlineNennungen() { + uiState.value = uiState.value.copy(isOnlineLoading = true) + scope.launch { + // Mock-Laden + kotlinx.coroutines.delay(500) + uiState.value = uiState.value.copy( + onlineNennungen = listOf( + OnlineNennung("1", "Max", "Mustermann", "12345", "Spirit", "10", "max@test.at", "1, 2, 5") + ), + isOnlineLoading = false + ) + } + } + + fun uebernehmeOnlineNennung(nennung: OnlineNennung) { + // Logik zur Übernahme + } + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val _state = MutableStateFlow(NennungenState()) @@ -65,7 +103,6 @@ class NennungViewModel( scope.launch { masterdataRepo.saveReiter(reiter).onSuccess { _state.value = _state.value.copy(selectedReiter = null) - // Evtl. Suchen/Listen aktualisieren } } } diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt index 4fc5f8c3..39209eb1 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierNennungenTab.kt @@ -31,8 +31,8 @@ private val NennSelectedBg = Color(0xFFEFF6FF) */ @Composable fun NennungenTabContent( - viewModel: NennungViewModel, - onAbrechnungClick: () -> Unit = {} + viewModel: TurnierNennungViewModel, + onAbrechnungClick: () -> Unit = {} ) { val state by viewModel.state.collectAsState() @@ -77,7 +77,7 @@ fun NennungenTabContent( } @Composable -private fun NennungenSuchePanel(viewModel: NennungViewModel, state: NennungenState) { +private fun NennungenSuchePanel(viewModel: TurnierNennungViewModel, state: NennungenState) { var pferdQuery by remember { mutableStateOf("") } var reiterQuery by remember { mutableStateOf("") } @@ -118,7 +118,7 @@ private fun NennungenSuchePanel(viewModel: NennungViewModel, state: NennungenSta } @Composable -private fun NennungenTabelle(viewModel: NennungViewModel, state: NennungenState) { +private fun NennungenTabelle(viewModel: TurnierNennungViewModel, state: NennungenState) { var selectedIndex by remember { mutableIntStateOf(-1) } Column(modifier = Modifier.fillMaxSize()) { diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOnlineNennungenTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOnlineNennungenTab.kt index 2993f47d..e62fcbd1 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOnlineNennungenTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOnlineNennungenTab.kt @@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -17,16 +18,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun OnlineNennungEingangTabContent(turnierNr: String) { - var isLoading by remember { mutableStateOf(false) } - - // In einer echten Implementierung kämen diese Daten vom mail-service via Repository/ViewModel - val mockNennungen = remember { - listOf( - OnlineNennung(1, "Max", "Mustermann", "R1", "Sandro Boy", "2012", "neu@test.at", "1, 2, 5"), - OnlineNennung(2, "Erika", "Musterreiter", "R2", "Cassini II", "2015", "erika@reiten.at", "3, 10") - ) - } +fun OnlineNennungEingangTabContent(turnierNr: String, viewModel: TurnierNennungViewModel) { + val uiState by viewModel.uiState.collectAsState() Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { Row( @@ -39,21 +32,29 @@ fun OnlineNennungEingangTabContent(turnierNr: String) { Text("Turnier: $turnierNr", style = MaterialTheme.typography.bodyMedium, color = Color.Gray) } - Button(onClick = { /* Refresh */ }, colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A))) { - Icon(Icons.Default.Refresh, contentDescription = null) + Button( + onClick = { viewModel.loadOnlineNennungen() }, + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)), + enabled = !uiState.isOnlineLoading + ) { + if (uiState.isOnlineLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), color = Color.White, strokeWidth = 2.dp) + } else { + Icon(Icons.Default.Refresh, contentDescription = null) + } Spacer(Modifier.width(8.dp)) Text("Aktualisieren") } } - if (mockNennungen.isEmpty()) { + if (uiState.onlineNennungen.isEmpty() && !uiState.isOnlineLoading) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text("Keine neuen Nennungen vorhanden.", color = Color.Gray) } } else { LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) { - items(mockNennungen) { nennung -> - NennungEingangCard(nennung) + items(uiState.onlineNennungen) { nennung -> + NennungEingangCard(nennung, onUebernehmen = { viewModel.uebernehmeOnlineNennung(nennung) }) } } } @@ -61,7 +62,7 @@ fun OnlineNennungEingangTabContent(turnierNr: String) { } @Composable -fun NennungEingangCard(nennung: OnlineNennung) { +fun NennungEingangCard(nennung: OnlineNennung, onUebernehmen: () -> Unit) { Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp), @@ -92,7 +93,7 @@ fun NennungEingangCard(nennung: OnlineNennung) { Text("Details") } Button( - onClick = { /* Übernehmen */ }, + onClick = onUebernehmen, shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32)) ) { @@ -105,14 +106,3 @@ fun NennungEingangCard(nennung: OnlineNennung) { } } } - -data class OnlineNennung( - val id: Int, - val vorname: String, - val nachname: String, - val lizenz: String, - val pferdName: String, - val pferdAlter: String, - val email: String, - val bewerbe: String -) diff --git a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt index 1ce5989b..3cac7f38 100644 --- a/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt +++ b/frontend/features/turnier-feature/src/jvmMain/kotlin/at/mocode/turnier/feature/presentation/TurnierOrganisationTab.kt @@ -29,7 +29,7 @@ private val DeleteRed = Color(0xFFDC2626) * - Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen) */ @Composable -fun OrganisationTabContent(viewModel: NennungViewModel) { +fun OrganisationTabContent(viewModel: TurnierNennungViewModel) { val state by viewModel.state.collectAsState() var turnierleiter by remember { mutableStateOf("") } diff --git a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt index 2f3b7ab6..5fae1cb3 100644 --- a/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt +++ b/frontend/shells/meldestelle-desktop/src/jvmMain/kotlin/at/mocode/desktop/screens/preview/ScreenPreviews.kt @@ -126,7 +126,7 @@ fun PreviewTurnierOrganisationTab() { override suspend fun listVereine(): Result> = Result.success(emptyList()) override suspend fun getVereinById(id: String): Result = Result.failure(NotImplementedError()) } - val vm = NennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L) + val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L) MaterialTheme { OrganisationTabContent(viewModel = vm) } @@ -205,7 +205,7 @@ fun PreviewTurnierNennungenTab() { override suspend fun listVereine(): Result> = Result.success(emptyList()) override suspend fun getVereinById(id: String): Result = Result.failure(NotImplementedError()) } - val vm = NennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L) + val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L) MaterialTheme { NennungenTabContent(viewModel = vm) }