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