diff --git a/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/CreateBewerbWizardScreen.kt b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/CreateBewerbWizardScreen.kt new file mode 100644 index 00000000..40412432 --- /dev/null +++ b/frontend/features/turnier-feature/src/commonMain/kotlin/at/mocode/turnier/feature/presentation/CreateBewerbWizardScreen.kt @@ -0,0 +1,321 @@ +package at.mocode.turnier.feature.presentation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.turnier.feature.data.remote.CreateBewerbPayload +import at.mocode.turnier.feature.data.remote.RichterEinsatzDto +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime + +enum class WizardStep { IDENTIFIKATION, DETAILS_FINANZEN, ORT_ZEIT, RICHTER_TEILUNG } + +data class CreateBewerbWizardState( + // Step 1 + val klasse: String = "", + val hoeheCm: String = "", // UI-Text, wird zu Int? geparst + val bezeichnung: String = "", + + // Step 2 + val beschreibung: String = "", + val aufgabe: String = "", + val startgeld: String = "", // UI-Text, wird zu Long? Cent + val geldpreisAusbezahlt: Boolean = false, + + // Step 3 + val austragungsplatzId: String = "", + val beginnZeitTyp: String = "", // FIX / ANSCHLIESSEND + val geplantesDatum: String = "", // yyyy-MM-dd + val beginnZeit: String = "", // HH:mm + val reitdauerMinuten: String = "", + val umbauMinuten: String = "", + val besichtigungMinuten: String = "", + val stechenGeplant: Boolean = false, + + // Step 4 + val richter: List = emptyList(), + val teilungsTyp: String = "", // Hinweis: aktuell nur UI; Backend-Feld folgt separat +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreateBewerbWizardScreen( + modifier: Modifier = Modifier, + state: CreateBewerbWizardState, + onStateChange: (CreateBewerbWizardState) -> Unit, + onSubmit: (CreateBewerbPayload) -> Unit, +) { + var selectedTab by remember { mutableStateOf(0) } + val steps = WizardStep.values() + + Column(modifier.fillMaxSize().padding(16.dp)) { + Text("Neuen Bewerb anlegen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Spacer(Modifier.height(8.dp)) + + TabRow(selectedTabIndex = selectedTab) { + Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }, text = { Text("Identifikation") }) + Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }, text = { Text("Details & Finanzen") }) + Tab(selected = selectedTab == 2, onClick = { selectedTab = 2 }, text = { Text("Ort & Zeitplan") }) + Tab(selected = selectedTab == 3, onClick = { selectedTab = 3 }, text = { Text("Richter & Teilung") }) + } + + Divider(Modifier.padding(vertical = 8.dp)) + + when (steps[selectedTab]) { + WizardStep.IDENTIFIKATION -> StepIdentifikation(state, onStateChange) + WizardStep.DETAILS_FINANZEN -> StepDetailsFinanzen(state, onStateChange) + WizardStep.ORT_ZEIT -> StepOrtZeit(state, onStateChange) + WizardStep.RICHTER_TEILUNG -> StepRichterTeilung(state, onStateChange) + } + + Spacer(Modifier.height(16.dp)) + + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + TextButton(enabled = selectedTab > 0, onClick = { selectedTab-- }) { Text("Zurück") } + Spacer(Modifier.weight(1f)) + if (selectedTab < steps.lastIndex) { + TextButton(onClick = { selectedTab++ }) { Text("Weiter") } + } else { + OutlinedButton(onClick = { + val payload = state.toPayloadOrNull() + if (payload != null) onSubmit(payload) + }) { Text("Bewerb anlegen") } + } + } + } +} + +@Composable +private fun StepIdentifikation(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) { + LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + item { + OutlinedTextField( + value = state.klasse, + onValueChange = { onStateChange(state.copy(klasse = it)) }, + label = { Text("Sparte/Kategorie/Klasse") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + OutlinedTextField( + value = state.hoeheCm, + onValueChange = { onStateChange(state.copy(hoeheCm = it.filter { ch -> ch.isDigit() })) }, + label = { Text("Höhe (cm)") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + OutlinedTextField( + value = state.bezeichnung, + onValueChange = { onStateChange(state.copy(bezeichnung = it)) }, + label = { Text("Bezeichnung") }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +private fun StepDetailsFinanzen(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) { + LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + item { + OutlinedTextField( + value = state.beschreibung, + onValueChange = { onStateChange(state.copy(beschreibung = it)) }, + label = { Text("Beschreibung (optional)") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + OutlinedTextField( + value = state.aufgabe, + onValueChange = { onStateChange(state.copy(aufgabe = it)) }, + label = { Text("Aufgabe (z.B. R1)") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + OutlinedTextField( + value = state.startgeld, + onValueChange = { onStateChange(state.copy(startgeld = it.filter { ch -> ch.isDigit() })) }, + label = { Text("Startgeld (Cent)") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = state.geldpreisAusbezahlt, onCheckedChange = { onStateChange(state.copy(geldpreisAusbezahlt = it)) }) + Text("Geldpreis ausbezahlt") + } + } + } +} + +@Composable +private fun StepOrtZeit(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) { + LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + item { + OutlinedTextField( + value = state.austragungsplatzId, + onValueChange = { onStateChange(state.copy(austragungsplatzId = it)) }, + label = { Text("Austragungsplatz-ID (optional)") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + OutlinedTextField( + value = state.beginnZeitTyp, + onValueChange = { onStateChange(state.copy(beginnZeitTyp = it)) }, + label = { Text("Beginn (FIX/ANSCHLIESSEND)") }, + modifier = Modifier.fillMaxWidth() + ) + } + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField( + value = state.geplantesDatum, + onValueChange = { onStateChange(state.copy(geplantesDatum = it)) }, + label = { Text("Datum (yyyy-MM-dd)") }, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = state.beginnZeit, + onValueChange = { onStateChange(state.copy(beginnZeit = it)) }, + label = { Text("Beginn (HH:mm)") }, + modifier = Modifier.weight(1f) + ) + } + } + item { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField( + value = state.reitdauerMinuten, + onValueChange = { onStateChange(state.copy(reitdauerMinuten = it.filter { ch -> ch.isDigit() })) }, + label = { Text("Reitdauer (min)") }, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = state.umbauMinuten, + onValueChange = { onStateChange(state.copy(umbauMinuten = it.filter { ch -> ch.isDigit() })) }, + label = { Text("Umbau (min)") }, + modifier = Modifier.weight(1f) + ) + OutlinedTextField( + value = state.besichtigungMinuten, + onValueChange = { onStateChange(state.copy(besichtigungMinuten = it.filter { ch -> ch.isDigit() })) }, + label = { Text("Besichtigung (min)") }, + modifier = Modifier.weight(1f) + ) + } + } + item { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = state.stechenGeplant, onCheckedChange = { onStateChange(state.copy(stechenGeplant = it)) }) + Text("Stechen geplant") + } + } + } +} + +@Composable +private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) { + Column(Modifier.fillMaxWidth()) { + // Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weiterer Prüfung -> TB-Hinweis + val warnTb = state.richter.isNotEmpty() + if (warnTb) { + Box( + Modifier.fillMaxWidth().background(Color(0xFFFFF8E1)).padding(12.dp) + ) { Text("Hinweis: Richter-Zuweisung erfordert Freigabe durch TB (Qualifikation prüfen)", color = Color(0xFFFFA000)) } + Spacer(Modifier.height(8.dp)) + } + + OutlinedTextField( + value = state.teilungsTyp, + onValueChange = { onStateChange(state.copy(teilungsTyp = it)) }, + label = { Text("Teilungsregel (z.B. MANUELL)") }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(Modifier.height(8.dp)) + + // Minimal-UI für das Hinzufügen eines Richters (freie Eingabe von UUID + Position) + var funktionaerId by remember { mutableStateOf("") } + var position by remember { mutableStateOf("") } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField(funktionaerId, { funktionaerId = it }, label = { Text("Funktionär-ID") }, modifier = Modifier.weight(1f)) + OutlinedTextField(position, { position = it }, label = { Text("Position (C/M/…)") }, modifier = Modifier.weight(1f)) + TextButton(onClick = { + if (funktionaerId.isNotBlank() && position.isNotBlank()) { + val list = state.richter + RichterEinsatzDto(funktionaerId = funktionaerId.trim(), position = position.trim()) + onStateChange(state.copy(richter = list)) + funktionaerId = ""; position = "" + } + }) { Text("Hinzufügen") } + } + + Spacer(Modifier.height(8.dp)) + + state.richter.forEachIndexed { idx, r -> + Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Text("${idx + 1}. ${r.position} – ${r.funktionaerId}") + TextButton(onClick = { + val list = state.richter.toMutableList().also { it.removeAt(idx) } + onStateChange(state.copy(richter = list)) + }) { Text("Entfernen") } + } + } + } +} + +// --- Mapping UI-State -> API-Payload --- +private fun CreateBewerbWizardState.toPayloadOrNull(): CreateBewerbPayload? { + if (klasse.isBlank() || bezeichnung.isBlank()) return null + + val hoehe: Int? = hoeheCm.toIntOrNull() + val startgeldCent: Long? = startgeld.toLongOrNull() + + val datum: LocalDate? = runCatching { if (geplantesDatum.isBlank()) null else LocalDate.parse(geplantesDatum) }.getOrNull() + val zeit: LocalTime? = runCatching { if (beginnZeit.isBlank()) null else LocalTime.parse(beginnZeit) }.getOrNull() + val beginnTyp: String? = beginnZeitTyp.ifBlank { null } + + val reitMin = reitdauerMinuten.toIntOrNull() + val umbauMin = umbauMinuten.toIntOrNull() + val besMin = besichtigungMinuten.toIntOrNull() + + return CreateBewerbPayload( + klasse = klasse.trim(), + hoeheCm = hoehe, + bezeichnung = bezeichnung.trim(), + beschreibung = beschreibung.ifBlank { null }, + aufgabe = aufgabe.ifBlank { null }, + aufgabenNummer = null, + paraGrade = null, + austragungsplatzId = austragungsplatzId.ifBlank { null }, + richterEinsaetze = richter, + geplantesDatum = datum, + beginnZeitTyp = beginnTyp, + beginnZeit = zeit, + reitdauerMinuten = reitMin, + umbauMinuten = umbauMin, + besichtigungMinuten = besMin, + stechenGeplant = stechenGeplant, + startgeldCent = startgeldCent, + geldpreisAusbezahlt = geldpreisAusbezahlt, + ) +}