feat(design-system): add MsSearchableSelect component and update roadmap

- Introduced `MsSearchableSelect` for autocomplete search and selection of objects like riders, horses, or clubs.
- Updated `Frontend_Komponenten_Roadmap.md` to mark Phase 3 as complete.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-31 10:46:14 +02:00
parent 442caa59ff
commit b2a0883388
2 changed files with 131 additions and 2 deletions
@@ -39,13 +39,13 @@ Turniermanagement bedeutet Arbeit mit Listen. Wir benötigen mächtige, aber kom
* [x] Filter-Chips für schnelle Status-Wechsel.
* [x] Anzeige der Trefferanzahl (Result Count).
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) 🔵 [IN ARBEIT]
## Phase 3: Formular- & Eingabe-System (Die Datenerfassung) [ABGESCHLOSSEN]
Eingabe von Stammdaten muss schnell und fehlerfrei erfolgen.
* [x] **`MsEnumDropdown`:** Automatisches Mapping von Backend-Enums (ÖTO) auf UI-Auswahl.
* [x] **`MsValidationWrapper`:** Konsistente Anzeige von Fehlern und Warnungen (z.B. ÖTO-Validierungsregeln).
* [ ] **`MsSearchableSelect`:** Für die Verknüpfung von Reitern/Pferden (Autocomplete).
* [x] **`MsSearchableSelect`:** Für die Verknüpfung von Reitern/Pferden (Autocomplete-Suche).
## Phase 4: Layout-Patterns & Navigation ⚪ [ZUKUNFT]
@@ -0,0 +1,129 @@
package at.mocode.frontend.core.designsystem.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
/**
* Eine Komponente zur Suche und Auswahl von Objekten aus einer Liste.
* Ideal für die Auswahl von Reitern, Pferden oder Vereinen.
*
* @param label Das Label über dem Auswahlfeld.
* @param selectedOption Das aktuell gewählte Objekt.
* @param onOptionSelected Callback bei Auswahl eines Objekts.
* @param onSearchQueryChange Callback bei Änderung des Suchbegriffs (für API-Calls).
* @param options Die aktuell verfügbaren Optionen (Suchergebnisse).
* @param optionLabel Transformation des Objekts in einen lesbaren Text.
* @param modifier Modifier für die gesamte Komponente.
*/
@Composable
fun <T> MsSearchableSelect(
label: String,
selectedOption: T?,
onOptionSelected: (T) -> Unit,
onSearchQueryChange: (String) -> Unit,
options: List<T>,
modifier: Modifier = Modifier,
optionLabel: (T) -> String = { it.toString() },
enabled: Boolean = true,
placeholder: String = "Suchen & Auswählen..."
) {
var showDialog by remember { mutableStateOf(false) }
var searchText by remember { mutableStateOf("") }
Column(modifier = modifier) {
// --- 1. Das Anzeige-Feld (sieht aus wie ein TextField, öffnet aber den Dialog) ---
OutlinedTextField(
value = selectedOption?.let { optionLabel(it) } ?: "",
onValueChange = {},
readOnly = true,
label = { Text(label, style = MaterialTheme.typography.bodySmall) },
placeholder = { Text(placeholder, style = MaterialTheme.typography.bodySmall) },
trailingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled) { showDialog = true },
enabled = enabled,
singleLine = true,
textStyle = MaterialTheme.typography.bodyMedium,
shape = MaterialTheme.shapes.small
)
// --- 2. Der Such-Dialog (Desktop-zentriert) ---
if (showDialog) {
Dialog(onDismissRequest = { showDialog = false }) {
Surface(
shape = MaterialTheme.shapes.medium,
tonalElevation = 8.dp,
modifier = Modifier
.fillMaxWidth(0.8f)
.fillMaxHeight(0.7f)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = label,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
// Internes Suchfeld im Dialog
OutlinedTextField(
value = searchText,
onValueChange = {
searchText = it
onSearchQueryChange(it)
},
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Suchbegriff eingeben...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true,
shape = MaterialTheme.shapes.small
)
Spacer(modifier = Modifier.height(16.dp))
// Ergebnisliste
LazyColumn(modifier = Modifier.weight(1f)) {
items(options) { option ->
ListItem(
headlineContent = {
Text(
text = optionLabel(option),
style = MaterialTheme.typography.bodyMedium
)
},
modifier = Modifier.clickable {
onOptionSelected(option)
showDialog = false
searchText = ""
}
)
HorizontalDivider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showDialog = false }) {
Text("Abbrechen")
}
}
}
}
}
}
}
}