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:
parent
52bc8f3fbe
commit
bc46054412
|
|
@ -3,26 +3,27 @@
|
|||
## 📅 Session Info
|
||||
- **Datum:** 2026-04-11
|
||||
- **Agenten:** 🏗️ Lead Architect, 👷 Backend Developer, 🎨 Frontend Expert, 🧹 Curator
|
||||
- **Fokus:** Implementierung Zeitplan-Optimierung (Frontend Prototyp)
|
||||
- **Fokus:** Integration Zeitplan-Optimierung & Datenanbindung
|
||||
|
||||
## 🏗️ Architektur-Entscheidungen
|
||||
- **Komponente:** `TurnierZeitplanTab.kt` wurde als zentraler Ort für die visuelle Zeitplanung geschaffen.
|
||||
- **Layout:** 14-Stunden-Raster (07:00 - 21:00) mit feststehender Zeitachse und scrollbarem Gitter.
|
||||
- **Interaktion:** Vertikales Drag & Drop mit 5-Minuten-Snapping zur präzisen Planung.
|
||||
- **Datenfluss:** `TurnierZeitplanTab.kt` wurde erfolgreich an das `BewerbViewModel` angebunden.
|
||||
- **DI:** Das `BewerbViewModel` wird nun zentral im `TurnierDetailScreen` via Koin injiziert und an die Tabs (Bewerbe & Zeitplan) verteilt, um State-Konsistenz zu gewährleisten.
|
||||
- **Domäne:** Das Domänenmodell `Bewerb` im Frontend wurde um Zeitplan-Felder (`beginnZeit`, `geplantesDatum`, etc.) erweitert, um das Mapping zum Backend zu vervollständigen.
|
||||
|
||||
## 👷 Backend/Integration (Status)
|
||||
- **Backend:** Logik in `BewerbService` bereits vorhanden (vorherige Sessions).
|
||||
- **Frontend-Store:** Aktuell noch Mock-Daten (`ZeitplanItemUi`), Anbindung an `BewerbViewModel` steht noch aus.
|
||||
## 👷 Backend/Integration
|
||||
- **API:** Unterstützung für `PATCH /bewerbe/{id}/zeitplan` im `DefaultBewerbRepository` implementiert.
|
||||
- **ViewModel:** Neuer Intent `BewerbIntent.UpdateZeitplan` zur persistierung von Zeitänderungen.
|
||||
|
||||
## 🎨 Frontend (Details)
|
||||
- **Modul:** `frontend:features:turnier-feature`
|
||||
- **Datei:** `TurnierZeitplanTab.kt` (Neu)
|
||||
- **Anpassung:** `TurnierDetailScreen.kt` um Tab "ZEITPLAN" erweitert.
|
||||
- **Mapping:** Automatisches Mapping von `Bewerb` (Domain) auf `ZeitplanItemUi` (Visual) inkl. dynamischer Farbwahl nach Sparte.
|
||||
- **Interaktion:** Drag & Drop Änderungen triggern nun echte API-Calls und laden den State neu.
|
||||
- **UI:** Integration des "Bewerbe"-Tabs im `TurnierDetailScreen` vervollständigt (war vorher ein Platzhalter).
|
||||
|
||||
## 🧹 Curator Status & Cleanup
|
||||
- ✅ Neue UI-Komponente erstellt und syntaktisch korrigiert.
|
||||
- ✅ Navigation im Turnier-Detail angepasst.
|
||||
- 📂 Nächster Schritt: Mapping von `BewerbUiModel` auf `ZeitplanItemUi`.
|
||||
- ✅ Datenmodelle und Mapper erweitert.
|
||||
- ✅ Repository-Anbindung vervollständigt.
|
||||
- ✅ ViewModel-Integration im UI-Layer abgeschlossen.
|
||||
- 📂 Nächster Schritt: Implementierung der automatischen Konfliktprüfung im Zeitplan (Rulebook-Validierung).
|
||||
|
||||
---
|
||||
*Erstellt durch den Curator.*
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ fun PreviewTurnierBewerbeTab() {
|
|||
override suspend fun create(model: Bewerb): Result<Bewerb> = Result.failure(NotImplementedError())
|
||||
override suspend fun update(id: Long, model: Bewerb): Result<Bewerb> = Result.failure(NotImplementedError())
|
||||
override suspend fun delete(id: Long): Result<Unit> = Result.success(Unit)
|
||||
override suspend fun updateZeitplan(id: Long, datum: String?, beginn: String?, platzId: String?): Result<Bewerb> = Result.failure(NotImplementedError())
|
||||
override suspend fun importBewerbe(turnierId: Long, bewerbe: List<ZnsBewerb>): Result<Unit> = Result.success(Unit)
|
||||
}
|
||||
val mockStartlistenRepo = object : StartlistenRepository {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user