diff --git a/docs/01_Architecture/Frontend_Komponenten_Roadmap.md b/docs/01_Architecture/Frontend_Komponenten_Roadmap.md index dfbe26a5..56d38e1f 100644 --- a/docs/01_Architecture/Frontend_Komponenten_Roadmap.md +++ b/docs/01_Architecture/Frontend_Komponenten_Roadmap.md @@ -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] diff --git a/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt new file mode 100644 index 00000000..b2940127 --- /dev/null +++ b/frontend/core/design-system/src/commonMain/kotlin/at/mocode/frontend/core/designsystem/components/MsSearchableSelect.kt @@ -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 MsSearchableSelect( + label: String, + selectedOption: T?, + onOptionSelected: (T) -> Unit, + onSearchQueryChange: (String) -> Unit, + options: List, + 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") + } + } + } + } + } + } + } +}