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:
2026-04-16 13:26:39 +02:00
parent 10f9e82718
commit cb4f2f855c
11 changed files with 318 additions and 121 deletions
@@ -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
}
@@ -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,
@@ -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))
} }
@@ -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,
@@ -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)
@@ -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)
}
}
} }
} }
} }
@@ -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
)
@@ -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()
} }
@@ -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()) }
} }
@@ -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(