feat: ZNS-Cloud-Sync und manuellen Veranstalter-Button im Wizard hinzugefügt
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 59s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m6s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m10s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 1m13s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m51s
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Failing after 59s
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 6m6s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m10s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 1m13s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m51s
Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
+14
@@ -10,11 +10,25 @@ data class ZnsImportState(
|
||||
val errors: List<String> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
val isFinished: Boolean = false,
|
||||
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
||||
val isSearching: Boolean = false,
|
||||
val lastSyncVersion: String? = null,
|
||||
val isSyncing: Boolean = false,
|
||||
)
|
||||
|
||||
data class ZnsRemoteVerein(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val oepsNummer: String,
|
||||
val ort: String?,
|
||||
val bundesland: String?,
|
||||
)
|
||||
|
||||
interface ZnsImportProvider {
|
||||
val state: ZnsImportState
|
||||
fun onFileSelected(path: String)
|
||||
fun startImport(mode: String = "FULL")
|
||||
fun searchRemote(query: String)
|
||||
fun syncFromCloud(onResult: (List<ZnsRemoteVerein>) -> Unit)
|
||||
fun reset()
|
||||
}
|
||||
|
||||
+78
-1
@@ -8,6 +8,7 @@ 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.domain.zns.ZnsRemoteVerein
|
||||
import at.mocode.frontend.core.network.NetworkConfig
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
@@ -22,7 +23,6 @@ import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ImportStartResponse(val jobId: String)
|
||||
|
||||
@@ -35,6 +35,15 @@ internal data class JobStatusResponse(
|
||||
val fehler: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class VereinRemoteDto(
|
||||
val vereinId: String,
|
||||
val vereinsNummer: String,
|
||||
val name: String,
|
||||
val ort: String? = null,
|
||||
val bundesland: String? = null,
|
||||
)
|
||||
|
||||
private val TERMINAL_STATES = setOf("ABGESCHLOSSEN", "FEHLER")
|
||||
private const val POLLING_INTERVAL_MS = 2000L
|
||||
private const val MAX_VISIBLE_ERRORS = 50
|
||||
@@ -98,6 +107,74 @@ class ZnsImportViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchRemote(query: String) {
|
||||
if (query.length < 3) {
|
||||
state = state.copy(remoteResults = emptyList())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSearching = true)
|
||||
try {
|
||||
val token = authTokenManager.authState.value.token
|
||||
// Wir nutzen den API-Gateway Pfad für masterdata
|
||||
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein/search") {
|
||||
parameter("q", query)
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val results = json.decodeFromString<List<VereinRemoteDto>>(response.bodyAsText())
|
||||
state = state.copy(
|
||||
isSearching = false,
|
||||
remoteResults = results.map {
|
||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
state = state.copy(isSearching = false, errorMessage = "Suche fehlgeschlagen: HTTP ${response.status.value}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isSearching = false, errorMessage = "Fehler bei der Cloud-Suche: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncFromCloud(onResult: (List<ZnsRemoteVerein>) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
state = state.copy(isSyncing = true, errorMessage = null)
|
||||
try {
|
||||
val token = authTokenManager.authState.value.token
|
||||
// Wir laden die Top 1000 Vereine für den Sync (einfache Implementierung)
|
||||
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein") {
|
||||
parameter("limit", 1000)
|
||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val results = json.decodeFromString<List<VereinRemoteDto>>(response.bodyAsText())
|
||||
val domainResults = results.map {
|
||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||
}
|
||||
|
||||
val now = java.time.LocalDateTime.now()
|
||||
val version = now.format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||
|
||||
state = state.copy(
|
||||
isSyncing = false,
|
||||
lastSyncVersion = version,
|
||||
isFinished = true
|
||||
)
|
||||
onResult(domainResults)
|
||||
} else {
|
||||
state = state.copy(isSyncing = false, errorMessage = "Sync fehlgeschlagen: HTTP ${response.status.value}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = state.copy(isSyncing = false, errorMessage = "Fehler beim Cloud-Sync: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPolling(jobId: String) {
|
||||
pollingJob?.cancel()
|
||||
pollingJob = viewModelScope.launch {
|
||||
|
||||
+65
-3
@@ -443,7 +443,56 @@ fun VeranstaltungKonfigV2(
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
// 2. Bestehende Veranstalter (Kompakt)
|
||||
// 2. Cloud Sync (Neu gemäß User-Wunsch)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
znsImporter.syncFromCloud { remoteList ->
|
||||
remoteList.forEach { remote ->
|
||||
StoreV2.vereine.find { it.oepsNummer == remote.oepsNummer }
|
||||
?: StoreV2.addVerein(remote.name, remote.oepsNummer, remote.ort ?: "")
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !znsState.isSyncing,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
|
||||
) {
|
||||
if (znsState.isSyncing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
color = MaterialTheme.colorScheme.onSecondary
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Synchronisiere...")
|
||||
} else {
|
||||
Icon(Icons.Default.CloudSync, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("ZNS-Daten-Sync")
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text(
|
||||
"ZNS-Daten geladen",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"[Version ${znsState.lastSyncVersion ?: "Kein Sync"}]",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (znsState.lastSyncVersion != null) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
|
||||
// 3. Bestehende Veranstalter (Kompakt)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) {
|
||||
var search by remember { mutableStateOf("") }
|
||||
val filteredVereine = remember(search) {
|
||||
@@ -465,7 +514,7 @@ fun VeranstaltungKonfigV2(
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(filteredVereine) { verein ->
|
||||
@@ -499,6 +548,18 @@ fun VeranstaltungKonfigV2(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Manueller Button für neuen Veranstalter
|
||||
OutlinedButton(
|
||||
onClick = { showVereinNeu = true },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
contentPadding = PaddingValues(12.dp),
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.primary)
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("+ Neuen Veranstalter anlegen", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
|
||||
if (showVereinNeu) {
|
||||
@@ -511,7 +572,8 @@ fun VeranstaltungKonfigV2(
|
||||
onCancel = { showVereinNeu = false },
|
||||
onVereinCreated = { newId ->
|
||||
showVereinNeu = false
|
||||
onVeranstalterCreated(newId)
|
||||
selectedVereinId = newId
|
||||
currentStep = 2 // Direkt zum nächsten Schritt
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user