chore: refactor Veranstalter screens and ViewModels to decouple from Koin, implement hoisting for better testability, consolidate legacy shell logic into modern components, and add mock repositories for previews
This commit is contained in:
+22
-189
@@ -1,22 +1,14 @@
|
||||
package at.mocode.frontend.shell.desktop.screens.management
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
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.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.shell.desktop.data.Store
|
||||
import at.mocode.frontend.shell.desktop.theme.DesktopTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
/**
|
||||
* Delegates für die Veranstalter-Screens in der Desktop-Shell.
|
||||
* Diese binden die Plug-and-Play Komponenten aus dem veranstalter-feature ein.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun VeranstalterAuswahl(
|
||||
@@ -24,55 +16,12 @@ fun VeranstalterAuswahl(
|
||||
onWeiter: (Long) -> Unit,
|
||||
onNeu: () -> Unit,
|
||||
) {
|
||||
DesktopTheme {
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text("Veranstalter auswählen", style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(onClick = onNeu) { Text("+ Neuer Veranstalter") }
|
||||
}
|
||||
|
||||
var selectedId by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize().weight(1f)) {
|
||||
items(Store.vereine) { v ->
|
||||
val sel = selectedId == v.id
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp)
|
||||
.clickable { selectedId = v.id },
|
||||
colors = if (sel) CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
else CardDefaults.cardColors()
|
||||
) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier.size(40.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text((v.kurzname ?: v.name).take(2).uppercase(), color = Color.White)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column {
|
||||
Text(v.name, style = MaterialTheme.typography.titleMedium)
|
||||
Text("OEPS: ${v.oepsNummer} · ${v.ort ?: ""}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { selectedId?.let(onWeiter) },
|
||||
enabled = selectedId != null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("Weiter zum Veranstalter") }
|
||||
}
|
||||
}
|
||||
VeranstalterAuswahlScreen(
|
||||
viewModel = koinInject(),
|
||||
onZurueck = onBack,
|
||||
onWeiter = onWeiter,
|
||||
onNeuerVeranstalter = onNeu
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -82,127 +31,11 @@ fun VeranstalterDetail(
|
||||
onZurVeranstaltung: (Long) -> Unit,
|
||||
onNeuVeranstaltung: () -> Unit,
|
||||
) {
|
||||
LaunchedEffect(veranstalterId) { println("[Screen] VeranstalterDetail geladen (VstID: $veranstalterId)") }
|
||||
DesktopTheme {
|
||||
val verein = remember(veranstalterId) { Store.vereine.firstOrNull { it.id == veranstalterId } }
|
||||
|
||||
if (verein == null) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Veranstalter nicht gefunden")
|
||||
}
|
||||
return@DesktopTheme
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Zurück",
|
||||
modifier = Modifier.clickable { onBack() })
|
||||
Text(verein.name, style = MaterialTheme.typography.titleLarge)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
|
||||
}
|
||||
|
||||
var editOpen by remember { mutableStateOf(false) }
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.fillMaxWidth().padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier.size(56.dp).background(Color(0xFF1F2937), shape = MaterialTheme.shapes.small),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
(verein.kurzname ?: verein.name).take(2).uppercase(),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(verein.name, style = MaterialTheme.typography.titleMedium)
|
||||
val line2 = listOfNotNull(
|
||||
"OEPS: ${verein.oepsNummer}",
|
||||
verein.ort,
|
||||
verein.plz,
|
||||
verein.strasse
|
||||
).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
if (line2.isNotBlank()) Text(line2, color = Color(0xFF6B7280))
|
||||
val line3 = listOfNotNull(verein.email, verein.telefon).filter { it.isNotBlank() }.joinToString(" · ")
|
||||
if (line3.isNotBlank()) Text(line3, color = Color(0xFF6B7280))
|
||||
}
|
||||
Button(onClick = { editOpen = true }) { Text("bearbeiten") }
|
||||
}
|
||||
}
|
||||
|
||||
if (editOpen) {
|
||||
var name by remember { mutableStateOf(verein.name) }
|
||||
var oeps by remember { mutableStateOf(verein.oepsNummer) }
|
||||
var ort by remember { mutableStateOf(verein.ort ?: "") }
|
||||
var plz by remember { mutableStateOf(verein.plz ?: "") }
|
||||
var strasse by remember { mutableStateOf(verein.strasse ?: "") }
|
||||
var email by remember { mutableStateOf(verein.email ?: "") }
|
||||
var tel by remember { mutableStateOf(verein.telefon ?: "") }
|
||||
var logo by remember { mutableStateOf(verein.logoUrl ?: "") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { editOpen = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
verein.name = name
|
||||
verein.oepsNummer = oeps
|
||||
verein.ort = ort.ifBlank { null }
|
||||
verein.plz = plz.ifBlank { null }
|
||||
verein.strasse = strasse.ifBlank { null }
|
||||
verein.email = email.ifBlank { null }
|
||||
verein.telefon = tel.ifBlank { null }
|
||||
verein.logoUrl = logo.ifBlank { null }
|
||||
editOpen = false
|
||||
}) { Text("Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { editOpen = false }) { Text("Abbrechen") } },
|
||||
title = { Text("Veranstalter bearbeiten") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(oeps, { oeps = it }, label = { Text("OEPS-Nummer") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(logo, { logo = it }, label = { Text("Logo-URL") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(ort, { ort = it }, label = { Text("Ort") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(plz, { plz = it }, label = { Text("PLZ") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
OutlinedTextField(
|
||||
strasse,
|
||||
{ strasse = it },
|
||||
label = { Text("Straße") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(email, { email = it }, label = { Text("E-Mail") }, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(tel, { tel = it }, label = { Text("Telefon") }, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text("Veranstaltungen", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 8.dp))
|
||||
val events = remember(veranstalterId) { Store.eventsFor(veranstalterId) }
|
||||
if (events.isEmpty()) {
|
||||
Text("Keine Veranstaltungen angelegt", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||
} else {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(events) { ev ->
|
||||
ListItem(
|
||||
headlineContent = { Text(ev.titel) },
|
||||
supportingContent = { Text("${ev.datumVon} bis ${ev.datumBis ?: "?"} · ${ev.status}") },
|
||||
trailingContent = { Icon(Icons.Default.ChevronRight, null) },
|
||||
modifier = Modifier.clickable { onZurVeranstaltung(ev.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = veranstalterId,
|
||||
viewModel = koinInject(),
|
||||
onZurueck = onBack,
|
||||
onVeranstaltungOeffnen = onZurVeranstaltung,
|
||||
onVeranstaltungNeu = onNeuVeranstaltung
|
||||
)
|
||||
}
|
||||
|
||||
+27
-3
@@ -7,11 +7,11 @@ import at.mocode.frontend.features.turnier.data.remote.dto.NennungEinreichenRequ
|
||||
import at.mocode.frontend.features.turnier.domain.*
|
||||
import at.mocode.frontend.features.turnier.domain.model.StartlistenZeile
|
||||
import at.mocode.frontend.features.turnier.presentation.*
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterAuswahlScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterDetailScreen
|
||||
import at.mocode.frontend.features.veranstalter.presentation.VeranstalterNeuScreen
|
||||
import at.mocode.frontend.features.veranstalter.domain.VeranstalterRepository
|
||||
import at.mocode.frontend.features.veranstalter.presentation.*
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungUebersichtScreen
|
||||
import at.mocode.zns.parser.ZnsBewerb
|
||||
import at.mocode.frontend.features.veranstalter.domain.Veranstalter as DomainVeranstalter
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Compose Desktop Previews – alle wichtigen Screens auf einen Blick
|
||||
@@ -25,8 +25,20 @@ import at.mocode.zns.parser.ZnsBewerb
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewVeranstalterAuswahlScreen() {
|
||||
val mockRepo = object : VeranstalterRepository {
|
||||
override suspend fun list(): Result<List<DomainVeranstalter>> = Result.success(emptyList())
|
||||
override suspend fun getById(id: Long): Result<DomainVeranstalter> = Result.failure(NotImplementedError())
|
||||
override suspend fun create(model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun update(id: Long, model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun delete(id: Long): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
MaterialTheme {
|
||||
VeranstalterAuswahlScreen(
|
||||
viewModel = VeranstalterViewModel(mockRepo),
|
||||
onZurueck = {},
|
||||
onWeiter = {},
|
||||
onNeuerVeranstalter = {},
|
||||
@@ -52,9 +64,21 @@ fun PreviewVeranstalterNeuScreen() {
|
||||
@ComponentPreview
|
||||
@Composable
|
||||
fun PreviewVeranstalterDetailScreen() {
|
||||
val mockRepo = object : VeranstalterRepository {
|
||||
override suspend fun list(): Result<List<DomainVeranstalter>> = Result.success(emptyList())
|
||||
override suspend fun getById(id: Long): Result<DomainVeranstalter> = Result.failure(NotImplementedError())
|
||||
override suspend fun create(model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun update(id: Long, model: DomainVeranstalter): Result<DomainVeranstalter> =
|
||||
Result.failure(NotImplementedError())
|
||||
|
||||
override suspend fun delete(id: Long): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
MaterialTheme {
|
||||
VeranstalterDetailScreen(
|
||||
veranstalterId = 1L,
|
||||
viewModel = VeranstalterDetailViewModel(mockRepo),
|
||||
onZurueck = {},
|
||||
onVeranstaltungOeffnen = {},
|
||||
onVeranstaltungNeu = {},
|
||||
|
||||
Reference in New Issue
Block a user