Add Zeitplan fields to domain and DTO models, implement UpdateZeitplan intent and API integration, and update ViewModel for Zeitplan state consistency.

This commit is contained in:
2026-04-11 20:42:36 +02:00
parent 52bc8f3fbe
commit bc46054412
9 changed files with 108 additions and 25 deletions
@@ -18,7 +18,13 @@ fun BewerbDto.toDomain(): Bewerb = Bewerb(
name = name,
sparte = sparte,
klasse = klasse,
nennungen = nennungen
nennungen = nennungen,
geplantesDatum = geplantesDatum,
beginnZeit = beginnZeit,
reitdauerMinuten = reitdauerMinuten,
umbauMinuten = umbauMinuten,
besichtigungMinuten = besichtigungMinuten,
austragungsplatzId = austragungsplatzId,
)
fun Bewerb.toDto(): BewerbDto = BewerbDto(
@@ -29,7 +35,13 @@ fun Bewerb.toDto(): BewerbDto = BewerbDto(
name = name,
sparte = sparte,
klasse = klasse,
nennungen = nennungen
nennungen = nennungen,
geplantesDatum = geplantesDatum,
beginnZeit = beginnZeit,
reitdauerMinuten = reitdauerMinuten,
umbauMinuten = umbauMinuten,
besichtigungMinuten = besichtigungMinuten,
austragungsplatzId = austragungsplatzId,
)
fun AbteilungDto.toDomain(): Abteilung = Abteilung(id = id, bewerbId = bewerbId, name = name)
@@ -18,6 +18,12 @@ data class BewerbDto(
val sparte: String,
val klasse: String,
val nennungen: Int,
val geplantesDatum: String? = null,
val beginnZeit: String? = null,
val reitdauerMinuten: Int? = null,
val umbauMinuten: Int? = null,
val besichtigungMinuten: Int? = null,
val austragungsplatzId: String? = null,
)
@Serializable
@@ -10,6 +10,13 @@ data class Bewerb(
val klasse: String,
val nennungen: Int,
val warnungen: List<AbteilungsWarnung> = emptyList(),
// Zeitplan-Felder
val geplantesDatum: String? = null, // ISO-Format
val beginnZeit: String? = null, // "HH:mm"
val reitdauerMinuten: Int? = null,
val umbauMinuten: Int? = null,
val besichtigungMinuten: Int? = null,
val austragungsplatzId: String? = null,
)
data class AbteilungsWarnung(
@@ -23,6 +30,7 @@ interface BewerbRepository {
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 updateZeitplan(id: Long, datum: String?, beginn: String?, platzId: String?): Result<Bewerb>
suspend fun delete(id: Long): Result<Unit>
suspend fun importBewerbe(turnierId: Long, bewerbe: List<at.mocode.zns.parser.ZnsBewerb>): Result<Unit>
}
@@ -70,6 +70,7 @@ sealed interface BewerbIntent {
data object StartNetworkScan : BewerbIntent
data object StopNetworkScan : BewerbIntent
data object RefreshDiscoveredNodes : BewerbIntent
data class UpdateZeitplan(val id: Long, val beginnZeit: String?) : BewerbIntent
}
@@ -160,6 +161,15 @@ class BewerbViewModel(
is BewerbIntent.StartNetworkScan -> startScan()
is BewerbIntent.StopNetworkScan -> stopScan()
is BewerbIntent.RefreshDiscoveredNodes -> refreshNodes()
is BewerbIntent.UpdateZeitplan -> updateZeitplan(intent.id, intent.beginnZeit)
}
}
private fun updateZeitplan(id: Long, beginn: String?) {
scope.launch {
repo.updateZeitplan(id, null, beginn, null).onSuccess {
load() // Neu laden um Konsistenz zu prüfen
}
}
}
@@ -72,6 +72,23 @@ class DefaultBewerbRepository(
}
}
override suspend fun updateZeitplan(id: Long, datum: String?, beginn: String?, platzId: String?): Result<Bewerb> = runCatching {
val response = client.patch("${ApiRoutes.API_PREFIX}/bewerbe/$id/zeitplan") {
contentType(ContentType.Application.Json)
setBody(mapOf(
"geplantesDatum" to datum,
"beginnZeit" to beginn,
"austragungsplatzId" to platzId
))
}
when {
response.status.isSuccess() -> response.body<BewerbDto>().toDomain()
response.status == HttpStatusCode.NotFound -> throw NotFound()
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.API_PREFIX}/bewerbe/$id")
when {
@@ -12,6 +12,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.koin.compose.koinInject
import org.koin.core.parameter.parametersOf
/**
* Detailansicht eines Turniers gemäß Vision_03.
*
@@ -61,6 +64,8 @@ fun TurnierDetailScreen(
"ERGEBNISLISTEN",
)
val bewerbViewModel: BewerbViewModel = koinInject { parametersOf(turnierId) }
Column(modifier = Modifier.fillMaxSize()) {
// Horizontale Tab-Bar (direkt unter der TopBar)
PrimaryScrollableTabRow(
@@ -100,13 +105,11 @@ fun TurnierDetailScreen(
veranstalterLogoUrl = veranstalterLogoUrl,
)
1 -> OrganisationTabContent()
2 -> Box(modifier = Modifier.fillMaxSize()) {
Text("BEWERBE Tab (Anbindung in Arbeit)", modifier = Modifier.align(Alignment.Center))
}
2 -> BewerbeTabContent(viewModel = bewerbViewModel, turnierId = turnierId)
3 -> ArtikelTabContent()
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
5 -> NennungenTabContent(onAbrechnungClick = { selectedTab = 4 })
6 -> ZeitplanTabContent(turnierId = turnierId)
6 -> ZeitplanTabContent(turnierId = turnierId, viewModel = bewerbViewModel)
7 -> StartlistenTabContent()
8 -> ErgebnislistenTabContent()
}
@@ -38,9 +38,33 @@ private val MINUTE_HEIGHT = HOUR_HEIGHT / 60
*/
@Composable
fun ZeitplanTabContent(
turnierId: Long
turnierId: Long,
viewModel: BewerbViewModel
) {
var items by remember { mutableStateOf(sampleZeitplanItems()) }
val state by viewModel.state.collectAsState()
val items = state.filtered.map { bewerb ->
val startMin = if (bewerb.beginnZeit != null) {
val parts = bewerb.beginnZeit.split(":")
parts[0].toInt() * 60 + parts[1].toInt()
} else {
7 * 60 // Default 07:00 wenn nichts gesetzt
}
ZeitplanItemUi(
id = bewerb.id,
nummer = bewerb.tag.filter { it.isDigit() }.toIntOrNull() ?: 0,
name = bewerb.name,
startMinutes = startMin,
durationMinutes = bewerb.reitdauerMinuten ?: 60,
color = when (bewerb.sparte) {
"DRESSUR" -> Color(0xFF1E3A8A)
"SPRINGEN" -> Color(0xFF059669)
else -> ZeitplanBlue
},
hasConflict = bewerb.warnungen.isNotEmpty()
)
}
val scrollState = rememberScrollState()
Box(modifier = Modifier.fillMaxSize().background(ZeitplanBg)) {
@@ -63,13 +87,14 @@ fun ZeitplanTabContent(
ZeitplanGitter()
// Bewerbe / Blöcke
items.forEachIndexed { index, item ->
items.forEach { item ->
DraggableBewerbBox(
item = item,
onPositionChange = { newMinutes ->
items = items.toMutableList().apply {
this[index] = item.copy(startMinutes = newMinutes)
}
val h = newMinutes / 60
val m = newMinutes % 60
val timeStr = "${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}"
viewModel.send(BewerbIntent.UpdateZeitplan(item.id, timeStr))
}
)
}