feat(ui): add reusable components for FIGMA-based UI system
- Implemented new reusable components including Label, Input, InputOTP, HoverCard, Popover, Pagination, NavigationMenu, Menubar, ScrollArea, Resizable, RadioGroup, and Progress under `docs/06_Frontend/FIGMA/src/app/components/ui/`. - Enhanced structural organization to improve scalability and maintainability. - Updated `settings.gradle.kts` to include the new module `frontend:features:nennung-feature`. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+2
-1
@@ -13,7 +13,7 @@ sealed class AppScreen(val route: String) {
|
||||
data object Profile : AppScreen("/profile")
|
||||
data object OrganizerProfile : AppScreen("/organizer/profile")
|
||||
data object AuthCallback : AppScreen("/auth/callback")
|
||||
|
||||
data object Nennung : AppScreen("/nennung")
|
||||
companion object {
|
||||
fun fromRoute(route: String): AppScreen {
|
||||
return when (route) {
|
||||
@@ -26,6 +26,7 @@ sealed class AppScreen(val route: String) {
|
||||
"/profile" -> Profile
|
||||
"/organizer/profile" -> OrganizerProfile
|
||||
"/auth/callback" -> AuthCallback
|
||||
"/nennung" -> Nennung
|
||||
else -> Landing // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Feature-Modul: Nennungs-Maske (Desktop-only)
|
||||
* Kapselt die gesamte UI und Logik für die Nennungserfassung am Turnier.
|
||||
*/
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
group = "at.mocode.clients"
|
||||
version = "1.0.0"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.domain)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package at.mocode.nennung.feature.di
|
||||
|
||||
import at.mocode.nennung.feature.presentation.NennungViewModel
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
val nennungFeatureModule = module {
|
||||
viewModel { NennungViewModel() }
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package at.mocode.nennung.feature.domain
|
||||
|
||||
// --- Pferd ---
|
||||
data class Pferd(
|
||||
val kopfNr: String,
|
||||
val name: String,
|
||||
val rasse: String = "",
|
||||
val farbe: String = "",
|
||||
val besitzer: String = "",
|
||||
val stallBox: String = "",
|
||||
)
|
||||
|
||||
// --- Reiter ---
|
||||
data class Reiter(
|
||||
val kopfNr: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val verein: String = "",
|
||||
val lizenzNr: String = "",
|
||||
val lizenzGueltig: Boolean = true,
|
||||
val kontoSaldo: Double = 0.0,
|
||||
) {
|
||||
val vollname: String get() = "$vorname $nachname"
|
||||
}
|
||||
|
||||
// --- Bewerb ---
|
||||
enum class Sparte { DRESSUR, SPRINGEN }
|
||||
|
||||
data class Bewerb(
|
||||
val nr: Int,
|
||||
val tag: String,
|
||||
val platz: Int,
|
||||
val beginn: String,
|
||||
val name: String,
|
||||
val sparte: Sparte,
|
||||
val klasse: String,
|
||||
val anzahlNennungen: Int = 0,
|
||||
)
|
||||
|
||||
// --- Startwunsch ---
|
||||
enum class Startwunsch { VORNE, HINTEN, KEINE_PRAEFERENZ }
|
||||
|
||||
// --- Nennung ---
|
||||
data class Nennung(
|
||||
val tag: String,
|
||||
val platz: Int,
|
||||
val bewerbNr: Int,
|
||||
val bewerbName: String,
|
||||
val pferdName: String,
|
||||
val reiterName: String,
|
||||
val startwunsch: Startwunsch = Startwunsch.KEINE_PRAEFERENZ,
|
||||
)
|
||||
|
||||
// --- Verkauf-Artikel ---
|
||||
data class VerkaufArtikel(
|
||||
val knr: String = "",
|
||||
val buchungstext: String,
|
||||
val einzelpreis: Double,
|
||||
val menge: Int = 0,
|
||||
val gebucht: Double = 0.0,
|
||||
) {
|
||||
val betrag: Double get() = menge * einzelpreis
|
||||
}
|
||||
|
||||
// --- Mock-Daten (werden später durch echte API ersetzt) ---
|
||||
object NennungMockData {
|
||||
|
||||
val bewerbe = listOf(
|
||||
Bewerb(1, "So", 1, "08:00", "Dressurreiterprüfung Ratepass", Sparte.DRESSUR, "E"),
|
||||
Bewerb(2, "So", 1, "08:20", "Dressurreiterprüfung Katecnadel", Sparte.DRESSUR, "E"),
|
||||
Bewerb(3, "So", 1, "08:40", "Dressurreiterprüfung ldf. (ldf.)", Sparte.DRESSUR, "A"),
|
||||
Bewerb(4, "So", 1, "09:00", "Dressurprüfung ldf. (ldf.)", Sparte.DRESSUR, "L"),
|
||||
Bewerb(5, "So", 1, "09:20", "Führzügelklasse", Sparte.DRESSUR, "Kl."),
|
||||
Bewerb(6, "So", 1, "09:40", "First Ridden", Sparte.DRESSUR, "Kl."),
|
||||
Bewerb(7, "So", 1, "10:00", "Pony Dressurprüfung Kl. A", Sparte.DRESSUR, "A"),
|
||||
Bewerb(8, "So", 1, "10:20", "Dressurreiterprüfung Kl. A", Sparte.DRESSUR, "A"),
|
||||
Bewerb(9, "So", 2, "11:00", "Stilspringen Kl. A", Sparte.SPRINGEN, "A"),
|
||||
Bewerb(10, "So", 2, "11:30", "Stilspringen Kl. L", Sparte.SPRINGEN, "L"),
|
||||
Bewerb(11, "So", 2, "13:00", "Springprüfung Kl. M", Sparte.SPRINGEN, "M"),
|
||||
Bewerb(12, "So", 2, "14:00", "Springprüfung Kl. S", Sparte.SPRINGEN, "S"),
|
||||
)
|
||||
|
||||
val pferde = listOf(
|
||||
Pferd("1001", "Amadeus", "Warmblut", "Braun", "Müller Hans", "Box 3"),
|
||||
Pferd("1002", "Bella", "Haflinger", "Fuchs", "Huber Maria", "Box 7"),
|
||||
Pferd("1003", "Casanova", "Lipizzaner", "Schimmel", "Gruber Josef", "Box 12"),
|
||||
Pferd("1004", "Donner", "Trakehner", "Rappe", "Wagner Anna", ""),
|
||||
Pferd("1005", "Estrella", "Andalusier", "Schimmel", "Bauer Klaus", "Box 2"),
|
||||
)
|
||||
|
||||
val reiter = listOf(
|
||||
Reiter("2001", "Hans", "Müller", "RV Neumarkt", "AT-12345", true, 0.0),
|
||||
Reiter("2002", "Maria", "Huber", "RV Salzburg", "AT-23456", true, -45.0),
|
||||
Reiter("2003", "Josef", "Gruber", "RV Wien", "AT-34567", false, 0.0),
|
||||
Reiter("2004", "Anna", "Wagner", "RV Graz", "AT-45678", true, 120.0),
|
||||
Reiter("2005", "Klaus", "Bauer", "RV Linz", "AT-56789", true, 0.0),
|
||||
)
|
||||
|
||||
val verkaufArtikel = listOf(
|
||||
VerkaufArtikel("", "Belastung", 0.0),
|
||||
VerkaufArtikel("", "Gutschrift", 0.0),
|
||||
VerkaufArtikel("BP", "Boxenpauschale", 115.0),
|
||||
VerkaufArtikel("AN", "Ansage", 2.0),
|
||||
VerkaufArtikel("FT", "Füttern", 3.0),
|
||||
VerkaufArtikel("HE", "Heu", 13.0),
|
||||
VerkaufArtikel("SP", "Späne", 15.0),
|
||||
VerkaufArtikel("ST", "Stroh", 5.0),
|
||||
VerkaufArtikel("EL", "Strom", 50.0),
|
||||
VerkaufArtikel("YN", "Y-Nummer", 35.0),
|
||||
VerkaufArtikel("ZN", "Z-Nummer", 10.0),
|
||||
)
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
package at.mocode.nennung.feature.presentation
|
||||
|
||||
import at.mocode.nennung.feature.domain.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
// --- UI State ---
|
||||
data class NennungUiState(
|
||||
val pferdSuche: String = "",
|
||||
val reiterSuche: String = "",
|
||||
val selectedPferd: Pferd? = null,
|
||||
val selectedReiter: Reiter? = null,
|
||||
val pferdVorschlaege: List<Pferd> = emptyList(),
|
||||
val reiterVorschlaege: List<Reiter> = emptyList(),
|
||||
val nennungen: List<Nennung> = emptyList(),
|
||||
val bewerbe: List<Bewerb> = NennungMockData.bewerbe,
|
||||
val verkaufArtikel: List<VerkaufArtikel> = NennungMockData.verkaufArtikel,
|
||||
val spartFilter: Sparte? = null,
|
||||
val activeNennungTab: NennungTab = NennungTab.REITER,
|
||||
val activeVerkaufTab: VerkaufTab = VerkaufTab.VERKAUF,
|
||||
val statusMeldung: String? = null,
|
||||
)
|
||||
|
||||
enum class NennungTab { REITER, PFERD, BEWERBE }
|
||||
enum class VerkaufTab { VERKAUF, BUCHUNGEN }
|
||||
|
||||
class NennungViewModel : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(NennungUiState())
|
||||
val uiState: StateFlow<NennungUiState> = _uiState.asStateFlow()
|
||||
|
||||
// --- Pferd-Suche ---
|
||||
fun onPferdSucheChanged(query: String) {
|
||||
val vorschlaege = if (query.length >= 2) {
|
||||
NennungMockData.pferde.filter {
|
||||
it.kopfNr.startsWith(query) || it.name.contains(query, ignoreCase = true)
|
||||
}
|
||||
} else emptyList()
|
||||
_uiState.update { it.copy(pferdSuche = query, pferdVorschlaege = vorschlaege, selectedPferd = null) }
|
||||
}
|
||||
|
||||
fun onPferdSelected(pferd: Pferd) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
selectedPferd = pferd,
|
||||
pferdSuche = "${pferd.kopfNr} – ${pferd.name}",
|
||||
pferdVorschlaege = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPferdLeeren() {
|
||||
_uiState.update { it.copy(pferdSuche = "", selectedPferd = null, pferdVorschlaege = emptyList()) }
|
||||
}
|
||||
|
||||
// --- Reiter-Suche ---
|
||||
fun onReiterSucheChanged(query: String) {
|
||||
val vorschlaege = if (query.length >= 2) {
|
||||
NennungMockData.reiter.filter {
|
||||
it.kopfNr.startsWith(query) || it.vollname.contains(query, ignoreCase = true)
|
||||
}
|
||||
} else emptyList()
|
||||
_uiState.update { it.copy(reiterSuche = query, reiterVorschlaege = vorschlaege, selectedReiter = null) }
|
||||
}
|
||||
|
||||
fun onReiterSelected(reiter: Reiter) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
selectedReiter = reiter,
|
||||
reiterSuche = "${reiter.kopfNr} – ${reiter.vollname}",
|
||||
reiterVorschlaege = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onReiterLeeren() {
|
||||
_uiState.update { it.copy(reiterSuche = "", selectedReiter = null, reiterVorschlaege = emptyList()) }
|
||||
}
|
||||
|
||||
// --- Nennung durchführen ---
|
||||
fun nennungDurchfuehren(bewerb: Bewerb, startwunsch: Startwunsch = Startwunsch.KEINE_PRAEFERENZ) {
|
||||
val pferd = _uiState.value.selectedPferd ?: return
|
||||
val reiter = _uiState.value.selectedReiter ?: return
|
||||
|
||||
val bereitsGenannt = _uiState.value.nennungen.any {
|
||||
it.bewerbNr == bewerb.nr && it.pferdName == pferd.name && it.reiterName == reiter.vollname
|
||||
}
|
||||
if (bereitsGenannt) {
|
||||
_uiState.update { it.copy(statusMeldung = "⚠️ ${reiter.vollname} mit ${pferd.name} ist bereits in Bewerb ${bewerb.nr} gemeldet!") }
|
||||
return
|
||||
}
|
||||
|
||||
val neueNennung = Nennung(
|
||||
tag = bewerb.tag,
|
||||
platz = bewerb.platz,
|
||||
bewerbNr = bewerb.nr,
|
||||
bewerbName = bewerb.name,
|
||||
pferdName = pferd.name,
|
||||
reiterName = reiter.vollname,
|
||||
startwunsch = startwunsch,
|
||||
)
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
nennungen = state.nennungen + neueNennung,
|
||||
statusMeldung = "✅ Nennung erfasst: ${reiter.vollname} / ${pferd.name} → Bewerb ${bewerb.nr}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Nennung stornieren ---
|
||||
fun nennungStornieren(nennung: Nennung) {
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
nennungen = state.nennungen - nennung,
|
||||
statusMeldung = "🗑️ Nennung storniert: ${nennung.reiterName} / ${nennung.pferdName} → Bewerb ${nennung.bewerbNr}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Verkauf ---
|
||||
fun onVerkaufMengeChanged(artikel: VerkaufArtikel, delta: Int) {
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
verkaufArtikel = state.verkaufArtikel.map {
|
||||
if (it.buchungstext == artikel.buchungstext)
|
||||
it.copy(menge = maxOf(0, it.menge + delta))
|
||||
else it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Filter & Tabs ---
|
||||
fun onSpartFilterChanged(sparte: Sparte?) {
|
||||
_uiState.update { it.copy(spartFilter = sparte) }
|
||||
}
|
||||
|
||||
fun onNennungTabChanged(tab: NennungTab) {
|
||||
_uiState.update { it.copy(activeNennungTab = tab) }
|
||||
}
|
||||
|
||||
fun onVerkaufTabChanged(tab: VerkaufTab) {
|
||||
_uiState.update { it.copy(activeVerkaufTab = tab) }
|
||||
}
|
||||
|
||||
fun statusMeldungDismiss() {
|
||||
_uiState.update { it.copy(statusMeldung = null) }
|
||||
}
|
||||
|
||||
// --- Gefilterte Bewerbsliste ---
|
||||
fun gefilterteBewerbe(): List<Bewerb> {
|
||||
val filter = _uiState.value.spartFilter
|
||||
return if (filter == null) _uiState.value.bewerbe
|
||||
else _uiState.value.bewerbe.filter { it.sparte == filter }
|
||||
}
|
||||
|
||||
// --- Nennungen für aktuellen Reiter/Pferd ---
|
||||
fun nennungenFuerAktuellen(): List<Nennung> {
|
||||
val state = _uiState.value
|
||||
return state.nennungen.filter {
|
||||
(state.selectedReiter == null || it.reiterName == state.selectedReiter.vollname) &&
|
||||
(state.selectedPferd == null || it.pferdName == state.selectedPferd.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
+812
@@ -0,0 +1,812 @@
|
||||
package at.mocode.nennung.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.nennung.feature.domain.*
|
||||
|
||||
// Farben für Startwunsch-Markierung
|
||||
private val FarbeVorne = Color(0xFFE8F5E9) // Grün
|
||||
private val FarbeHinten = Color(0xFFE3F2FD) // Blau
|
||||
private val FarbeDressur = Color(0xFF3F51B5) // Indigo
|
||||
private val FarbeSpringen = Color(0xFFE65100) // Orange
|
||||
|
||||
@Composable
|
||||
fun NennungsMaske(
|
||||
viewModel: NennungViewModel,
|
||||
onStartlisteOeffnen: () -> Unit = {},
|
||||
onErgebnisseOeffnen: () -> Unit = {},
|
||||
onAbrechnungOeffnen: () -> Unit = {},
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
// Status-Snackbar
|
||||
state.statusMeldung?.let { meldung ->
|
||||
LaunchedEffect(meldung) {
|
||||
kotlinx.coroutines.delay(3000)
|
||||
viewModel.statusMeldungDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
// --- Status-Banner ---
|
||||
state.statusMeldung?.let { meldung ->
|
||||
Surface(
|
||||
color = if (meldung.startsWith("✅")) Color(0xFF388E3C)
|
||||
else if (meldung.startsWith("⚠️")) Color(0xFFF57C00)
|
||||
else MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = meldung,
|
||||
color = Color.White,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Zeile 1: Pferd/Reiter + Verkauf/Buchungen (50% Höhe) ---
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.5f)
|
||||
) {
|
||||
// Linke Hälfte: Pferd & Reiter Suche (60%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.6f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
PferdReiterEingabe(
|
||||
state = state,
|
||||
onPferdSucheChanged = viewModel::onPferdSucheChanged,
|
||||
onPferdSelected = viewModel::onPferdSelected,
|
||||
onPferdLeeren = viewModel::onPferdLeeren,
|
||||
onReiterSucheChanged = viewModel::onReiterSucheChanged,
|
||||
onReiterSelected = viewModel::onReiterSelected,
|
||||
onReiterLeeren = viewModel::onReiterLeeren,
|
||||
)
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.fillMaxHeight().width(1.dp))
|
||||
|
||||
// Rechte Hälfte: Verkauf/Buchungen (40%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.4f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
VerkaufBuchungenPanel(
|
||||
state = state,
|
||||
onTabChanged = viewModel::onVerkaufTabChanged,
|
||||
onMengeChanged = viewModel::onVerkaufMengeChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// --- Zeile 2: Aktions-Buttons (fix) ---
|
||||
AktionsButtonLeiste(
|
||||
canNennen = state.selectedPferd != null && state.selectedReiter != null,
|
||||
onStartlisteOeffnen = onStartlisteOeffnen,
|
||||
onErgebnisseOeffnen = onErgebnisseOeffnen,
|
||||
onAbrechnungOeffnen = onAbrechnungOeffnen,
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
// --- Zeile 3: Nennungstabelle + Bewerbsliste (50% Höhe) ---
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(0.5f)
|
||||
) {
|
||||
// Links: Nennungsübersicht (60%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.6f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
NennungenTabelle(
|
||||
state = state,
|
||||
nennungen = viewModel.nennungenFuerAktuellen(),
|
||||
onTabChanged = viewModel::onNennungTabChanged,
|
||||
onStornieren = viewModel::nennungStornieren,
|
||||
)
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.fillMaxHeight().width(1.dp))
|
||||
|
||||
// Rechts: Bewerbsliste (40%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.4f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
BewerbslistePanel(
|
||||
bewerbe = viewModel.gefilterteBewerbe(),
|
||||
nennungen = state.nennungen,
|
||||
selectedPferd = state.selectedPferd,
|
||||
selectedReiter = state.selectedReiter,
|
||||
spartFilter = state.spartFilter,
|
||||
onSpartFilterChanged = viewModel::onSpartFilterChanged,
|
||||
onNennung = { bewerb -> viewModel.nennungDurchfuehren(bewerb) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pferd & Reiter Eingabe
|
||||
// ---------------------------------------------------------------------------
|
||||
@Composable
|
||||
private fun PferdReiterEingabe(
|
||||
state: NennungUiState,
|
||||
onPferdSucheChanged: (String) -> Unit,
|
||||
onPferdSelected: (Pferd) -> Unit,
|
||||
onPferdLeeren: () -> Unit,
|
||||
onReiterSucheChanged: (String) -> Unit,
|
||||
onReiterSelected: (Reiter) -> Unit,
|
||||
onReiterLeeren: () -> Unit,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxSize().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
|
||||
// --- Pferd ---
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
SuchfeldMitVorschlaegen(
|
||||
label = "Pferd:",
|
||||
value = state.pferdSuche,
|
||||
onValueChange = onPferdSucheChanged,
|
||||
onLeeren = onPferdLeeren,
|
||||
vorschlaege = state.pferdVorschlaege.map { "${it.kopfNr} – ${it.name}" },
|
||||
onVorschlagSelected = { idx -> onPferdSelected(state.pferdVorschlaege[idx]) },
|
||||
)
|
||||
state.selectedPferd?.let { pferd ->
|
||||
MetaDatenBox {
|
||||
MetaZeile("Rasse:", pferd.rasse)
|
||||
MetaZeile("Farbe:", pferd.farbe)
|
||||
MetaZeile("Besitzer:", pferd.besitzer)
|
||||
if (pferd.stallBox.isNotEmpty()) MetaZeile("Box:", pferd.stallBox)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 4.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.weight(1f).height(28.dp),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text("Neu anlegen", fontSize = 10.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.weight(1f).height(28.dp),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text("Bearbeiten", fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.fillMaxHeight().width(1.dp))
|
||||
|
||||
// --- Reiter ---
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
SuchfeldMitVorschlaegen(
|
||||
label = "Reiter:",
|
||||
value = state.reiterSuche,
|
||||
onValueChange = onReiterSucheChanged,
|
||||
onLeeren = onReiterLeeren,
|
||||
vorschlaege = state.reiterVorschlaege.map { "${it.kopfNr} – ${it.vollname}" },
|
||||
onVorschlagSelected = { idx -> onReiterSelected(state.reiterVorschlaege[idx]) },
|
||||
)
|
||||
state.selectedReiter?.let { reiter ->
|
||||
MetaDatenBox {
|
||||
MetaZeile("Verein:", reiter.verein)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text("Lizenz:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(reiter.lizenzNr, fontSize = 10.sp, fontWeight = FontWeight.Medium)
|
||||
Surface(
|
||||
color = if (reiter.lizenzGueltig) Color(0xFF388E3C) else MaterialTheme.colorScheme.error,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
Text(
|
||||
text = if (reiter.lizenzGueltig) "Gültig" else "ABGELAUFEN",
|
||||
color = Color.White,
|
||||
fontSize = 9.sp,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text("Konto:", fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(
|
||||
text = "%.2f €".format(reiter.kontoSaldo),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (reiter.kontoSaldo < 0) MaterialTheme.colorScheme.error else Color(0xFF388E3C),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 4.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.weight(1f).height(28.dp),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text("Neu anlegen", fontSize = 10.sp)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = {},
|
||||
modifier = Modifier.weight(1f).height(28.dp),
|
||||
contentPadding = PaddingValues(0.dp)
|
||||
) {
|
||||
Text("Bearbeiten", fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuchfeldMitVorschlaegen(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
onLeeren: () -> Unit,
|
||||
vorschlaege: List<String>,
|
||||
onVorschlagSelected: (Int) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
Text(label, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
placeholder = { Text("Kopfnummer oder Name", fontSize = 11.sp) },
|
||||
textStyle = LocalTextStyle.current.copy(fontSize = 11.sp),
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = onLeeren,
|
||||
modifier = Modifier.height(36.dp),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||
) {
|
||||
Text("Leeren", fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
if (vorschlaege.isNotEmpty()) {
|
||||
Surface(shadowElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(modifier = Modifier.heightIn(max = 120.dp)) {
|
||||
itemsIndexed(vorschlaege) { idx, vorschlag ->
|
||||
Text(
|
||||
text = vorschlag,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onVorschlagSelected(idx) }
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
)
|
||||
if (idx < vorschlaege.lastIndex) Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MetaDatenBox(content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(2.dp), content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MetaZeile(label: String, value: String) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(value, fontSize = 10.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Aktions-Button-Leiste
|
||||
// ---------------------------------------------------------------------------
|
||||
@Composable
|
||||
private fun AktionsButtonLeiste(
|
||||
canNennen: Boolean,
|
||||
onStartlisteOeffnen: () -> Unit,
|
||||
onErgebnisseOeffnen: () -> Unit,
|
||||
onAbrechnungOeffnen: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Haupt-Aktion: Nennung durchführen (wird von Bewerbsliste getriggert via Doppelklick)
|
||||
Surface(
|
||||
color = if (canNennen) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CheckCircle, contentDescription = null, modifier = Modifier.size(14.dp),
|
||||
tint = if (canNennen) Color.White else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"Nennung: Doppelklick auf Bewerb [F5]",
|
||||
fontSize = 10.sp,
|
||||
color = if (canNennen) Color.White else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
SmallActionButton("Startliste", Icons.Default.List, "F7", onStartlisteOeffnen)
|
||||
SmallActionButton("Ergebnisse", Icons.Default.EmojiEvents, "F8", onErgebnisseOeffnen)
|
||||
SmallActionButton("Abrechnung", Icons.Default.Receipt, "F9", onAbrechnungOeffnen)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmallActionButton(
|
||||
label: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
shortcut: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
|
||||
modifier = Modifier.height(28.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, modifier = Modifier.size(12.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text("$label [$shortcut]", fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nennungen-Tabelle (unten links)
|
||||
// ---------------------------------------------------------------------------
|
||||
@Composable
|
||||
private fun NennungenTabelle(
|
||||
state: NennungUiState,
|
||||
nennungen: List<Nennung>,
|
||||
onTabChanged: (NennungTab) -> Unit,
|
||||
onStornieren: (Nennung) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Tabs
|
||||
TabRow(selectedTabIndex = state.activeNennungTab.ordinal, modifier = Modifier.height(32.dp)) {
|
||||
NennungTab.entries.forEach { tab ->
|
||||
Tab(
|
||||
selected = state.activeNennungTab == tab,
|
||||
onClick = { onTabChanged(tab) },
|
||||
modifier = Modifier.height(32.dp),
|
||||
) {
|
||||
Text(tab.name, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Aktualisieren", modifier = Modifier.size(14.dp))
|
||||
}
|
||||
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("${nennungen.size} Nennungen", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = {},
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("Positionieren", fontSize = 10.sp)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {},
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("Stornieren", fontSize = 10.sp, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Tabellen-Header
|
||||
TabellenHeader(
|
||||
listOf("Tag", "Pl.", "Bewerb", "Bewerbsname", "Startwunsch", "Pferd"),
|
||||
listOf(30f, 25f, 45f, 1f, 70f, 80f)
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
// Tabellen-Inhalt
|
||||
if (nennungen.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Keine Nennungen vorhanden", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(nennungen) { idx, nennung ->
|
||||
val bgColor = when (nennung.startwunsch) {
|
||||
Startwunsch.VORNE -> FarbeVorne
|
||||
Startwunsch.HINTEN -> FarbeHinten
|
||||
else -> if (idx % 2 == 0) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(bgColor)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(nennung.tag, fontSize = 10.sp, modifier = Modifier.width(30.dp))
|
||||
Text("${nennung.platz}", fontSize = 10.sp, modifier = Modifier.width(25.dp))
|
||||
Text("${nennung.bewerbNr}", fontSize = 10.sp, modifier = Modifier.width(45.dp))
|
||||
Text(
|
||||
nennung.bewerbName,
|
||||
fontSize = 10.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
when (nennung.startwunsch) {
|
||||
Startwunsch.VORNE -> "Vorne"
|
||||
Startwunsch.HINTEN -> "Hinten"
|
||||
else -> "–"
|
||||
},
|
||||
fontSize = 10.sp,
|
||||
modifier = Modifier.width(70.dp),
|
||||
)
|
||||
Text(
|
||||
nennung.pferdName,
|
||||
fontSize = 10.sp,
|
||||
modifier = Modifier.width(80.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Divider(thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bewerbsliste (unten rechts)
|
||||
// ---------------------------------------------------------------------------
|
||||
@Composable
|
||||
private fun BewerbslistePanel(
|
||||
bewerbe: List<Bewerb>,
|
||||
nennungen: List<Nennung>,
|
||||
selectedPferd: Pferd?,
|
||||
selectedReiter: Reiter?,
|
||||
spartFilter: Sparte?,
|
||||
onSpartFilterChanged: (Sparte?) -> Unit,
|
||||
onNennung: (Bewerb) -> Unit,
|
||||
) {
|
||||
val canNennen = selectedPferd != null && selectedReiter != null
|
||||
var lastClickTime by remember { mutableStateOf(0L) }
|
||||
var lastClickedBewerb by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Überschrift + Filter
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Bewerbsübersicht", fontSize = 11.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
// Sparte-Filter
|
||||
FilterChipKlein("Alle", spartFilter == null) { onSpartFilterChanged(null) }
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
FilterChipKlein("D", spartFilter == Sparte.DRESSUR) { onSpartFilterChanged(Sparte.DRESSUR) }
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
FilterChipKlein("S", spartFilter == Sparte.SPRINGEN) { onSpartFilterChanged(Sparte.SPRINGEN) }
|
||||
}
|
||||
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("${bewerbe.size} Bewerbe", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// Tabellen-Header
|
||||
TabellenHeader(
|
||||
listOf("Tag", "Pl.", "Bewerb", "Beginn", "Nenn.", "Bewerbsname"),
|
||||
listOf(28f, 22f, 45f, 45f, 35f, 1f)
|
||||
)
|
||||
|
||||
Divider()
|
||||
|
||||
// Tabellen-Inhalt
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(bewerbe) { idx, bewerb ->
|
||||
val bereitsGenannt = canNennen && nennungen.any {
|
||||
it.bewerbNr == bewerb.nr &&
|
||||
it.pferdName == selectedPferd?.name &&
|
||||
it.reiterName == selectedReiter?.vollname
|
||||
}
|
||||
val bgColor = when {
|
||||
bereitsGenannt -> Color(0xFFBBDEFB) // Blau = bereits gemeldet
|
||||
idx % 2 == 0 -> Color.Transparent
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(bgColor)
|
||||
.clickable(enabled = canNennen) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (lastClickedBewerb == bewerb.nr && now - lastClickTime < 400) {
|
||||
onNennung(bewerb)
|
||||
lastClickedBewerb = null
|
||||
} else {
|
||||
lastClickedBewerb = bewerb.nr
|
||||
lastClickTime = now
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(bewerb.tag, fontSize = 10.sp, modifier = Modifier.width(28.dp))
|
||||
Text("${bewerb.platz}", fontSize = 10.sp, modifier = Modifier.width(22.dp))
|
||||
// Bewerb-Nr mit Sparte-Farbe
|
||||
Text(
|
||||
"${bewerb.nr}",
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (bewerb.sparte == Sparte.DRESSUR) FarbeDressur else FarbeSpringen,
|
||||
modifier = Modifier.width(45.dp),
|
||||
)
|
||||
Text(bewerb.beginn, fontSize = 10.sp, modifier = Modifier.width(45.dp))
|
||||
Text("${bewerb.anzahlNennungen}", fontSize = 10.sp, modifier = Modifier.width(35.dp))
|
||||
Text(
|
||||
bewerb.name,
|
||||
fontSize = 10.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Divider(thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
|
||||
if (!canNennen) {
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
Text(
|
||||
"Bitte wählen Sie zuerst ein Pferd und einen Reiter aus",
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterChipKlein(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Surface(
|
||||
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
fontSize = 9.sp,
|
||||
color = if (selected) Color.White else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Verkauf & Buchungen Panel (oben rechts)
|
||||
// ---------------------------------------------------------------------------
|
||||
@Composable
|
||||
private fun VerkaufBuchungenPanel(
|
||||
state: NennungUiState,
|
||||
onTabChanged: (VerkaufTab) -> Unit,
|
||||
onMengeChanged: (VerkaufArtikel, Int) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
TabRow(selectedTabIndex = state.activeVerkaufTab.ordinal, modifier = Modifier.height(32.dp)) {
|
||||
VerkaufTab.entries.forEach { tab ->
|
||||
Tab(
|
||||
selected = state.activeVerkaufTab == tab,
|
||||
onClick = { onTabChanged(tab) },
|
||||
modifier = Modifier.height(32.dp),
|
||||
) {
|
||||
Text(tab.name, fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (state.activeVerkaufTab) {
|
||||
VerkaufTab.VERKAUF -> VerkaufTabInhalt(state.verkaufArtikel, onMengeChanged)
|
||||
VerkaufTab.BUCHUNGEN -> BuchungenTabInhalt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerkaufTabInhalt(artikel: List<VerkaufArtikel>, onMengeChanged: (VerkaufArtikel, Int) -> Unit) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Toolbar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("${artikel.size} Artikel", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = {},
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("Rückgängig", fontSize = 10.sp)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {},
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("Speichern", fontSize = 10.sp)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
TabellenHeader(
|
||||
listOf("KNr", "+", "Menge", "–", "Buchungstext", "Betrag", "Gebucht"),
|
||||
listOf(30f, 20f, 45f, 20f, 1f, 55f, 55f)
|
||||
)
|
||||
Divider()
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
itemsIndexed(artikel) { idx, art ->
|
||||
val bgColor = when {
|
||||
art.buchungstext == "Belastung" || art.buchungstext == "Gutschrift" -> Color(0xFFFFFDE7)
|
||||
idx % 2 == 0 -> Color.Transparent
|
||||
else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().background(bgColor).padding(horizontal = 4.dp, vertical = 1.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(art.knr, fontSize = 10.sp, modifier = Modifier.width(30.dp))
|
||||
IconButton(onClick = { onMengeChanged(art, 1) }, modifier = Modifier.size(20.dp)) {
|
||||
Icon(Icons.Default.Add, contentDescription = "+", modifier = Modifier.size(12.dp))
|
||||
}
|
||||
Text("${art.menge}", fontSize = 10.sp, modifier = Modifier.width(45.dp), fontWeight = FontWeight.Medium)
|
||||
IconButton(onClick = { onMengeChanged(art, -1) }, modifier = Modifier.size(20.dp)) {
|
||||
Icon(Icons.Default.Remove, contentDescription = "–", modifier = Modifier.size(12.dp))
|
||||
}
|
||||
Text(
|
||||
art.buchungstext,
|
||||
fontSize = 10.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text("%.2f".format(art.betrag), fontSize = 10.sp, modifier = Modifier.width(55.dp))
|
||||
Text("%.2f".format(art.gebucht), fontSize = 10.sp, modifier = Modifier.width(55.dp))
|
||||
}
|
||||
Divider(thickness = 0.5.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BuchungenTabInhalt() {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = {}, modifier = Modifier.size(20.dp)) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
Text("Aktualisieren", fontSize = 10.sp, modifier = Modifier.padding(start = 2.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("0 Buchungen", fontSize = 10.sp, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = {},
|
||||
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("Stornieren", fontSize = 10.sp, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
TabellenHeader(listOf("Kopfnr", "Menge", "Buchungstext", "Soll", "Haben"), listOf(55f, 45f, 1f, 55f, 55f))
|
||||
Divider()
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Keine Buchungen vorhanden", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hilfs-Composable: Tabellen-Header
|
||||
// ---------------------------------------------------------------------------
|
||||
@Composable
|
||||
private fun TabellenHeader(spalten: List<String>, breiten: List<Float>) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
spalten.forEachIndexed { idx, name ->
|
||||
val breite = breiten.getOrNull(idx) ?: 1f
|
||||
if (breite == 1f) {
|
||||
Text(name, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f))
|
||||
} else {
|
||||
Text(name, fontSize = 10.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(breite.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ kotlin {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
implementation(libs.koin.core)
|
||||
implementation(project(":frontend:features:nennung-feature"))
|
||||
}
|
||||
|
||||
jsMain.dependencies {
|
||||
|
||||
@@ -74,6 +74,7 @@ fun MainApp() {
|
||||
|
||||
is AppScreen.Dashboard -> DashboardScreen(
|
||||
authTokenManager = authTokenManager,
|
||||
onNennungOeffnen = { navigationPort.navigateToScreen(AppScreen.Nennung) },
|
||||
onLogout = {
|
||||
authTokenManager.clearToken()
|
||||
if (currentPlatform() == PlatformType.DESKTOP) {
|
||||
@@ -145,6 +146,7 @@ fun MainApp() {
|
||||
}
|
||||
}
|
||||
|
||||
is AppScreen.Nennung -> NennungScreenContent()
|
||||
is AppScreen.Profile -> AuthStatusScreen(
|
||||
authTokenManager = authTokenManager,
|
||||
onNavigateToLogin = {
|
||||
@@ -412,7 +414,8 @@ private fun FeatureCard(number: String, title: String, body: String) {
|
||||
private fun DashboardScreen(
|
||||
authTokenManager: AuthTokenManager,
|
||||
onLogout: () -> Unit,
|
||||
onCreateTournament: () -> Unit
|
||||
onCreateTournament: () -> Unit,
|
||||
onNennungOeffnen: () -> Unit = {},
|
||||
) {
|
||||
val authState by authTokenManager.authState.collectAsState()
|
||||
val scrollState = rememberScrollState()
|
||||
@@ -474,6 +477,15 @@ private fun DashboardScreen(
|
||||
if (isDesktop && isAdmin) {
|
||||
// DESKTOP VIEW - STEUERUNGSZENTRALE FÜR DEN ADMIN (DICH)
|
||||
// Neues Turnier anlegen Button
|
||||
Button(
|
||||
onClick = onNennungOeffnen,
|
||||
modifier = Modifier.fillMaxWidth().height(64.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "📋 Nennungs-Maske öffnen",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onCreateTournament,
|
||||
modifier = Modifier.fillMaxWidth().height(64.dp)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
expect fun NennungScreenContent()
|
||||
@@ -0,0 +1,8 @@
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun NennungScreenContent() {
|
||||
// Nennungs-Maske ist nur für Desktop (JVM) verfügbar
|
||||
Text("Nennungs-Maske ist nur in der Desktop-App verfügbar.")
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import androidx.compose.runtime.Composable
|
||||
import at.mocode.nennung.feature.presentation.NennungsMaske
|
||||
import at.mocode.nennung.feature.presentation.NennungViewModel
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
actual fun NennungScreenContent() {
|
||||
val viewModel: NennungViewModel = koinViewModel()
|
||||
NennungsMaske(viewModel = viewModel)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import at.mocode.frontend.core.localdb.localDbModule
|
||||
import at.mocode.frontend.core.network.networkModule
|
||||
import at.mocode.frontend.core.sync.di.syncModule
|
||||
import at.mocode.ping.feature.di.pingFeatureModule
|
||||
import at.mocode.nennung.feature.di.nennungFeatureModule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import navigation.navigationModule
|
||||
import org.koin.core.context.loadKoinModules
|
||||
@@ -19,7 +20,17 @@ fun main() = application {
|
||||
// Initialize DI (Koin) with shared modules + network module
|
||||
try {
|
||||
// Updated: Only load the consolidated pingFeatureModule from at.mocode.ping.feature.di
|
||||
startKoin { modules(networkModule, syncModule, pingFeatureModule, authModule, navigationModule, localDbModule) }
|
||||
startKoin {
|
||||
modules(
|
||||
networkModule,
|
||||
syncModule,
|
||||
pingFeatureModule,
|
||||
nennungFeatureModule,
|
||||
authModule,
|
||||
navigationModule,
|
||||
localDbModule
|
||||
)
|
||||
}
|
||||
println("[DesktopApp] Koin initialized with networkModule + authModule + navigationModule + pingFeatureModule + localDbModule")
|
||||
} catch (e: Exception) {
|
||||
println("[DesktopApp] Koin initialization warning: ${e.message}")
|
||||
|
||||
Reference in New Issue
Block a user