Extend Bewerb table and add BewerbRichterEinsatz table: introduce new properties, indexes, and mappings to support richer domain model and scheduling capabilities.
This commit is contained in:
+321
@@ -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<RichterEinsatzDto> = 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user