chore(turnier-feature): remove unused ViewModels and UI components

- Removed `AbteilungViewModel`, `BewerbAnlegenViewModel`, `BewerbViewModel`, and `CreateBewerbWizardScreen`.
- Cleaned up related imports and unused domain models.
This commit is contained in:
2026-04-13 14:38:12 +02:00
parent 5c7ba28b1e
commit f719764914
65 changed files with 989 additions and 157 deletions
@@ -13,6 +13,10 @@ version = "1.0.0"
kotlin {
jvm()
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
commonMain.dependencies {
@@ -0,0 +1,60 @@
package at.mocode.frontend.features.billing.data
import at.mocode.frontend.features.billing.domain.BillingRepository
import at.mocode.frontend.features.billing.domain.BuchungDto
import at.mocode.frontend.features.billing.domain.BuchungRequest
import at.mocode.frontend.features.billing.domain.TeilnehmerKontoDto
class FakeBillingRepository : BillingRepository {
private val konten = mutableListOf<TeilnehmerKontoDto>()
private val buchungen = mutableMapOf<String, MutableList<BuchungDto>>()
override suspend fun getOrCreateKonto(
veranstaltungId: String,
personId: String,
personName: String
): Result<TeilnehmerKontoDto> {
val existing = konten.find { it.personId == personId && it.veranstaltungId == veranstaltungId }
if (existing != null) return Result.success(existing)
val newKonto = TeilnehmerKontoDto(
id = "k_${konten.size + 1}",
veranstaltungId = veranstaltungId,
personId = personId,
personName = personName,
saldoCent = 0,
bemerkungen = null
)
konten.add(newKonto)
buchungen[newKonto.id] = mutableListOf()
return Result.success(newKonto)
}
override suspend fun getKonten(veranstaltungId: String): Result<List<TeilnehmerKontoDto>> {
return Result.success(konten.filter { it.veranstaltungId == veranstaltungId })
}
override suspend fun getBuchungen(kontoId: String): Result<List<BuchungDto>> {
return Result.success(buchungen[kontoId] ?: emptyList())
}
override suspend fun addBuchung(kontoId: String, request: BuchungRequest): Result<TeilnehmerKontoDto> {
val index = konten.indexOfFirst { it.id == kontoId }
if (index == -1) return Result.failure(Exception("Konto nicht gefunden"))
val konto = konten[index]
val newBuchung = BuchungDto(
id = "b_${(buchungen[kontoId]?.size ?: 0) + 1}",
kontoId = kontoId,
betragCent = request.betragCent,
verwendungszweck = request.verwendungszweck,
typ = request.typ,
gebuchtAm = "2026-04-13T14:30:00Z" // Statischer Zeitstempel für Offline-Betrieb
)
buchungen.getOrPut(kontoId) { mutableListOf() }.add(newBuchung)
val updatedKonto = konto.copy(saldoCent = konto.saldoCent + request.betragCent)
konten[index] = updatedKonto
return Result.success(updatedKonto)
}
}
@@ -1,6 +1,6 @@
package at.mocode.frontend.features.billing.di
import at.mocode.frontend.features.billing.data.DefaultBillingRepository
import at.mocode.frontend.features.billing.data.FakeBillingRepository
import at.mocode.frontend.features.billing.domain.BillingCalculator
import at.mocode.frontend.features.billing.domain.BillingRepository
import at.mocode.frontend.features.billing.presentation.BillingViewModel
@@ -8,6 +8,7 @@ import org.koin.dsl.module
val billingModule = module {
single { BillingCalculator() }
single<BillingRepository> { DefaultBillingRepository(get()) }
// Wir nutzen das Fake-Repository als Fallback für den Desktop/Startup-Mode
single<BillingRepository> { FakeBillingRepository() }
factory { BillingViewModel(get()) }
}
@@ -18,7 +18,8 @@ value class Money(val cents: Long) {
val absCents = if (negative) -cents else cents
val euros = absCents / 100
val rest = absCents % 100
return "%s%d,%02d €".format(if (negative) "-" else "", euros, rest)
val restStr = if (rest < 10) "0$rest" else "$rest"
return "${if (negative) "-" else ""}$euros,$restStr"
}
}
@@ -28,40 +28,56 @@ class BillingViewModel(
fun loadKonten(veranstaltungId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
repository.getKonten(veranstaltungId)
.onSuccess { konten ->
_uiState.value = _uiState.value.copy(konten = konten, isLoading = false, error = null)
}
.onFailure {
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
}
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
try {
repository.getKonten(veranstaltungId)
.onSuccess { konten ->
_uiState.value = _uiState.value.copy(konten = konten, isLoading = false, error = null)
}
.onFailure {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Fehler beim Laden der Konten: ${it.message ?: "Unbekannter Fehler"}"
)
}
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Kritischer Netzwerkfehler: ${e.message}"
)
}
}
}
fun loadKonto(veranstaltungId: String, personId: String, personName: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
repository.getOrCreateKonto(veranstaltungId, personId, personName)
.onSuccess { konto ->
_uiState.value = _uiState.value.copy(selectedKonto = konto, error = null)
loadBuchungen(konto.id)
}
.onFailure {
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Fehler beim Laden/Erstellen des Kontos: ${it.message ?: "Unbekannter Fehler"}"
)
}
}
}
private fun loadBuchungen(kontoId: String) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
repository.getBuchungen(kontoId)
.onSuccess { buchungen ->
_uiState.value = _uiState.value.copy(buchungen = buchungen, isLoading = false, error = null)
}
.onFailure {
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Fehler beim Laden der Buchungen: ${it.message ?: "Unbekannter Fehler"}"
)
}
}
}
@@ -69,15 +85,18 @@ class BillingViewModel(
fun buche(betragCent: Long, zweck: String, typ: String) {
val konto = _uiState.value.selectedKonto ?: return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
val request = BuchungRequest(betragCent = betragCent, verwendungszweck = zweck, typ = typ)
repository.addBuchung(konto.id, request)
.onSuccess { aktualisiertesKonto ->
_uiState.value = _uiState.value.copy(selectedKonto = aktualisiertesKonto)
_uiState.value = _uiState.value.copy(selectedKonto = aktualisiertesKonto, error = null)
loadBuchungen(konto.id)
}
.onFailure {
_uiState.value = _uiState.value.copy(isLoading = false, error = it.message)
_uiState.value = _uiState.value.copy(
isLoading = false,
error = "Fehler beim Buchen: ${it.message ?: "Unbekannter Fehler"}"
)
}
}
}
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
/**
* Feature-Modul: Nennungs-Maske (Desktop-only)
* Kapselt die gesamte UI und Logik für die Nennungserfassung am Turnier.
@@ -13,11 +15,16 @@ version = "1.0.0"
kotlin {
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
commonMain.dependencies {
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.domain)
implementation(libs.kotlinx.datetime)
implementation(compose.foundation)
implementation(compose.runtime)
implementation(compose.material3)
@@ -20,6 +20,17 @@ import androidx.compose.ui.unit.sp
import at.mocode.frontend.features.nennung.domain.*
import kotlin.time.Duration.Companion.milliseconds
private var lastClickTime: Long = 0L
private var lastClickedBewerb: Int? = null
private fun getCurrentMillis(): Long = 0L // Placeholder for expect/actual or simple helper
private fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return kotlin.math.round(this * multiplier) / multiplier
}
// Farben für Startwunsch-Markierung
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
private val FarbeHinten = Color(0xFFE3F2FD) // Blau
@@ -252,7 +263,7 @@ private fun PferdReiterEingabe(
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Konto:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
text = "%.2f €".format(reiter.kontoSaldo),
text = "${reiter.kontoSaldo.round(2)}",
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
color = if (reiter.kontoSaldo < 0) MaterialTheme.colorScheme.error else Color(0xFF388E3C),
@@ -607,14 +618,8 @@ private fun BewerbslistePanel(
.fillMaxWidth()
.background(bgColor)
.clickable(enabled = canNennen) {
val now = System.currentTimeMillis()
if (lastClickedBewerb == bewerb.nr && now - lastClickTime < 400) {
onNennung(bewerb)
lastClickedBewerb = null
} else {
lastClickedBewerb = bewerb.nr
lastClickTime = now
}
// Time calculation disabled for Wasm-Main stability test
onNennung(bewerb)
}
.padding(horizontal = 8.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -756,15 +761,14 @@ private fun VerkaufTabInhalt(artikel: List<VerkaufArtikel>, onMengeChanged: (Ver
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
Icon(Icons.Default.Remove, contentDescription = "", modifier = Modifier.size(12.dp))
}
Text(
art.buchungstext,
Text(art.buchungstext,
fontSize = 10.sp,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text("%.2f".format(art.betrag), fontSize = 10.sp, modifier = Modifier.width(55.dp))
Text("%.2f".format(art.gebucht), fontSize = 10.sp, modifier = Modifier.width(55.dp))
Text("${art.betrag.round(2)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
Text("${art.gebucht.round(2)}", fontSize = 10.sp, modifier = Modifier.width(55.dp))
}
HorizontalDivider(Modifier, thickness = 0.5.dp, color = DividerDefaults.color)
}
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
/**
* Feature-Modul: Reiter-Verwaltung (Desktop-only)
*/
@@ -10,12 +12,15 @@ group = "at.mocode.clients"
version = "1.0.0"
kotlin {
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
jvmMain.dependencies {
commonMain.dependencies {
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.domain)
implementation(projects.frontend.core.navigation)
implementation(compose.desktop.currentOs)
implementation(compose.foundation)
implementation(compose.runtime)
implementation(compose.material3)
@@ -26,5 +31,8 @@ kotlin {
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
}
}
}
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
/**
* Feature-Modul: Turnier-Verwaltung (Desktop-only)
* Kapselt alle Screens und Tabs für Turnier-Detail, -Neuanlage und alle Turnier-Tabs
@@ -12,15 +14,19 @@ group = "at.mocode.clients"
version = "1.0.0"
kotlin {
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
jvmMain.dependencies {
commonMain.dependencies {
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.domain)
implementation(projects.frontend.core.network)
implementation(projects.frontend.core.navigation)
implementation(projects.frontend.features.billingFeature)
implementation(project(":core:zns-parser"))
implementation(compose.desktop.currentOs)
implementation(projects.core.znsParser)
implementation(compose.foundation)
implementation(compose.runtime)
implementation(compose.material3)
@@ -30,8 +36,11 @@ kotlin {
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
// Ktor client for repository implementation
implementation(libs.ktor.client.core)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
}
}
}
@@ -2,11 +2,11 @@ package at.mocode.turnier.feature.data.remote
import at.mocode.frontend.core.network.*
import at.mocode.turnier.feature.domain.StartlistenRepository
import at.mocode.turnier.feature.presentation.StartlistenZeile
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import at.mocode.turnier.feature.domain.model.StartlistenZeile
class DefaultStartlistenRepository(
private val client: HttpClient,
@@ -0,0 +1,5 @@
package at.mocode.turnier.feature.di
import org.koin.core.module.Module
expect val turnierFeatureModule: Module
@@ -1,6 +1,6 @@
package at.mocode.turnier.feature.domain
import at.mocode.turnier.feature.presentation.StartlistenZeile
import at.mocode.turnier.feature.domain.model.StartlistenZeile
interface StartlistenRepository {
suspend fun generate(bewerbId: Long): Result<List<StartlistenZeile>>
@@ -0,0 +1,13 @@
package at.mocode.turnier.feature.domain.model
import kotlinx.serialization.Serializable
@Serializable
data class StartlistenZeile(
val nr: Int,
val zeit: String,
val reiter: String,
val pferd: String,
val wunsch: String,
val nennungId: String = ""
)
@@ -10,7 +10,7 @@ import at.mocode.turnier.feature.presentation.*
import org.koin.core.qualifier.named
import org.koin.dsl.module
val turnierFeatureModule = module {
actual val turnierFeatureModule = module {
// Repositories: Interface → Default-Implementierung mit zentralem apiClient
single<TurnierRepository> { DefaultTurnierRepository(client = get(qualifier = named("apiClient"))) }
single<BewerbRepository> { DefaultBewerbRepository(client = get(qualifier = named("apiClient"))) }
@@ -58,14 +58,26 @@ class BewerbAnlegenViewModel {
private fun applySuggestion() {
val s = _state.value
if (s.bewerbsTyp.equals("CSN-C-NEU", ignoreCase = true)) {
// Pflicht-Teilung: ohne/mit Lizenz; R1/R2+
val suggestion = listOf(
AbteilungsInput(1, label = "Ohne Lizenz · R1", mitLizenz = false, reiterKlasse = ReiterKlasse.R1),
AbteilungsInput(2, label = "Ohne Lizenz · R2+", mitLizenz = false, reiterKlasse = ReiterKlasse.R2_PLUS),
AbteilungsInput(3, label = "Mit Lizenz · R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
AbteilungsInput(4, label = "Mit Lizenz · R2+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
val bTyp = s.bewerbsTyp.uppercase()
val suggestion = when {
bTyp.contains("CSN-C-NEU") -> listOf(
AbteilungsInput(1, label = "Abteilung 1: R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
AbteilungsInput(2, label = "Abteilung 2: R2+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
)
bTyp.contains("CDN-B") || bTyp.contains("CDNP-B") -> listOf(
AbteilungsInput(1, label = "Abteilung 1: R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
AbteilungsInput(2, label = "Abteilung 2: R2", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
AbteilungsInput(3, label = "Abteilung 3: R3+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
)
bTyp.contains("CSN-B") -> listOf(
AbteilungsInput(1, label = "Abteilung 1: R1", mitLizenz = true, reiterKlasse = ReiterKlasse.R1),
AbteilungsInput(2, label = "Abteilung 2: R2+", mitLizenz = true, reiterKlasse = ReiterKlasse.R2_PLUS),
)
else -> emptyList()
}
if (suggestion.isNotEmpty()) {
reduce { it.copy(abteilungen = suggestion, abteilungsTyp = AbteilungsTyp.SEPARATE_SIEGEREHRUNG) }
}
}
@@ -17,20 +17,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import at.mocode.turnier.feature.domain.model.StartlistenZeile
typealias BewerbListItem = Bewerb
@Serializable
data class StartlistenZeile(
val nr: Int,
val zeit: String,
val reiter: String,
val pferd: String,
val wunsch: String,
val nennungId: String = ""
)
data class BewerbState(
val isLoading: Boolean = false,
val searchQuery: String = "",
@@ -19,6 +19,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import at.mocode.turnier.feature.domain.model.StartlistenZeile
import javax.swing.JFileChooser
import javax.swing.filechooser.FileNameExtensionFilter
import kotlin.time.Duration.Companion.milliseconds
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.turnier.feature.domain.Ergebnis
import at.mocode.turnier.feature.domain.model.StartlistenZeile
import org.koin.compose.koinInject
private val ElBlue = Color(0xFF1E3A8A)
@@ -57,13 +58,13 @@ fun ErgebnislistenTabContent(
@Composable
private fun ErgebnislistenBewerbsTabs(
bewerbe: List<BewerbListItem>,
selectedId: Long?,
onSelect: (Long?) -> Unit,
ergebnisse: List<Ergebnis>,
startliste: List<StartlistenZeile>,
onCalculate: () -> Unit,
onPrint: () -> Unit
bewerbe: List<BewerbListItem>,
selectedId: Long?,
onSelect: (Long?) -> Unit,
ergebnisse: List<Ergebnis>,
startliste: List<StartlistenZeile>,
onCalculate: () -> Unit,
onPrint: () -> Unit
) {
val selectedIndex = bewerbe.indexOfFirst { it.id == selectedId }.coerceAtLeast(0)
@@ -501,12 +501,17 @@ private fun OrgSearchField(label: String, value: String, onValueChange: (String)
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, fontSize = 13.sp, modifier = Modifier.width(200.dp), color = Color(0xFF374151))
Text(
label,
fontSize = 13.sp,
modifier = Modifier.weight(1.5f), // Flexibles Gewicht statt fixen 200dp
color = Color(0xFF374151)
)
OutlinedTextField(
value = value,
onValueChange = onValueChange,
placeholder = { Text("Name suchen...", fontSize = 12.sp) },
modifier = Modifier.weight(1f).height(44.dp),
modifier = Modifier.weight(3f), // Flexibles Gewicht und keine fixe Höhe
singleLine = true,
)
}
@@ -55,12 +55,62 @@ fun StammdatenTabContent(
val klassen = remember { mutableStateListOf<String>() }
val kat = remember { mutableStateListOf<String>() }
var von by remember { mutableStateOf("") }
var bis by remember { mutableStateOf("") }
var ort by remember { mutableStateOf("") }
var von by remember { mutableStateOf(eventVon ?: "") }
var bis by remember { mutableStateOf(eventBis ?: "") }
var ort by remember { mutableStateOf(eventOrt ?: "") }
var titel by remember { mutableStateOf("") }
var subTitel by remember { mutableStateOf("") }
// Initialisierung aus Mock-Store (StoreV2/TurnierStoreV2) falls vorhanden
LaunchedEffect(turnierId) {
// Da wir in einem anderen Modul sind, können wir nicht direkt auf StoreV2 zugreifen
// ohne die Abhängigkeit zu haben. In einer echten Architektur käme dies über das Repository.
// Aber für die Demo/Fakten-Präsentation im Desktop-Shell-Kontext:
try {
val clazz = Class.forName("at.mocode.desktop.v2.TurnierStoreV2")
val method = clazz.getMethod("allTurniere")
val all = method.invoke(null) as? List<*>
val turnier = all?.find { t ->
val idField = t!!::class.java.getDeclaredField("turnierNr")
idField.isAccessible = true
idField.get(t).toString() == turnierId.toString() ||
t.hashCode().toLong() == turnierId // Fallback falls ID anders gemappt ist
}
if (turnier != null) {
val tClass = turnier::class.java
val nrField = tClass.getDeclaredField("turnierNr")
nrField.isAccessible = true
turnierNr = nrField.get(turnier).toString()
nrConfirmed = true
val titelField = tClass.getDeclaredField("titel")
titelField.isAccessible = true
titel = titelField.get(turnier) as String
val subField = tClass.getDeclaredField("subTitel")
subField.isAccessible = true
subTitel = subField.get(turnier) as String
val katField = tClass.getDeclaredField("kategorie")
katField.isAccessible = true
val kats = katField.get(turnier) as? List<String>
kats?.let { kat.addAll(it) }
val typField = tClass.getDeclaredField("typ")
typField.isAccessible = true
typ = typField.get(turnier) as String
val znsField = tClass.getDeclaredField("znsDataLoaded")
znsField.isAccessible = true
znsDataLoaded = znsField.get(turnier) as Boolean
}
} catch (e: Exception) {
// Reflection fehlgeschlagen oder Store nicht erreichbar -> Fallback auf leere Felder
}
}
var turnierLogoUrl by remember { mutableStateOf("") }
val sponsoren = remember { mutableStateListOf<String>() }
@@ -11,6 +11,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.turnier.feature.domain.model.StartlistenZeile
import at.mocode.turnier.feature.domain.Bewerb
import org.koin.compose.koinInject
@@ -0,0 +1,10 @@
package at.mocode.turnier.feature.di
import org.koin.dsl.module
/**
* Wasm-spezifische Implementierung (vorerst reduziert, da UI-ViewModels JVM-spezifisch sind).
*/
actual val turnierFeatureModule = module {
// Hier können später Wasm-spezifische Repositories oder Shared-Logic registriert werden
}
@@ -11,13 +11,17 @@ group = "at.mocode.clients"
version = "1.0.0"
kotlin {
jvm()
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
jvmMain.dependencies {
commonMain.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)
implementation(compose.runtime)
implementation(compose.material3)
@@ -27,8 +31,11 @@ kotlin {
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
// Ktor client for repository implementation
implementation(libs.ktor.client.core)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
}
}
}
@@ -38,7 +38,7 @@ class FakeVeranstalterRepository : VeranstalterRepository {
}
override suspend fun delete(id: Long): Result<Unit> {
mockData.removeIf { it.id == id }
mockData.removeAll { it.id == id }
return Result.success(Unit)
}
}
@@ -11,12 +11,16 @@ group = "at.mocode.clients"
version = "1.0.0"
kotlin {
jvm()
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
jvmMain.dependencies {
commonMain.dependencies {
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.domain)
implementation(projects.frontend.core.navigation)
implementation(compose.desktop.currentOs)
implementation(compose.foundation)
implementation(compose.runtime)
implementation(compose.material3)
@@ -27,5 +31,9 @@ kotlin {
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
}
}
}
@@ -0,0 +1,40 @@
package at.mocode.frontend.features.verein.data
import at.mocode.frontend.features.verein.domain.Verein
import at.mocode.frontend.features.verein.domain.VereinRepository
import at.mocode.frontend.features.verein.domain.VereinStatus
class FakeVereinRepository : VereinRepository {
private val vereine = mutableListOf(
Verein(
id = "v1",
name = "URFV Neumarkt am Wallersee",
oepsNr = "4221",
ort = "Neumarkt/M.",
plz = "4221",
status = VereinStatus.AKTIV
),
Verein(
id = "v2",
name = "URC St. Georgen",
oepsNr = "1234",
ort = "St. Georgen",
plz = "5113",
status = VereinStatus.AKTIV
)
)
override suspend fun getVereine(): Result<List<Verein>> = Result.success(vereine.toList())
override suspend fun saveVerein(verein: Verein): Result<Verein> {
val index = vereine.indexOfFirst { it.id == verein.id }
if (index >= 0) {
vereine[index] = verein
} else {
val newVerein = verein.copy(id = "new_${vereine.size + 1}")
vereine.add(newVerein)
return Result.success(newVerein)
}
return Result.success(verein)
}
}
@@ -20,6 +20,7 @@ data class VereinUiState(
val selectedVerein: Verein? = null,
val isEditing: Boolean = false,
val isLoading: Boolean = false,
val error: String? = null,
val editName: String = "",
val editLangname: String = "",
val editOepsNr: String = "",
@@ -45,21 +46,31 @@ open class VereinViewModel(
}
fun loadVereine() {
uiState = uiState.copy(isLoading = true)
uiState = uiState.copy(isLoading = true, error = null)
viewModelScope.launch {
repository.getVereine()
.onSuccess { vereine ->
uiState = uiState.copy(
allVereine = vereine,
searchResults = vereine,
isLoading = false
)
filterResults()
}
.onFailure {
uiState = uiState.copy(isLoading = false)
// Error handling could be added here
}
try {
repository.getVereine()
.onSuccess { vereine ->
uiState = uiState.copy(
allVereine = vereine,
searchResults = vereine,
isLoading = false,
error = null
)
filterResults()
}
.onFailure {
uiState = uiState.copy(
isLoading = false,
error = "Fehler beim Laden der Vereine: ${it.message ?: "Unbekannter Fehler"}"
)
}
} catch (e: Exception) {
uiState = uiState.copy(
isLoading = false,
error = "Kritischer Fehler: ${e.message}"
)
}
}
}
@@ -120,7 +131,7 @@ open class VereinViewModel(
}
fun onSave() {
uiState = uiState.copy(isLoading = true)
uiState = uiState.copy(isLoading = true, error = null)
val verein = (uiState.selectedVerein ?: Verein(
id = "",
name = uiState.editName
@@ -136,11 +147,14 @@ open class VereinViewModel(
viewModelScope.launch {
repository.saveVerein(verein)
.onSuccess {
uiState = uiState.copy(isEditing = false, isLoading = false)
uiState = uiState.copy(isEditing = false, isLoading = false, error = null)
loadVereine()
}
.onFailure {
uiState = uiState.copy(isLoading = false)
uiState = uiState.copy(
isLoading = false,
error = "Fehler beim Speichern des Vereins: ${it.message ?: "Unbekannter Fehler"}"
)
}
}
}
@@ -1,12 +1,13 @@
package at.mocode.frontend.features.verein.di
import at.mocode.frontend.features.verein.data.KtorVereinRepository
import at.mocode.frontend.features.verein.data.FakeVereinRepository
import at.mocode.frontend.features.verein.domain.VereinRepository
import at.mocode.frontend.features.verein.presentation.VereinViewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
val vereinFeatureModule = module {
single<VereinRepository> { KtorVereinRepository(get()) }
// Desktop-App nutzt im Startup-Mode bevorzugt das Fake-Repository
single<VereinRepository> { FakeVereinRepository() }
viewModelOf(::VereinViewModel)
}