diff --git a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportMode.kt b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportMode.kt new file mode 100644 index 00000000..b077eb63 --- /dev/null +++ b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportMode.kt @@ -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 +} diff --git a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt index cf263023..36034959 100644 --- a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt +++ b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt @@ -91,9 +91,13 @@ class ZnsImportService( * Importiert eine ZNS-ZIP-Datei aus einem [InputStream]. * * @param zipInputStream Der InputStream der ZIP-Datei. + * @param mode Der [ZnsImportMode] (Standard: [ZnsImportMode.FULL]). * @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) // println("[DEBUG_LOG] Gefundene Dateien: ${dateien.keys}") // 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 (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( vereineImportiert = vereineNeu, diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt index 5f6ab34a..1689388a 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/api/ZnsImportController.kt @@ -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.ImportJobRegistry import at.mocode.zns.import.service.job.ZnsImportOrchestrator +import at.mocode.zns.importer.ZnsImportMode import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @@ -23,9 +24,12 @@ class ZnsImportController( * Rückgabe: 202 Accepted mit JobId. */ @PostMapping(consumes = ["multipart/form-data"]) - fun starteImport(@RequestParam("file") file: MultipartFile): ResponseEntity { + fun starteImport( + @RequestParam("file") file: MultipartFile, + @RequestParam("mode", defaultValue = "FULL") mode: ZnsImportMode + ): ResponseEntity { 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)) } diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt index 77de6c96..2df893b9 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/job/ZnsImportOrchestrator.kt @@ -1,6 +1,6 @@ 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -19,7 +19,7 @@ class ZnsImportOrchestrator( ) { private val scope = CoroutineScope(Dispatchers.IO) - fun starteImport(jobId: String, zipBytes: ByteArray) { + fun starteImport(jobId: String, zipBytes: ByteArray, mode: ZnsImportMode = ZnsImportMode.FULL) { scope.launch { runCatching { jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.ENTPACKEN, "Entpacke ZIP-Datei...", 5) @@ -28,7 +28,7 @@ class ZnsImportOrchestrator( archiviereZip(zipBytes) jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.VERARBEITUNG, "Verarbeite ZNS-Daten...", 20) - val result = service.importiereZip(zipBytes.inputStream()) + val result = service.importiereZip(zipBytes.inputStream(), mode) jobRegistry.aktualisiereStatus( jobId, ImportJobStatus.ABGESCHLOSSEN, diff --git a/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt new file mode 100644 index 00000000..501aa6e0 --- /dev/null +++ b/frontend/core/domain/src/commonMain/kotlin/at/mocode/frontend/core/domain/zns/ZnsImportProvider.kt @@ -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 = 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() +} diff --git a/frontend/features/veranstaltung-feature/build.gradle.kts b/frontend/features/veranstaltung-feature/build.gradle.kts index 8616d0c9..e1387518 100644 --- a/frontend/features/veranstaltung-feature/build.gradle.kts +++ b/frontend/features/veranstaltung-feature/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl /** * 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 { alias(libs.plugins.kotlinMultiplatform) @@ -40,6 +40,7 @@ kotlin { implementation(projects.frontend.core.network) implementation(projects.frontend.core.domain) implementation(projects.core.coreDomain) + implementation(projects.frontend.core.auth) implementation(compose.foundation) implementation(compose.runtime) diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungNeuScreen.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungNeuScreen.kt index 7728f0e1..ac9f5abb 100644 --- a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungNeuScreen.kt +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungNeuScreen.kt @@ -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.MsTextField 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). @@ -23,24 +43,17 @@ fun VeranstaltungNeuScreen( onBack: () -> Unit, onSave: () -> Unit, ) { - var selectedTab by remember { mutableIntStateOf(0) } // Stammdaten ist Standard-Tab - val tabs = listOf("Stammdaten (A-Satz)", "Organisation", "Preisliste") + var currentStep by remember { mutableStateOf(MsWizardStep.ZNS_BASIS) } - // Formular-State für Stammdaten + // Formular-State var name by remember { mutableStateOf("") } var ort by remember { mutableStateOf("") } var startDatum by remember { mutableStateOf("") } var endDatum by remember { mutableStateOf("") } - var veranstalter 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 + var veranstalterId by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize()) { - // Toolbar (Header) + // Wizard Header Surface( modifier = Modifier.fillMaxWidth(), shadowElevation = 2.dp, @@ -52,33 +65,58 @@ fun VeranstaltungNeuScreen( 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") } Spacer(Modifier.width(Dimens.SpacingS)) - Text( - text = "Neue Veranstaltung anlegen", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold - ) + Column { + Text( + text = "Neue Veranstaltung anlegen", + 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) { - tabs.forEachIndexed { index, title -> - Tab( - selected = selectedTab == index, - onClick = { selectedTab = index }, - text = { Text(title) }, - ) - } - } + // Step Indicator + LinearProgressIndicator( + progress = { (currentStep.ordinal + 1).toFloat() / MsWizardStep.entries.size.toFloat() }, + modifier = Modifier.fillMaxWidth(), + ) Box( modifier = Modifier @@ -86,88 +124,69 @@ fun VeranstaltungNeuScreen( .padding(Dimens.SpacingL) .verticalScroll(rememberScrollState()) ) { - when (selectedTab) { - 0 -> { // Stammdaten + when (currentStep) { + 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( modifier = Modifier.widthIn(max = 600.dp), verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM) ) { - Text( - "Allgemeine Informationen (A-Satz)", - style = MaterialTheme.typography.titleLarge, - 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 - ) - } - + Text("Veranstaltungs-Details", style = MaterialTheme.typography.titleLarge) + MsTextField(value = name, onValueChange = { name = it }, label = "Name") + MsTextField(value = ort, onValueChange = { ort = it }, label = "Ort") Row(horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) { MsTextField( value = startDatum, onValueChange = { startDatum = it }, - label = "Startdatum (TT.MM.JJJJ)", - placeholder = "24.04.2026", + label = "Start", modifier = Modifier.weight(1f) ) MsTextField( value = endDatum, onValueChange = { endDatum = it }, - label = "Enddatum (TT.MM.JJJJ)", - placeholder = "26.04.2026", + label = "Ende", 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 - Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) { - Text("Funktionäre & Verantwortliche", style = MaterialTheme.typography.titleLarge) - Text("Hier werden Richter, Parcourschefs und Tierärzte zugewiesen.") - // Später: MsSearchableSelect für Funktionäre - } - } - - 2 -> { // Preisliste - Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) { - Text("Gebühren & Preisliste", style = MaterialTheme.typography.titleLarge) - Text("Definition der Nenngebühren und Pauschalen gemäß ÖTO.") + MsWizardStep.FACHLICHER_TYP -> { + Column( + modifier = Modifier.widthIn(max = 600.dp), + verticalArrangement = Arrangement.spacedBy(Dimens.SpacingM) + ) { + Text("Typ der Veranstaltung", style = MaterialTheme.typography.titleLarge) + Card(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.padding(Dimens.SpacingM), + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically + ) { + Text("🏆 Turnier", modifier = Modifier.weight(1f), style = MaterialTheme.typography.titleMedium) + RadioButton(selected = true, onClick = null) + } + } } } } diff --git a/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt new file mode 100644 index 00000000..c83ad6df --- /dev/null +++ b/frontend/features/veranstaltung-feature/src/jvmMain/kotlin/at/mocode/veranstaltung/feature/presentation/VeranstaltungWizardViewModel.kt @@ -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 +) diff --git a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt index 2a4e0f23..65683ef5 100644 --- a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt +++ b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/ZnsImportViewModel.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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 io.ktor.client.* import io.ktor.client.request.* @@ -20,17 +22,6 @@ import kotlinx.serialization.json.Json import java.io.File 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 = emptyList(), - val errorMessage: String? = null, - val isFinished: Boolean = false, -) @Serializable internal data class JobIdResponse(val jobId: String) @@ -51,19 +42,19 @@ private const val MAX_VISIBLE_ERRORS = 50 class ZnsImportViewModel( private val httpClient: HttpClient, private val authTokenManager: AuthTokenManager, -) : ViewModel() { +) : ViewModel(), ZnsImportProvider { - var state by mutableStateOf(ZnsImportState()) + override var state by mutableStateOf(ZnsImportState()) private set private var pollingJob: Job? = null private val json = Json { ignoreUnknownKeys = true } - fun onFileSelected(path: String) { + override fun onFileSelected(path: String) { state = ZnsImportState(selectedFilePath = path) } - fun startImport() { + override fun startImport(mode: String) { val filePath = state.selectedFilePath ?: return val file = File(filePath) if (!file.exists() || !file.name.endsWith(".zip", ignoreCase = true)) { @@ -79,6 +70,7 @@ class ZnsImportViewModel( try { val token = authTokenManager.authState.value.token val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") { + parameter("mode", mode) if (token != null) header(HttpHeaders.Authorization, "Bearer $token") setBody(MultiPartFormDataContent(formData { append("file", file.readBytes(), Headers.build { @@ -129,7 +121,7 @@ class ZnsImportViewModel( } } - fun reset() { + override fun reset() { pollingJob?.cancel() state = ZnsImportState() } diff --git a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/di/ZnsImportModule.kt b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/di/ZnsImportModule.kt index d1f02cd8..0bf3f0b4 100644 --- a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/di/ZnsImportModule.kt +++ b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/di/ZnsImportModule.kt @@ -1,9 +1,11 @@ package at.mocode.zns.feature.di +import at.mocode.frontend.core.domain.zns.ZnsImportProvider import at.mocode.zns.feature.ZnsImportViewModel import org.koin.core.qualifier.named import org.koin.dsl.module val znsImportModule = module { + factory { ZnsImportViewModel(get(named("apiClient")), get()) } factory { ZnsImportViewModel(get(named("apiClient")), get()) } } diff --git a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt index bd56112b..f6f21579 100644 --- a/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt +++ b/frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/zns/feature/presentation/StammdatenImportScreen.kt @@ -114,7 +114,10 @@ fun StammdatenImportScreen( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { 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, ) { Text("Status", style = MaterialTheme.typography.titleMedium) - StatusChip(state.jobStatus) + val statusMsg = state.jobStatus + if (statusMsg != null) { + StatusChip(statusMsg) + } } LinearProgressIndicator(