feat(zns-import): DAT-Dateisupport hinzugefügt, Fehlerbehebung und UI-Anpassungen

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-16 16:12:49 +02:00
parent 29c35c524b
commit 3b7abc55a4
4 changed files with 31 additions and 12 deletions
@@ -62,3 +62,12 @@ Parameter sind hiermit fixiert und umgesetzt.
`management.endpoint.health.probes.enabled: true`).
* **Optimierung**: Healthcheck in `dc-backend.yaml` auf `wget --no-verbose` umgestellt für bessere
Diagnosemöglichkeiten.
### Nachtrag: 2026-04-16 15:30 - Connectivity & Serialization Fixes
* **Connectivity**: `ConnectivityTracker` wurde auf den korrekten Health-Pfad `/api/ping/health` umgestellt. Der Footer
zeigt nun korrekt den Online-Status ("Cloud synchronisiert") an.
* **Serialization**: Behebung des Fehlers `Serializer for class 'JobIdResponse' is not found`. Die DTO-Klasse wurde im
Frontend in `ImportStartResponse` umbenannt (passend zum Backend) und die Sichtbarkeit auf `public` erhöht.
* **Flexibilität**: Der ZNS-Importer unterstützt nun sowohl `.zip` als auch `.dat` Dateien (z.B. direkte
`VEREIN01.dat`). Die UI (`pickZnsFile`) und das ViewModel wurden entsprechend erweitert.
@@ -35,7 +35,7 @@ class ConnectivityTracker : KoinComponent {
private suspend fun checkConnection(): Boolean {
return try {
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/ping")
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/api/ping/health")
response.status.value in 200..299
} catch (e: Exception) {
false
@@ -24,7 +24,7 @@ import kotlin.time.Duration.Companion.milliseconds
@Serializable
internal data class JobIdResponse(val jobId: String)
data class ImportStartResponse(val jobId: String)
@Serializable
internal data class JobStatusResponse(
@@ -57,8 +57,12 @@ class ZnsImportViewModel(
override fun startImport(mode: String) {
val filePath = state.selectedFilePath ?: return
val file = File(filePath)
if (!file.exists() || !file.name.endsWith(".zip", ignoreCase = true)) {
state = state.copy(errorMessage = "Bitte eine gültige .zip-Datei auswählen.")
if (!file.exists() || !(file.name.endsWith(".zip", ignoreCase = true) || file.name.endsWith(
".dat",
ignoreCase = true
))
) {
state = state.copy(errorMessage = "Bitte eine gültige .zip oder .dat-Datei auswählen.")
return
}
@@ -72,15 +76,17 @@ class ZnsImportViewModel(
val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") {
parameter("mode", mode)
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
val contentType =
if (file.name.endsWith(".zip", ignoreCase = true)) "application/zip" else "application/octet-stream"
setBody(MultiPartFormDataContent(formData {
append("file", file.readBytes(), Headers.build {
append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"")
append(HttpHeaders.ContentType, "application/zip")
append(HttpHeaders.ContentType, contentType)
})
}))
}
if (response.status == HttpStatusCode.Accepted) {
val body = json.decodeFromString<JobIdResponse>(response.bodyAsText())
val body = json.decodeFromString<ImportStartResponse>(response.bodyAsText())
state = state.copy(isUploading = false, jobId = body.jobId, jobStatus = "AUSSTEHEND")
startPolling(body.jobId)
} else {
@@ -1587,7 +1587,11 @@ fun ZnsImportWizardSection(
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.CloudUpload, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
Text("ZNS-Stammdaten Import (ZIP)", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(
"ZNS-Stammdaten Import (ZIP/DAT)",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(Modifier.weight(1f))
if (state.isFinished || state.errorMessage != null) {
TextButton(onClick = onReset) {
@@ -1609,14 +1613,14 @@ fun ZnsImportWizardSection(
value = state.selectedFilePath ?: "",
onValueChange = {},
readOnly = true,
placeholder = { Text("ZNS.zip auswählen...") },
placeholder = { Text("ZNS-Datei auswählen (.zip, .dat)...") },
modifier = Modifier.weight(1f),
singleLine = true,
textStyle = MaterialTheme.typography.bodySmall
)
Button(
onClick = {
val path = pickZipFile()
val path = pickZnsFile()
if (path != null) onFileSelect(path)
},
enabled = !state.isUploading
@@ -1714,10 +1718,10 @@ fun ZnsImportWizardSection(
}
}
private fun pickZipFile(): String? {
private fun pickZnsFile(): String? {
val chooser = JFileChooser()
chooser.dialogTitle = "ZNS.zip auswählen"
chooser.fileFilter = FileNameExtensionFilter("ZIP-Archiv (*.zip)", "zip")
chooser.dialogTitle = "ZNS-Datei auswählen"
chooser.fileFilter = FileNameExtensionFilter("ZNS Dateien (*.zip, *.dat)", "zip", "dat")
chooser.isAcceptAllFileFilterUsed = false
val result = chooser.showOpenDialog(null)
return if (result == JFileChooser.APPROVE_OPTION) chooser.selectedFile.absolutePath else null