feat(veranstaltung): Wizard für neue Veranstaltung implementiert und ZNS-Light-Integration hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package at.mocode.zns.importer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Modus des ZNS-Imports.
|
||||||
|
*
|
||||||
|
* [FULL] - Alle Dateien (Vereine, Reiter, Pferde, Funktionäre) werden importiert.
|
||||||
|
* [LIGHT] - Nur Stammdaten (Vereine, Reiter) werden importiert (Performance-Optimiert).
|
||||||
|
*/
|
||||||
|
enum class ZnsImportMode {
|
||||||
|
FULL,
|
||||||
|
LIGHT
|
||||||
|
}
|
||||||
+20
-3
@@ -91,9 +91,13 @@ class ZnsImportService(
|
|||||||
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
|
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
|
||||||
*
|
*
|
||||||
* @param zipInputStream Der InputStream der ZIP-Datei.
|
* @param zipInputStream Der InputStream der ZIP-Datei.
|
||||||
|
* @param mode Der [ZnsImportMode] (Standard: [ZnsImportMode.FULL]).
|
||||||
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
|
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
|
||||||
*/
|
*/
|
||||||
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
|
suspend fun importiereZip(
|
||||||
|
zipInputStream: InputStream,
|
||||||
|
mode: ZnsImportMode = ZnsImportMode.FULL
|
||||||
|
): ZnsImportResult {
|
||||||
val dateien = extrahiereDateien(zipInputStream)
|
val dateien = extrahiereDateien(zipInputStream)
|
||||||
// println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}")
|
// println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}")
|
||||||
// dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
|
// dateien.forEach { (name, lines) -> println("[DEBUG_LOG] Datei $name hat ${lines.size} Zeilen") }
|
||||||
@@ -103,8 +107,21 @@ class ZnsImportService(
|
|||||||
|
|
||||||
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
|
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
|
||||||
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
|
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
|
||||||
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
|
|
||||||
val (richterNeu, richterUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
|
var pferdeNeu = 0
|
||||||
|
var pferdeUpd = 0
|
||||||
|
var richterNeu = 0
|
||||||
|
var richterUpd = 0
|
||||||
|
|
||||||
|
if (mode == ZnsImportMode.FULL) {
|
||||||
|
val (pNeu, pUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
|
||||||
|
pferdeNeu = pNeu
|
||||||
|
pferdeUpd = pUpd
|
||||||
|
|
||||||
|
val (rNeu, rUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
|
||||||
|
richterNeu = rNeu
|
||||||
|
richterUpd = rUpd
|
||||||
|
}
|
||||||
|
|
||||||
return ZnsImportResult(
|
return ZnsImportResult(
|
||||||
vereineImportiert = vereineNeu,
|
vereineImportiert = vereineNeu,
|
||||||
|
|||||||
+6
-2
@@ -3,6 +3,7 @@ package at.mocode.zns.import.service.api
|
|||||||
import at.mocode.zns.import.service.job.ImportJob
|
import at.mocode.zns.import.service.job.ImportJob
|
||||||
import at.mocode.zns.import.service.job.ImportJobRegistry
|
import at.mocode.zns.import.service.job.ImportJobRegistry
|
||||||
import at.mocode.zns.import.service.job.ZnsImportOrchestrator
|
import at.mocode.zns.import.service.job.ZnsImportOrchestrator
|
||||||
|
import at.mocode.zns.importer.ZnsImportMode
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
@@ -23,9 +24,12 @@ class ZnsImportController(
|
|||||||
* Rückgabe: 202 Accepted mit JobId.
|
* Rückgabe: 202 Accepted mit JobId.
|
||||||
*/
|
*/
|
||||||
@PostMapping(consumes = ["multipart/form-data"])
|
@PostMapping(consumes = ["multipart/form-data"])
|
||||||
fun starteImport(@RequestParam("file") file: MultipartFile): ResponseEntity<ImportStartResponse> {
|
fun starteImport(
|
||||||
|
@RequestParam("file") file: MultipartFile,
|
||||||
|
@RequestParam("mode", defaultValue = "FULL") mode: ZnsImportMode
|
||||||
|
): ResponseEntity<ImportStartResponse> {
|
||||||
val job = jobRegistry.erstelleJob()
|
val job = jobRegistry.erstelleJob()
|
||||||
orchestrator.starteImport(job.jobId, file.bytes)
|
orchestrator.starteImport(job.jobId, file.bytes, mode)
|
||||||
return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId))
|
return ResponseEntity.status(HttpStatus.ACCEPTED).body(ImportStartResponse(job.jobId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
package at.mocode.zns.import.service.job
|
package at.mocode.zns.import.service.job
|
||||||
|
|
||||||
import at.mocode.zns.importer.ZnsImportResult
|
import at.mocode.zns.importer.ZnsImportMode
|
||||||
import at.mocode.zns.importer.ZnsImportService
|
import at.mocode.zns.importer.ZnsImportService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -19,7 +19,7 @@ class ZnsImportOrchestrator(
|
|||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
fun starteImport(jobId: String, zipBytes: ByteArray) {
|
fun starteImport(jobId: String, zipBytes: ByteArray, mode: ZnsImportMode = ZnsImportMode.FULL) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5)
|
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5)
|
||||||
@@ -28,7 +28,7 @@ class ZnsImportOrchestrator(
|
|||||||
archiviereZip(zipBytes)
|
archiviereZip(zipBytes)
|
||||||
|
|
||||||
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20)
|
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20)
|
||||||
val result = service.importiereZip(zipBytes.inputStream())
|
val result = service.importiereZip(zipBytes.inputStream(), mode)
|
||||||
|
|
||||||
jobRegistry.aktualisiereStatus(
|
jobRegistry.aktualisiereStatus(
|
||||||
jobId, ImportJobStatus.ABGESCHLOSSEN,
|
jobId, ImportJobStatus.ABGESCHLOSSEN,
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package at.mocode.frontend.core.domain.zns
|
||||||
|
|
||||||
|
data class ZnsImportState(
|
||||||
|
val selectedFilePath: String? = null,
|
||||||
|
val isUploading: Boolean = false,
|
||||||
|
val jobId: String? = null,
|
||||||
|
val jobStatus: String? = null,
|
||||||
|
val progress: Int = 0,
|
||||||
|
val progressDetail: String = "",
|
||||||
|
val errors: List<String> = emptyList(),
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
val isFinished: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ZnsImportProvider {
|
||||||
|
val state: ZnsImportState
|
||||||
|
fun onFileSelected(path: String)
|
||||||
|
fun startImport(mode: String = "FULL")
|
||||||
|
fun reset()
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Feature-Modul: Veranstaltungs-Verwaltung (Desktop-only)
|
* Feature-Modul: Veranstaltungs-Verwaltung (Desktop-only)
|
||||||
* Kapselt alle Screens und Logik für Veranstaltungs-Übersicht, -Detail und -Neuanlage.
|
* kapselt alle Screens und Logik für Veranstaltungs-Übersicht, -Detail und -Neuanlage.
|
||||||
*/
|
*/
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
@@ -40,6 +40,7 @@ kotlin {
|
|||||||
implementation(projects.frontend.core.network)
|
implementation(projects.frontend.core.network)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
|
implementation(projects.frontend.core.auth)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|||||||
+113
-94
@@ -13,6 +13,26 @@ import androidx.compose.ui.unit.dp
|
|||||||
import at.mocode.frontend.core.designsystem.components.MsButton
|
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
|
import javax.swing.JFileChooser
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter
|
||||||
|
|
||||||
|
enum class MsWizardStep {
|
||||||
|
ZNS_BASIS,
|
||||||
|
META_DATEN,
|
||||||
|
FACHLICHER_TYP
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pickZipFile(): String? {
|
||||||
|
val chooser = JFileChooser()
|
||||||
|
val filter = FileNameExtensionFilter("ZNS ZIP Datei", "zip")
|
||||||
|
chooser.fileFilter = filter
|
||||||
|
val returnVal = chooser.showOpenDialog(null)
|
||||||
|
return if (returnVal == JFileChooser.APPROVE_OPTION) {
|
||||||
|
chooser.selectedFile.absolutePath
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
* Formular zum Anlegen einer neuen Veranstaltung (Vision_03: /veranstaltung/neu).
|
||||||
@@ -23,24 +43,17 @@ fun VeranstaltungNeuScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(0) } // Stammdaten ist Standard-Tab
|
var currentStep by remember { mutableStateOf(MsWizardStep.ZNS_BASIS) }
|
||||||
val tabs = listOf("Stammdaten (A-Satz)", "Organisation", "Preisliste")
|
|
||||||
|
|
||||||
// Formular-State für Stammdaten
|
// Formular-State
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
var ort by remember { mutableStateOf("") }
|
var ort by remember { mutableStateOf("") }
|
||||||
var startDatum by remember { mutableStateOf("") }
|
var startDatum by remember { mutableStateOf("") }
|
||||||
var endDatum by remember { mutableStateOf("") }
|
var endDatum by remember { mutableStateOf("") }
|
||||||
var veranstalter by remember { mutableStateOf("") }
|
var veranstalterId by remember { mutableStateOf("") }
|
||||||
|
|
||||||
// Validierung
|
|
||||||
val isNameValid = name.isNotBlank()
|
|
||||||
val isOrtValid = ort.isNotBlank()
|
|
||||||
val isStartDatumValid = startDatum.length >= 8 // Einfache Prüfung für DD.MM.YY
|
|
||||||
val isFormValid = isNameValid && isOrtValid && isStartDatumValid
|
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
// Toolbar (Header)
|
// Wizard Header
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
@@ -52,33 +65,58 @@ fun VeranstaltungNeuScreen(
|
|||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = {
|
||||||
|
if (currentStep == MsWizardStep.ZNS_BASIS) onBack()
|
||||||
|
else currentStep = MsWizardStep.entries[currentStep.ordinal - 1]
|
||||||
|
}) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(Dimens.SpacingS))
|
Spacer(Modifier.width(Dimens.SpacingS))
|
||||||
Text(
|
Column {
|
||||||
text = "Neue Veranstaltung anlegen",
|
Text(
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
text = "Neue Veranstaltung anlegen",
|
||||||
fontWeight = FontWeight.Bold
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = when (currentStep) {
|
||||||
|
MsWizardStep.ZNS_BASIS -> "Schritt 1: ZNS-Basis & Veranstalter"
|
||||||
|
MsWizardStep.META_DATEN -> "Schritt 2: Meta-Daten & DB-Initialisierung"
|
||||||
|
MsWizardStep.FACHLICHER_TYP -> "Schritt 3: Fachlicher Typ"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||||
|
if (currentStep != MsWizardStep.FACHLICHER_TYP) {
|
||||||
|
MsButton(
|
||||||
|
text = "Weiter",
|
||||||
|
onClick = { currentStep = MsWizardStep.entries[currentStep.ordinal + 1] },
|
||||||
|
enabled = when (currentStep) {
|
||||||
|
MsWizardStep.ZNS_BASIS -> true // Vereinfacht für Prototypen
|
||||||
|
MsWizardStep.META_DATEN -> name.isNotBlank() && ort.isNotBlank()
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MsButton(
|
||||||
|
text = "Veranstaltung finalisieren",
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
MsButton(
|
|
||||||
text = "Veranstaltung speichern",
|
|
||||||
onClick = onSave,
|
|
||||||
enabled = isFormValid
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PrimaryTabRow(selectedTabIndex = selectedTab) {
|
// Step Indicator
|
||||||
tabs.forEachIndexed { index, title ->
|
LinearProgressIndicator(
|
||||||
Tab(
|
progress = { (currentStep.ordinal + 1).toFloat() / MsWizardStep.entries.size.toFloat() },
|
||||||
selected = selectedTab == index,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { selectedTab = index },
|
)
|
||||||
text = { Text(title) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -86,88 +124,69 @@ fun VeranstaltungNeuScreen(
|
|||||||
.padding(Dimens.SpacingL)
|
.padding(Dimens.SpacingL)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
when (selectedTab) {
|
when (currentStep) {
|
||||||
0 -> { // Stammdaten
|
MsWizardStep.ZNS_BASIS -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.widthIn(max = 800.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||||
|
) {
|
||||||
|
Text("ZNS-Daten importieren (Light-Modus)", style = MaterialTheme.typography.titleLarge)
|
||||||
|
Text("Invertierter Workflow: Zuerst ZNS-Basisdaten laden, dann Veranstaltung anlegen.")
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
text = "ZNS.zip auswählen & importieren",
|
||||||
|
onClick = { pickZipFile() }
|
||||||
|
)
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = veranstalterId,
|
||||||
|
onValueChange = { veranstalterId = it },
|
||||||
|
label = "Veranstalter (aus importierten Vereinen)",
|
||||||
|
placeholder = "Verein suchen..."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsWizardStep.META_DATEN -> {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.widthIn(max = 600.dp),
|
modifier = Modifier.widthIn(max = 600.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text("Veranstaltungs-Details", style = MaterialTheme.typography.titleLarge)
|
||||||
"Allgemeine Informationen (A-Satz)",
|
MsTextField(value = name, onValueChange = { name = it }, label = "Name")
|
||||||
style = MaterialTheme.typography.titleLarge,
|
MsTextField(value = ort, onValueChange = { ort = it }, label = "Ort")
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
MsTextField(
|
|
||||||
value = name,
|
|
||||||
onValueChange = { name = it },
|
|
||||||
label = "Name der Veranstaltung (z.B. Pfingstturnier)",
|
|
||||||
placeholder = "Name eingeben",
|
|
||||||
isError = name.isBlank(),
|
|
||||||
errorMessage = if (name.isBlank()) "Name ist erforderlich" else null
|
|
||||||
)
|
|
||||||
|
|
||||||
MsTextField(
|
|
||||||
value = veranstalter,
|
|
||||||
onValueChange = { veranstalter = it },
|
|
||||||
label = "Veranstalter (Verein)",
|
|
||||||
placeholder = "Verein suchen oder eingeben"
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
|
||||||
MsTextField(
|
|
||||||
value = ort,
|
|
||||||
onValueChange = { ort = it },
|
|
||||||
label = "Ort",
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
isError = ort.isBlank()
|
|
||||||
)
|
|
||||||
MsTextField(
|
|
||||||
value = "Österreich",
|
|
||||||
onValueChange = {},
|
|
||||||
label = "Land",
|
|
||||||
modifier = Modifier.weight(0.5f),
|
|
||||||
readOnly = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = startDatum,
|
value = startDatum,
|
||||||
onValueChange = { startDatum = it },
|
onValueChange = { startDatum = it },
|
||||||
label = "Startdatum (TT.MM.JJJJ)",
|
label = "Start",
|
||||||
placeholder = "24.04.2026",
|
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = endDatum,
|
value = endDatum,
|
||||||
onValueChange = { endDatum = it },
|
onValueChange = { endDatum = it },
|
||||||
label = "Enddatum (TT.MM.JJJJ)",
|
label = "Ende",
|
||||||
placeholder = "26.04.2026",
|
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
|
||||||
"Hinweis: Diese Daten bilden die Basis für den ZNS-Import und die Abrechnung.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> { // Organisation
|
MsWizardStep.FACHLICHER_TYP -> {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
Column(
|
||||||
Text("Funktionäre & Verantwortliche", style = MaterialTheme.typography.titleLarge)
|
modifier = Modifier.widthIn(max = 600.dp),
|
||||||
Text("Hier werden Richter, Parcourschefs und Tierärzte zugewiesen.")
|
verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||||
// Später: MsSearchableSelect für Funktionäre
|
) {
|
||||||
}
|
Text("Typ der Veranstaltung", style = MaterialTheme.typography.titleLarge)
|
||||||
}
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Row(
|
||||||
2 -> { // Preisliste
|
modifier = Modifier.padding(Dimens.SpacingM),
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||||
Text("Gebühren & Preisliste", style = MaterialTheme.typography.titleLarge)
|
) {
|
||||||
Text("Definition der Nenngebühren und Pauschalen gemäß ÖTO.")
|
Text("🏆 Turnier", modifier = Modifier.weight(1f), style = MaterialTheme.typography.titleMedium)
|
||||||
|
RadioButton(selected = true, onClick = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package at.mocode.veranstaltung.feature.presentation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||||
|
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||||
|
import at.mocode.frontend.core.network.NetworkConfig
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
enum class WizardStep {
|
||||||
|
ZNS_IMPORT,
|
||||||
|
META_DATA,
|
||||||
|
TYPE_SELECTION
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
data class VeranstaltungWizardState(
|
||||||
|
val currentStep: WizardStep = WizardStep.ZNS_IMPORT,
|
||||||
|
val veranstalterId: Uuid? = null,
|
||||||
|
val name: String = "",
|
||||||
|
val ort: String = "",
|
||||||
|
val startDatum: LocalDate? = null,
|
||||||
|
val endDatum: LocalDate? = null,
|
||||||
|
val isSaving: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val createdVeranstaltungId: Uuid? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
class VeranstaltungWizardViewModel(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
private val authTokenManager: AuthTokenManager,
|
||||||
|
val znsViewModel: ZnsImportProvider
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
var state by mutableStateOf(VeranstaltungWizardState())
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun nextStep() {
|
||||||
|
state = state.copy(
|
||||||
|
currentStep = when (state.currentStep) {
|
||||||
|
WizardStep.ZNS_IMPORT -> WizardStep.META_DATA
|
||||||
|
WizardStep.META_DATA -> WizardStep.TYPE_SELECTION
|
||||||
|
WizardStep.TYPE_SELECTION -> WizardStep.TYPE_SELECTION
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun previousStep() {
|
||||||
|
state = state.copy(
|
||||||
|
currentStep = when (state.currentStep) {
|
||||||
|
WizardStep.ZNS_IMPORT -> WizardStep.ZNS_IMPORT
|
||||||
|
WizardStep.META_DATA -> WizardStep.ZNS_IMPORT
|
||||||
|
WizardStep.TYPE_SELECTION -> WizardStep.META_DATA
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMetaData(name: String, ort: String, start: LocalDate?, end: LocalDate?) {
|
||||||
|
state = state.copy(name = name, ort = ort, startDatum = start, endDatum = end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVeranstalter(id: Uuid) {
|
||||||
|
state = state.copy(veranstalterId = id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveVeranstaltung() {
|
||||||
|
val veranstalterId = state.veranstalterId ?: return
|
||||||
|
val start = state.startDatum ?: return
|
||||||
|
val end = state.endDatum ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
state = state.copy(isSaving = true, error = null)
|
||||||
|
try {
|
||||||
|
val token = authTokenManager.authState.value.token
|
||||||
|
val response = httpClient.post("${NetworkConfig.baseUrl}/api/events") {
|
||||||
|
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
CreateEventRequest(
|
||||||
|
name = state.name,
|
||||||
|
startDatum = start,
|
||||||
|
endDatum = end,
|
||||||
|
ort = state.ort,
|
||||||
|
veranstalterVereinId = veranstalterId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == HttpStatusCode.Created) {
|
||||||
|
// Hier müsste die ID aus dem Response gelesen werden, falls benötigt
|
||||||
|
state = state.copy(isSaving = false)
|
||||||
|
nextStep()
|
||||||
|
} else {
|
||||||
|
state = state.copy(isSaving = false, error = "Fehler beim Speichern: ${response.status}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state = state.copy(isSaving = false, error = "Netzwerkfehler: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
data class CreateEventRequest(
|
||||||
|
val name: String,
|
||||||
|
val startDatum: LocalDate,
|
||||||
|
val endDatum: LocalDate,
|
||||||
|
val ort: String,
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
val veranstalterVereinId: Uuid
|
||||||
|
)
|
||||||
+8
-16
@@ -6,6 +6,8 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
import at.mocode.frontend.core.auth.data.AuthTokenManager
|
||||||
|
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||||
|
import at.mocode.frontend.core.domain.zns.ZnsImportState
|
||||||
import at.mocode.frontend.core.network.NetworkConfig
|
import at.mocode.frontend.core.network.NetworkConfig
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
@@ -20,17 +22,6 @@ import kotlinx.serialization.json.Json
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
data class ZnsImportState(
|
|
||||||
val selectedFilePath: String? = null,
|
|
||||||
val isUploading: Boolean = false,
|
|
||||||
val jobId: String? = null,
|
|
||||||
val jobStatus: String? = null,
|
|
||||||
val progress: Int = 0,
|
|
||||||
val progressDetail: String = "",
|
|
||||||
val errors: List<String> = emptyList(),
|
|
||||||
val errorMessage: String? = null,
|
|
||||||
val isFinished: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal data class JobIdResponse(val jobId: String)
|
internal data class JobIdResponse(val jobId: String)
|
||||||
@@ -51,19 +42,19 @@ private const val MAX_VISIBLE_ERRORS = 50
|
|||||||
class ZnsImportViewModel(
|
class ZnsImportViewModel(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val authTokenManager: AuthTokenManager,
|
private val authTokenManager: AuthTokenManager,
|
||||||
) : ViewModel() {
|
) : ViewModel(), ZnsImportProvider {
|
||||||
|
|
||||||
var state by mutableStateOf(ZnsImportState())
|
override var state by mutableStateOf(ZnsImportState())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var pollingJob: Job? = null
|
private var pollingJob: Job? = null
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
fun onFileSelected(path: String) {
|
override fun onFileSelected(path: String) {
|
||||||
state = ZnsImportState(selectedFilePath = path)
|
state = ZnsImportState(selectedFilePath = path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startImport() {
|
override fun startImport(mode: String) {
|
||||||
val filePath = state.selectedFilePath ?: return
|
val filePath = state.selectedFilePath ?: return
|
||||||
val file = File(filePath)
|
val file = File(filePath)
|
||||||
if (!file.exists() || !file.name.endsWith(".zip", ignoreCase = true)) {
|
if (!file.exists() || !file.name.endsWith(".zip", ignoreCase = true)) {
|
||||||
@@ -79,6 +70,7 @@ class ZnsImportViewModel(
|
|||||||
try {
|
try {
|
||||||
val token = authTokenManager.authState.value.token
|
val token = authTokenManager.authState.value.token
|
||||||
val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") {
|
val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") {
|
||||||
|
parameter("mode", mode)
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||||
setBody(MultiPartFormDataContent(formData {
|
setBody(MultiPartFormDataContent(formData {
|
||||||
append("file", file.readBytes(), Headers.build {
|
append("file", file.readBytes(), Headers.build {
|
||||||
@@ -129,7 +121,7 @@ class ZnsImportViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
override fun reset() {
|
||||||
pollingJob?.cancel()
|
pollingJob?.cancel()
|
||||||
state = ZnsImportState()
|
state = ZnsImportState()
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -1,9 +1,11 @@
|
|||||||
package at.mocode.zns.feature.di
|
package at.mocode.zns.feature.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||||
import at.mocode.zns.feature.ZnsImportViewModel
|
import at.mocode.zns.feature.ZnsImportViewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val znsImportModule = module {
|
val znsImportModule = module {
|
||||||
|
factory<ZnsImportProvider> { ZnsImportViewModel(get(named("apiClient")), get()) }
|
||||||
factory { ZnsImportViewModel(get(named("apiClient")), get()) }
|
factory { ZnsImportViewModel(get(named("apiClient")), get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -114,7 +114,10 @@ fun StammdatenImportScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Error, contentDescription = null, tint = MaterialTheme.colorScheme.error)
|
Icon(Icons.Default.Error, contentDescription = null, tint = MaterialTheme.colorScheme.error)
|
||||||
Text(state.errorMessage, color = MaterialTheme.colorScheme.onErrorContainer)
|
val errorMsg = state.errorMessage
|
||||||
|
if (errorMsg != null) {
|
||||||
|
Text(errorMsg, color = MaterialTheme.colorScheme.onErrorContainer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +132,10 @@ fun StammdatenImportScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text("Status", style = MaterialTheme.typography.titleMedium)
|
Text("Status", style = MaterialTheme.typography.titleMedium)
|
||||||
StatusChip(state.jobStatus)
|
val statusMsg = state.jobStatus
|
||||||
|
if (statusMsg != null) {
|
||||||
|
StatusChip(statusMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
|
|||||||
Reference in New Issue
Block a user