feat(billing-feature): introduce billing module with Money class, calculation logic, and DI setup

- Added `Money` value class for precise monetary operations.
- Implemented `BillingCalculator` to handle fee calculations, including ÖTO-compliant contributions and prize distribution rules.
- Created `BillingModule` for dependency injection using Koin.
- Integrated `billing-feature` into the desktop shell and project dependencies.
- Introduced `TurnierWizardV2` and `VeranstalterAuswahlV2` screens with improved UI and billing synchronization support.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-30 16:38:25 +02:00
parent 0503cf8bcc
commit b2e6158328
9 changed files with 462 additions and 9 deletions
@@ -0,0 +1,152 @@
package at.mocode.turnier.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Check
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.unit.dp
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
private val PrimaryBlue = Color(0xFF1E3A8A)
/**
* TurnierWizardV2 - Der neue Wizard für die Turnieranlage (Vision_03).
*/
@Composable
fun TurnierWizardV2(
veranstaltungId: Long,
onBack: () -> Unit,
onSave: () -> Unit,
) {
var currentStep by remember { mutableIntStateOf(0) }
val steps = listOf("Stammdaten", "Organisation", "Bewerbe ⭐", "Preisliste")
Column(modifier = Modifier.fillMaxSize().background(Color.White)) {
// Wizard Header
Surface(shadowElevation = 4.dp, color = Color.White) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBack) { Icon(Icons.Default.ArrowBack, null) }
Spacer(Modifier.width(8.dp))
Text("Neues Turnier anlegen", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
}
Button(
onClick = onSave,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
enabled = currentStep == steps.size - 1
) {
Text("Turnier finalisieren")
}
}
Spacer(Modifier.height(16.dp))
// Stepper UI
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
steps.forEachIndexed { index, title ->
StepItem(
title = title,
isActive = index == currentStep,
isCompleted = index < currentStep,
modifier = Modifier.weight(1f)
)
if (index < steps.size - 1) {
Box(
modifier = Modifier.height(2.dp).weight(0.5f)
.background(if (index < currentStep) PrimaryBlue else Color.LightGray)
)
}
}
}
}
}
// Content Area
Box(modifier = Modifier.weight(1f).padding(32.dp)) {
when (currentStep) {
0 -> PlaceholderContent("Stammdaten", "Hier werden OEPS-Nummer, Kategorie und Sparte konfiguriert.")
1 -> PlaceholderContent("Organisation", "Zuweisung von Richtern, Parcourschefs und Tierärzten.")
2 -> PlaceholderContent("Bewerbe", "Konfiguration der Bewerbe und Abteilungen gemäß § 39 ÖTO.")
3 -> PlaceholderContent("Preisliste", "Einstellung der Nenngebühren und Sportförderbeiträge (Billing-Sync).")
}
}
// Wizard Navigation
Surface(shadowElevation = 8.dp, color = Color.White) {
Row(
modifier = Modifier.fillMaxWidth().padding(24.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(
onClick = { if (currentStep > 0) currentStep-- else onBack() }
) {
Text(if (currentStep == 0) "Abbrechen" else "Zurück")
}
if (currentStep < steps.size - 1) {
Button(
onClick = { currentStep++ },
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
) {
Text("Weiter")
Spacer(Modifier.width(8.dp))
Icon(Icons.Default.ArrowForward, null, modifier = Modifier.size(16.dp))
}
}
}
}
}
}
@Composable
private fun StepItem(title: String, isActive: Boolean, isCompleted: Boolean, modifier: Modifier = Modifier) {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
Box(
modifier = Modifier
.size(32.dp)
.background(
color = when {
isCompleted -> PrimaryBlue
isActive -> PrimaryBlue
else -> Color.LightGray
},
shape = androidx.compose.foundation.shape.CircleShape
),
contentAlignment = Alignment.Center
) {
if (isCompleted) {
Icon(Icons.Default.Check, null, tint = Color.White, modifier = Modifier.size(16.dp))
} else {
Text(
text = "", // Or step number
color = Color.White,
style = MaterialTheme.typography.bodySmall
)
}
}
Text(
text = title,
style = MaterialTheme.typography.bodySmall,
fontWeight = if (isActive) FontWeight.Bold else FontWeight.Normal,
color = if (isActive || isCompleted) PrimaryBlue else Color.Gray,
modifier = Modifier.padding(top = 4.dp)
)
}
}