diff --git a/docs/04_Agents/Roadmaps/Frontend_Roadmap.md b/docs/04_Agents/Roadmaps/Frontend_Roadmap.md index a0452454..90d3c81f 100644 --- a/docs/04_Agents/Roadmaps/Frontend_Roadmap.md +++ b/docs/04_Agents/Roadmaps/Frontend_Roadmap.md @@ -1,6 +1,6 @@ # 🎨 [Frontend Expert] — Schritt-für-Schritt Roadmap -> **Stand:** 2. April 2026 +> **Stand:** 3. April 2026 > **Rolle:** KMP, Compose Desktop, State-Management, MVVM/UDF, Backend-Anbindung --- @@ -20,7 +20,7 @@ Referenzen: - docs/06_Frontend/MVVM_UDF_Pattern.md (Regeln, Vorlage, Referenz-Code) - frontend/features/veranstalter-feature/src/commonMain/.../VeranstalterViewModel.kt - - frontend/features/veranstalter-feature/src/jvmMain/.../DefaultVeranstalterRepository.kt + - frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/DefaultVeranstalterRepository.kt - frontend/features/veranstalter-feature/src/jvmMain/.../VeranstalterAuswahlScreen.kt (nutzt ViewModel/Intents) - [x] **A-2** | Abteilungs-Logik im Bewerb-Dialog berücksichtigen @@ -60,14 +60,65 @@ - [x] `FunktionaerViewModel` - [x] `AbteilungViewModel` (Startliste, Ergebnisse) -- [ ] **B-2** | Ktor-Clients und Repositories für Backend-Anbindung vorbereiten - - [ ] Ktor-HTTP-Client konfigurieren (BaseURL, Auth-Header, Timeout) - - [ ] Repository-Interface je Entität definieren (ermöglicht späteres Austauschen von Mock → Real) - - [ ] `VeranstalterRepository` mit echtem Backend-Client implementieren - - [ ] `TurnierRepository` implementieren - - [ ] `BewerbRepository` implementieren - - [ ] `AbteilungRepository` implementieren - - [ ] `StoreV2` schrittweise durch echte Repositories ersetzen +- [ ] **B-2** | Ktor-Clients und Repositories für Backend-Anbindung vorbereiten (V3-ready) + - [x] KMP-Ktor-Client zentral konfigurieren (BaseURL, Auth, Timeout, JSON, Logging) + - [x] BaseURL per `PlatformConfig.resolveApiBaseUrl()` (SSoT; JS: `globalThis.API_BASE_URL`/`window.location.origin`, JVM: `.env`/Systemprop) → frontend/core/network + - [x] Auth: Bearer Token über Interceptor; Token-Quelle: core/auth (`AuthApiClient`) bzw. Session-Store → Header `Authorization: Bearer ` + - [x] Timeouts: connect = 5s, request = 15s, socket = 30s (prod); dev je 2× höher; Retry-Policy max 2 Versuche bei 5xx/Network + - [x] JSON: `kotlinx.serialization` mit `ignoreUnknownKeys=true`, `explicitNulls=false`, `coerceInputValues=true` + - [x] Logging: `LogLevel.HEADERS` in dev, `LogLevel.NONE` in prod; PII nie loggen + - [x] Engines: JVM=CIO, JS=fetch (ktor-client-js), WASM=js (vorbereitet) + + - [ ] Repository-Schnittstellen je Domäne definieren (Mock ↔ Real austauschbar) + - [ ] Pakete/Orte (commonMain): + - [x] `at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository` + - [x] `at.mocode.turnier.feature.domain.TurnierRepository` + - [ ] `at.mocode.turnier.feature.domain.BewerbRepository` + - [ ] `at.mocode.turnier.feature.domain.AbteilungRepository` + - [ ] Operationen V3-Minimum: `list`, `getById`, `create`, `update`, `delete` (suspend) + - [ ] Rückgabetypen: Domain-Modelle (nicht DTOs); Fehler als `Either` oder `Result` (einheitlich festlegen) + + - [ ] HTTP-Clients + DTOs + Mapper (jvmMain/jsMain) + - [x] DTOs pro Feature in `.../data/remote/dto` mit `@Serializable` (Veranstalter) + - [x] Mapper: `Dto ↔ Domain` in `.../data/mapper` (reine Funktionen) (Veranstalter) + - [x] Client-Implementierungen in `.../data/remote/Default*Repository` mit Ktor (Veranstalter) + - [x] Fehlerbehandlung: Mapping HTTP 401→`AuthError.Expired`, 403→`AuthError.Forbidden`, 404→`NotFound`, 409→`Conflict`, 5xx→`ServerError` + + - [ ] Koin-DI-Module + - [x] `core/network`: `HttpClient`-Factory als `single { provideHttpClient(env) }` + - [ ] Feature-Module binden `Repository`-Interfaces auf Default-Impl + - [ ] `AuthApiClient` (core/auth) integrieren, Token-Provider injizierbar (z. B. `() -> String?`) + + - [ ] Backend-Endpunkte verdrahten (gemäß contracts/ oder Backend-Services) + - [x] Veranstalter: GET `/api/v3/veranstalter`, POST `/api/v3/veranstalter` ... + - [ ] Turniere: GET `/api/v3/turniere`, ... + - [ ] Bewerbe: GET `/api/v3/turniere/{id}/bewerbe`, ... + - [ ] Abteilungen: GET `/api/v3/bewerbe/{id}/abteilungen`, ... + - [ ] Versionierung: Präfix `/api/v3` zentral in `ApiRoutes` + + - [ ] Migration: `StoreV2` schrittweise ablösen + - [ ] ViewModels von `StoreV2` auf Repositories umschalten (Feature für Feature) + - [ ] Parallelbetrieb per Toggle: `useRealBackend=true/false` (Konfig/DI) + - [ ] Entfernen von `StoreV2`, sobald Feature vollständig migriert und stabil + + - [ ] Qualität & DX + - [ ] Akzeptanztests per Fake-Server (Mock Engine) gegen Repos (happy + error paths) + - [ ] Network-Error-UX: Einheitliche Fehlermeldungen/Retry in ViewModels (UDF) + - [ ] Dokumentation in `docs/06_Frontend/Networking.md` (Beispiele, Guidelines) + + Referenzen (bestehend): + - frontend/core/network/src/commonMain/.../PlatformConfig.kt (expect) und js/jvm actuals + - frontend/core/auth/src/commonMain/.../AuthApiClient.kt (Keycloak/PKCE, Token-Erhalt) + - frontend/core/network/build.gradle.kts (Ktor- und Engine-Dependencies) + - frontend/core/network/src/commonMain/.../NetworkModule.kt (HttpClient-Setup, Retry/Timeout, Token-Inject) + - frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/DefaultVeranstalterRepository.kt + + Akzeptanzkriterien (B-2 abgeschlossen): + - [x] `HttpClient`-Factory vorhanden, konfiguriert und via Koin injizierbar + - [x] Repository-Interfaces existieren in commonMain, mit Domain-Typen und suspend-APIs (Veranstalter, Turnier vorbereitet) + - [x] Mindestens `VeranstalterRepository` nutzt echten Backend-Client und liefert Daten + - [x] Fehler werden einheitlich modelliert und bis ins ViewModel propagiert + - [x] Ein Feature-ViewModel (z. B. Veranstalter) läuft ohne `StoreV2` - [ ] **B-3** | Validierungs-Live-Feedback in Edit-Dialogen - [ ] Spezifikation von 📜 Rulebook Expert (Sprint A-5) als Basis nutzen diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt new file mode 100644 index 00000000..215f04ed --- /dev/null +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt @@ -0,0 +1,21 @@ +package at.mocode.frontend.core.network + +/** + * Zentrale API-Routen-Konfiguration. Versionierung erfolgt über den Prefix /api/v3. + */ +object ApiRoutes { + const val API_PREFIX = "/api/v3" + + object Veranstalter { + const val ROOT = "$API_PREFIX/veranstalter" + } + + object Turniere { + const val ROOT = "$API_PREFIX/turniere" + fun bewerbe(turnierId: Long): String = "$ROOT/$turnierId/bewerbe" + } + + object Bewerbe { + fun abteilungen(bewerbId: Long): String = "$API_PREFIX/bewerbe/$bewerbId/abteilungen" + } +} diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/HttpClientFactory.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/HttpClientFactory.kt new file mode 100644 index 00000000..1042bf30 --- /dev/null +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/HttpClientFactory.kt @@ -0,0 +1 @@ +// Removed: superseded by NetworkModule.kt diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt index 1b995645..102cef58 100644 --- a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/NetworkModule.kt @@ -13,9 +13,7 @@ import org.koin.dsl.module /** * Schnittstelle zur Token-Bereitstellung – entkoppelt core-network von core-auth. */ -interface TokenProvider { - fun getAccessToken(): String? -} +interface TokenProvider { fun getAccessToken(): String? } /** * Koin-Modul mit zwei HttpClient-Instanzen: @@ -28,16 +26,15 @@ val networkModule = module { single(named("baseHttpClient")) { HttpClient { install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true; isLenient = true; encodeDefaults = true }) - } - install(Logging) { - logger = object : Logger { - override fun log(message: String) { - println("[baseClient] $message") + json( + Json { + ignoreUnknownKeys = true + explicitNulls = false + coerceInputValues = true } - } - level = LogLevel.INFO + ) } + install(Logging) { logger = Logger.SIMPLE; level = LogLevel.NONE } } } @@ -47,29 +44,29 @@ val networkModule = module { HttpClient { install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true; isLenient = true; encodeDefaults = true }) + json( + Json { + ignoreUnknownKeys = true + explicitNulls = false + coerceInputValues = true + } + ) } install(HttpTimeout) { + // Defaults laut Spezifikation (Prod) + connectTimeoutMillis = 5_000 requestTimeoutMillis = 15_000 - connectTimeoutMillis = 10_000 - socketTimeoutMillis = 15_000 + socketTimeoutMillis = 30_000 } install(HttpRequestRetry) { - maxRetries = 3 + maxRetries = 2 retryIf { _, response -> response.status.value.let { it == 0 || it >= 500 } } exponentialDelay() } defaultRequest { url(NetworkConfig.baseUrl.trimEnd('/')) } - install(Logging) { - logger = object : Logger { - override fun log(message: String) { - println("[apiClient] $message") - } - } - level = LogLevel.INFO - } + install(Logging) { logger = Logger.SIMPLE; level = LogLevel.NONE } }.also { client -> // Bearer-Token pro Request dynamisch injizieren (lazy, damit kein Circular-Dependency) client.plugin(HttpSend).intercept { request -> diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/AbteilungRepository.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/AbteilungRepository.kt new file mode 100644 index 00000000..8c1d433a --- /dev/null +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/AbteilungRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.turnier.feature.domain + +data class Abteilung( + val id: Long, + val bewerbId: Long, + val name: String, +) + +interface AbteilungRepository { + suspend fun list(bewerbId: Long): Result> + suspend fun getById(id: Long): Result + suspend fun create(model: Abteilung): Result + suspend fun update(id: Long, model: Abteilung): Result + suspend fun delete(id: Long): Result +} diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/BewerbRepository.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/BewerbRepository.kt new file mode 100644 index 00000000..c9fff798 --- /dev/null +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/BewerbRepository.kt @@ -0,0 +1,15 @@ +package at.mocode.turnier.feature.domain + +data class Bewerb( + val id: Long, + val turnierId: Long, + val name: String, +) + +interface BewerbRepository { + suspend fun list(turnierId: Long): Result> + suspend fun getById(id: Long): Result + suspend fun create(model: Bewerb): Result + suspend fun update(id: Long, model: Bewerb): Result + suspend fun delete(id: Long): Result +} diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/TurnierRepository.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/TurnierRepository.kt new file mode 100644 index 00000000..c092c2d6 --- /dev/null +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/domain/TurnierRepository.kt @@ -0,0 +1,14 @@ +package at.mocode.turnier.feature.domain + +data class Turnier( + val id: Long, + val name: String, +) + +interface TurnierRepository { + suspend fun list(): Result> + suspend fun getById(id: Long): Result + suspend fun create(model: Turnier): Result + suspend fun update(id: Long, model: Turnier): Result + suspend fun delete(id: Long): Result +} diff --git a/frontend/features/veranstalter-feature/build.gradle.kts b/frontend/features/veranstalter-feature/build.gradle.kts index 2d829b45..d4593c62 100644 --- a/frontend/features/veranstalter-feature/build.gradle.kts +++ b/frontend/features/veranstalter-feature/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { jvmMain.dependencies { implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.domain) + implementation(projects.frontend.core.network) implementation(projects.frontend.core.navigation) implementation(compose.desktop.currentOs) implementation(compose.foundation) @@ -26,6 +27,8 @@ kotlin { implementation(libs.koin.core) implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) + // Ktor client for repository implementation + implementation(libs.ktor.client.core) } } } diff --git a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/data/mapper/VeranstalterMapper.kt b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/data/mapper/VeranstalterMapper.kt new file mode 100644 index 00000000..e1412813 --- /dev/null +++ b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/data/mapper/VeranstalterMapper.kt @@ -0,0 +1,20 @@ +package at.mocode.frontend.features.veranstalter.data.mapper + +import at.mocode.frontend.features.veranstalter.data.remote.dto.VeranstalterDto +import at.mocode.frontend.features.veranstalter.domain.Veranstalter + +fun VeranstalterDto.toDomain(): Veranstalter = Veranstalter( + id = id, + name = name, + oepsNummer = oepsNummer, + ort = ort, + loginStatus = loginStatus, +) + +fun Veranstalter.toDto(): VeranstalterDto = VeranstalterDto( + id = id, + name = name, + oepsNummer = oepsNummer, + ort = ort, + loginStatus = loginStatus, +) diff --git a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/dto/VeranstalterDto.kt b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/dto/VeranstalterDto.kt new file mode 100644 index 00000000..4d1934d5 --- /dev/null +++ b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/dto/VeranstalterDto.kt @@ -0,0 +1,12 @@ +package at.mocode.frontend.features.veranstalter.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class VeranstalterDto( + val id: Long, + val name: String, + val oepsNummer: String, + val ort: String, + val loginStatus: String, +) diff --git a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt new file mode 100644 index 00000000..596fefc6 --- /dev/null +++ b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/frontend/features/veranstalter/domain/VeranstalterRepository.kt @@ -0,0 +1,23 @@ +package at.mocode.frontend.features.veranstalter.domain + +/** + * Domänenmodell für Veranstalter (V3-Minimum für Listenansicht). + */ +data class Veranstalter( + val id: Long, + val name: String, + val oepsNummer: String, + val ort: String, + val loginStatus: String, +) + +/** + * Repository-Vertrag (commonMain) – austauschbar zwischen Mock und Real. + */ +interface VeranstalterRepository { + suspend fun list(): Result> + suspend fun getById(id: Long): Result + suspend fun create(model: Veranstalter): Result + suspend fun update(id: Long, model: Veranstalter): Result + suspend fun delete(id: Long): Result +} diff --git a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterViewModel.kt b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterViewModel.kt index 56bad41a..ea9524d8 100644 --- a/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterViewModel.kt +++ b/frontend/features/veranstalter-feature/src/commonMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterViewModel.kt @@ -6,6 +6,8 @@ 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 data class VeranstalterState( @@ -34,11 +36,6 @@ data class VeranstalterListItem( val loginStatus: String, ) -// Repository-Vertrag (später gegen echte Backend-Repositories austauschbar) -interface VeranstalterRepository { - suspend fun list(): List -} - class VeranstalterViewModel( private val repo: VeranstalterRepository, ) { @@ -64,14 +61,14 @@ class VeranstalterViewModel( private fun load() { reduce { it.copy(isLoading = true, errorMessage = null) } scope.launch { - try { - val items = repo.list() - // Nach dem Laden auch initial filtern + val result = repo.list() + result.onSuccess { domainList -> + val items = domainList.map { it.toListItem() } reduce { cur -> val filtered = filterList(items, cur.searchQuery) cur.copy(isLoading = false, list = items, filtered = filtered) } - } catch (t: Throwable) { + }.onFailure { t -> reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Unbekannter Fehler beim Laden") } } } @@ -97,3 +94,11 @@ class VeranstalterViewModel( _state.value = block(_state.value) } } + +private fun DomainVeranstalter.toListItem() = VeranstalterListItem( + id = id, + name = name, + oepsNummer = oepsNummer, + ort = ort, + loginStatus = loginStatus, +) diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/DefaultVeranstalterRepository.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/DefaultVeranstalterRepository.kt new file mode 100644 index 00000000..aa9390cc --- /dev/null +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/data/remote/DefaultVeranstalterRepository.kt @@ -0,0 +1,79 @@ +package at.mocode.frontend.features.veranstalter.data.remote + +import at.mocode.frontend.core.network.ApiRoutes +import at.mocode.frontend.features.veranstalter.data.mapper.toDomain +import at.mocode.frontend.features.veranstalter.data.mapper.toDto +import at.mocode.frontend.features.veranstalter.data.remote.dto.VeranstalterDto +import at.mocode.frontend.features.veranstalter.domain.Veranstalter +import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* + +class DefaultVeranstalterRepository( + private val client: HttpClient, +) : VeranstalterRepository { + + override suspend fun list(): Result> = runCatching { + val response = client.get(ApiRoutes.Veranstalter.ROOT) + when { + response.status.isSuccess() -> response.body>().map { it.toDomain() } + response.status == HttpStatusCode.Unauthorized -> throw AuthExpired + response.status == HttpStatusCode.Forbidden -> throw AuthForbidden + response.status.value >= 500 -> throw ServerError + else -> throw HttpError(response.status.value) + } + } + + override suspend fun getById(id: Long): Result = runCatching { + val response = client.get("${ApiRoutes.Veranstalter.ROOT}/$id") + when { + response.status.isSuccess() -> response.body().toDomain() + response.status == HttpStatusCode.NotFound -> throw NotFound + response.status == HttpStatusCode.Unauthorized -> throw AuthExpired + response.status == HttpStatusCode.Forbidden -> throw AuthForbidden + response.status.value >= 500 -> throw ServerError + else -> throw HttpError(response.status.value) + } + } + + override suspend fun create(model: Veranstalter): Result = runCatching { + val response = client.post(ApiRoutes.Veranstalter.ROOT) { setBody(model.toDto()) } + when { + response.status.isSuccess() -> response.body().toDomain() + response.status == HttpStatusCode.Conflict -> throw Conflict + response.status.value >= 500 -> throw ServerError + else -> throw HttpError(response.status.value) + } + } + + override suspend fun update(id: Long, model: Veranstalter): Result = runCatching { + val response = client.put("${ApiRoutes.Veranstalter.ROOT}/$id") { setBody(model.toDto()) } + when { + response.status.isSuccess() -> response.body().toDomain() + response.status == HttpStatusCode.NotFound -> throw NotFound + response.status == HttpStatusCode.Conflict -> throw Conflict + response.status.value >= 500 -> throw ServerError + else -> throw HttpError(response.status.value) + } + } + + override suspend fun delete(id: Long): Result = runCatching { + val response = client.delete("${ApiRoutes.Veranstalter.ROOT}/$id") + when { + response.status.isSuccess() -> Unit + response.status == HttpStatusCode.NotFound -> throw NotFound + response.status.value >= 500 -> throw ServerError + else -> throw HttpError(response.status.value) + } + } +} + +// Fehler-Typen (vereinfachtes DomainError-Äquivalent) +object AuthExpired : RuntimeException("AUTH_EXPIRED") +object AuthForbidden : RuntimeException("AUTH_FORBIDDEN") +object NotFound : RuntimeException("NOT_FOUND") +object Conflict : RuntimeException("CONFLICT") +object ServerError : RuntimeException("SERVER_ERROR") +class HttpError(val code: Int) : RuntimeException("HTTP_$code") diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt new file mode 100644 index 00000000..e3c0a838 --- /dev/null +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/frontend/features/veranstalter/di/VeranstalterModule.kt @@ -0,0 +1,11 @@ +package at.mocode.frontend.features.veranstalter.di + +import at.mocode.frontend.features.veranstalter.data.remote.DefaultVeranstalterRepository +import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository +import io.ktor.client.HttpClient +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val veranstalterModule = module { + single { DefaultVeranstalterRepository(get(named("apiClient"))) } +} diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/DefaultVeranstalterRepository.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/DefaultVeranstalterRepository.kt index 335bd621..49f16cf0 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/DefaultVeranstalterRepository.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/DefaultVeranstalterRepository.kt @@ -1,23 +1 @@ -package at.mocode.veranstalter.feature.presentation - -import at.mocode.frontend.core.designsystem.models.LoginStatus - -class DefaultVeranstalterRepository : VeranstalterRepository { - override suspend fun list(): List { - // Aus Fake-Store lesen (Prototyp) - return FakeVeranstalterStore.all().map { it.toListItem() } - } -} - -private fun LoginStatus.asLabel(): String = when (this) { - LoginStatus.AKTIV -> "AKTIV" - LoginStatus.AUSSTEHEND -> "AUSSTEHEND" -} - -private fun VeranstalterUiModel.toListItem() = VeranstalterListItem( - id = id, - name = name, - oepsNummer = oepsNummer, - ort = ort, - loginStatus = loginStatus.asLabel(), -) +// Deprecated fake repository removed in favor of real Ktor-backed implementation. diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlScreen.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlScreen.kt index 89569436..9cb34f40 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlScreen.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlScreen.kt @@ -44,7 +44,8 @@ fun VeranstalterAuswahlScreen( onNeuerVeranstalter: () -> Unit = {}, ) { // MVVM + UDF: ViewModel hält gesamten Zustand, Composable rendert nur State und sendet Intents - val viewModel = remember { VeranstalterViewModel(DefaultVeranstalterRepository()) } + val repo: at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository = org.koin.compose.koinInject() + val viewModel = remember { VeranstalterViewModel(repo) } val state by viewModel.state.collectAsState() Column(modifier = Modifier.fillMaxSize()) { diff --git a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlV2.kt b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlV2.kt index f0be90cf..a7521128 100644 --- a/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlV2.kt +++ b/frontend/features/veranstalter-feature/src/jvmMain/kotlin/at/mocode/veranstalter/feature/presentation/VeranstalterAuswahlV2.kt @@ -7,6 +7,7 @@ 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.ArrowForward import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Close @@ -175,7 +176,7 @@ fun VeranstalterAuswahlV2( ) { Text("Weiter zur Turnier-Konfiguration") Spacer(Modifier.width(8.dp)) - Icon(Icons.Default.ArrowForward, null, modifier = Modifier.size(16.dp)) + Icon(Icons.AutoMirrored.Filled.ArrowForward, null, modifier = Modifier.size(16.dp)) } } }