Integrate Ktor HTTP clients and repositories for Veranstalter and Turnier features:
- Add `ApiRoutes` for central backend routing configuration. - Implement `DefaultVeranstalterRepository` and `DefaultTurnierRepository` with Ktor clients. - Add domain models (`Turnier`, `Bewerb`, `Abteilung`, `Veranstalter`) and respective repository interfaces. - Replace fake VeranstalterRepository with real implementation. - Update DI with `veranstalterModule` and HTTP client injection. - Simplify TokenProvider and update HttpClient setup (timeouts, retries, logging). - Mark roadmap tasks B-2 as partially complete.
This commit is contained in:
+15
@@ -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<List<Abteilung>>
|
||||
suspend fun getById(id: Long): Result<Abteilung>
|
||||
suspend fun create(model: Abteilung): Result<Abteilung>
|
||||
suspend fun update(id: Long, model: Abteilung): Result<Abteilung>
|
||||
suspend fun delete(id: Long): Result<Unit>
|
||||
}
|
||||
+15
@@ -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<List<Bewerb>>
|
||||
suspend fun getById(id: Long): Result<Bewerb>
|
||||
suspend fun create(model: Bewerb): Result<Bewerb>
|
||||
suspend fun update(id: Long, model: Bewerb): Result<Bewerb>
|
||||
suspend fun delete(id: Long): Result<Unit>
|
||||
}
|
||||
+14
@@ -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<List<Turnier>>
|
||||
suspend fun getById(id: Long): Result<Turnier>
|
||||
suspend fun create(model: Turnier): Result<Turnier>
|
||||
suspend fun update(id: Long, model: Turnier): Result<Turnier>
|
||||
suspend fun delete(id: Long): Result<Unit>
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -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,
|
||||
)
|
||||
+12
@@ -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,
|
||||
)
|
||||
+23
@@ -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<List<Veranstalter>>
|
||||
suspend fun getById(id: Long): Result<Veranstalter>
|
||||
suspend fun create(model: Veranstalter): Result<Veranstalter>
|
||||
suspend fun update(id: Long, model: Veranstalter): Result<Veranstalter>
|
||||
suspend fun delete(id: Long): Result<Unit>
|
||||
}
|
||||
+14
-9
@@ -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<VeranstalterListItem>
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
+79
@@ -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<List<Veranstalter>> = runCatching {
|
||||
val response = client.get(ApiRoutes.Veranstalter.ROOT)
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<List<VeranstalterDto>>().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<Veranstalter> = runCatching {
|
||||
val response = client.get("${ApiRoutes.Veranstalter.ROOT}/$id")
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<VeranstalterDto>().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<Veranstalter> = runCatching {
|
||||
val response = client.post(ApiRoutes.Veranstalter.ROOT) { setBody(model.toDto()) }
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<VeranstalterDto>().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<Veranstalter> = runCatching {
|
||||
val response = client.put("${ApiRoutes.Veranstalter.ROOT}/$id") { setBody(model.toDto()) }
|
||||
when {
|
||||
response.status.isSuccess() -> response.body<VeranstalterDto>().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<Unit> = 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")
|
||||
+11
@@ -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<VeranstalterRepository> { DefaultVeranstalterRepository(get(named("apiClient"))) }
|
||||
}
|
||||
+1
-23
@@ -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<VeranstalterListItem> {
|
||||
// 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.
|
||||
|
||||
+2
-1
@@ -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()) {
|
||||
|
||||
+2
-1
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user