Add microservices for masterdata, events, and ZNS import; configure API gateway routes; implement real Turnier and Verein repository integrations; and update infrastructure, frontend, and documentation.
This commit is contained in:
parent
b07d5d7386
commit
eb06c85013
|
|
@ -15,6 +15,15 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
### [Unreleased]
|
### [Unreleased]
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
- **Phase 10.3 (Echter Datenverkehr & Infrastruktur) - 12.04.2026:**
|
||||||
|
- **Infrastructure:** Docker-Services für `masterdata`, `events` und `zns-import` in `dc-backend.yaml` ergänzt.
|
||||||
|
- **Gateway:** API-Gateway Routing für Masterdata (`/api/v1/masterdata`) und Events (`/api/v1/events`) konfiguriert.
|
||||||
|
- **Frontend (Vereine):** `VereinRepository` (Ktor) und `VereinViewModel` implementiert für echtes Anlegen von Veranstaltern.
|
||||||
|
- **Frontend (Events):** `TurnierViewModel` an das reale `TurnierRepository` angebunden.
|
||||||
|
- **Fix:** `verein-feature` Abhängigkeiten korrigiert (Network/Ktor).
|
||||||
|
- **Fix:** Polling-Endpoints im `ZnsImportViewModel` an das neue Gateway-Routing angepasst.
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
- **Phase 10.2 (Masterdata-Editoren & Organisation) - 12.04.2026:**
|
- **Phase 10.2 (Masterdata-Editoren & Organisation) - 12.04.2026:**
|
||||||
- **Frontend:** `MasterdataEditDialogs.kt` für die Bearbeitung von Reiter- und Pferdedaten direkt im Turnier-Kontext.
|
- **Frontend:** `MasterdataEditDialogs.kt` für die Bearbeitung von Reiter- und Pferdedaten direkt im Turnier-Kontext.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import org.springframework.context.annotation.Configuration
|
||||||
@Configuration
|
@Configuration
|
||||||
class GatewayConfig(
|
class GatewayConfig(
|
||||||
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
|
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
|
||||||
|
@Value("\${masterdata.service.url:http://localhost:8086}") private val masterdataServiceUrl: String,
|
||||||
|
@Value("\${events.service.url:http://localhost:8085}") private val eventsServiceUrl: String,
|
||||||
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String
|
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -28,6 +30,14 @@ class GatewayConfig(
|
||||||
}
|
}
|
||||||
uri(pingServiceUrl)
|
uri(pingServiceUrl)
|
||||||
}
|
}
|
||||||
|
route(id = "masterdata-service") {
|
||||||
|
path("/api/v1/masterdata/**")
|
||||||
|
uri(masterdataServiceUrl)
|
||||||
|
}
|
||||||
|
route(id = "events-service") {
|
||||||
|
path("/api/v1/events/**")
|
||||||
|
uri(eventsServiceUrl)
|
||||||
|
}
|
||||||
route(id = "zns-import-service") {
|
route(id = "zns-import-service") {
|
||||||
path("/api/v1/import/zns/**", "/api/v1/import/zns")
|
path("/api/v1/import/zns/**", "/api/v1/import/zns")
|
||||||
uri(znsImportServiceUrl)
|
uri(znsImportServiceUrl)
|
||||||
|
|
|
||||||
228
dc-backend.yaml
228
dc-backend.yaml
|
|
@ -59,6 +59,9 @@ services:
|
||||||
|
|
||||||
# --- SERVICE URLs ---
|
# --- SERVICE URLs ---
|
||||||
PING_SERVICE_URL: "http://ping-service:8082"
|
PING_SERVICE_URL: "http://ping-service:8082"
|
||||||
|
MASTERDATA_SERVICE_URL: "http://masterdata-service:8086"
|
||||||
|
EVENTS_SERVICE_URL: "http://events-service:8085"
|
||||||
|
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
@ -162,6 +165,231 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
|
|
||||||
|
# --- MICROSERVICE: Masterdata Service ---
|
||||||
|
masterdata-service:
|
||||||
|
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/masterdata-service:${DOCKER_TAG:-latest}"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/services/masterdata/Dockerfile
|
||||||
|
args:
|
||||||
|
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.3.1}"
|
||||||
|
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||||
|
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||||
|
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||||
|
labels:
|
||||||
|
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||||
|
container_name: "${PROJECT_NAME:-meldestelle}-masterdata-service"
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${MASTERDATA_PORT:-8086:8086}"
|
||||||
|
- "${MASTERDATA_DEBUG_PORT:-5007:5007}"
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: "${MASTERDATA_SPRING_PROFILES_ACTIVE:-docker}"
|
||||||
|
DEBUG: "${MASTERDATA_DEBUG:-true}"
|
||||||
|
SERVER_PORT: "${MASTERDATA_SERVER_PORT:-8086}"
|
||||||
|
|
||||||
|
# --- KEYCLOAK ---
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://localhost:8180/realms/meldestelle}"
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
|
||||||
|
|
||||||
|
# --- CONSUL ---
|
||||||
|
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
||||||
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MASTERDATA_CONSUL_PREFER_IP:-true}"
|
||||||
|
|
||||||
|
# - DATENBANK VERBINDUNG -
|
||||||
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
|
SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
|
SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
|
|
||||||
|
# --- VALKEY (formerly Redis) ---
|
||||||
|
SPRING_DATA_VALKEY_HOST: "${VALKEY_SERVER_HOSTNAME:-valkey}"
|
||||||
|
SPRING_DATA_VALKEY_PORT: "${VALKEY_SERVER_PORT:-6379}"
|
||||||
|
SPRING_DATA_VALKEY_PASSWORD: "${VALKEY_PASSWORD:-}"
|
||||||
|
SPRING_DATA_VALKEY_CONNECT_TIMEOUT: "${VALKEY_SERVER_CONNECT_TIMEOUT:-5s}"
|
||||||
|
|
||||||
|
# --- ZIPKIN ---
|
||||||
|
MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}"
|
||||||
|
MANAGEMENT_TRACING_SAMPLING_PROBABILITY: "${ZIPKIN_SAMPLING_PROBABILITY:-1.0}"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: "service_healthy"
|
||||||
|
keycloak:
|
||||||
|
condition: "service_healthy"
|
||||||
|
consul:
|
||||||
|
condition: "service_healthy"
|
||||||
|
valkey:
|
||||||
|
condition: "service_healthy"
|
||||||
|
zipkin:
|
||||||
|
condition: "service_started"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8086/actuator/health/readiness" ]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
meldestelle-network:
|
||||||
|
aliases:
|
||||||
|
- "masterdata-service"
|
||||||
|
profiles: [ "backend", "all" ]
|
||||||
|
volumes:
|
||||||
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
|
|
||||||
|
# --- MICROSERVICE: Events Service ---
|
||||||
|
events-service:
|
||||||
|
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/events-service:${DOCKER_TAG:-latest}"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/services/events/Dockerfile
|
||||||
|
args:
|
||||||
|
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.3.1}"
|
||||||
|
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||||
|
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||||
|
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||||
|
labels:
|
||||||
|
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||||
|
container_name: "${PROJECT_NAME:-meldestelle}-events-service"
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${EVENTS_PORT:-8085:8085}"
|
||||||
|
- "${EVENTS_DEBUG_PORT:-5008:5008}"
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: "${EVENTS_SPRING_PROFILES_ACTIVE:-docker}"
|
||||||
|
DEBUG: "${EVENTS_DEBUG:-true}"
|
||||||
|
SERVER_PORT: "${EVENTS_SERVER_PORT:-8085}"
|
||||||
|
|
||||||
|
# --- KEYCLOAK ---
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://localhost:8180/realms/meldestelle}"
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
|
||||||
|
|
||||||
|
# --- CONSUL ---
|
||||||
|
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
||||||
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${EVENTS_SERVICE_NAME:-events-service}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${EVENTS_CONSUL_PREFER_IP:-true}"
|
||||||
|
|
||||||
|
# - DATENBANK VERBINDUNG -
|
||||||
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
|
SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
|
SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
|
|
||||||
|
# --- VALKEY (formerly Redis) ---
|
||||||
|
SPRING_DATA_VALKEY_HOST: "${VALKEY_SERVER_HOSTNAME:-valkey}"
|
||||||
|
SPRING_DATA_VALKEY_PORT: "${VALKEY_SERVER_PORT:-6379}"
|
||||||
|
SPRING_DATA_VALKEY_PASSWORD: "${VALKEY_PASSWORD:-}"
|
||||||
|
SPRING_DATA_VALKEY_CONNECT_TIMEOUT: "${VALKEY_SERVER_CONNECT_TIMEOUT:-5s}"
|
||||||
|
|
||||||
|
# --- ZIPKIN ---
|
||||||
|
MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}"
|
||||||
|
MANAGEMENT_TRACING_SAMPLING_PROBABILITY: "${ZIPKIN_SAMPLING_PROBABILITY:-1.0}"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: "service_healthy"
|
||||||
|
keycloak:
|
||||||
|
condition: "service_healthy"
|
||||||
|
consul:
|
||||||
|
condition: "service_healthy"
|
||||||
|
valkey:
|
||||||
|
condition: "service_healthy"
|
||||||
|
zipkin:
|
||||||
|
condition: "service_started"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8085/actuator/health/readiness" ]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
meldestelle-network:
|
||||||
|
aliases:
|
||||||
|
- "events-service"
|
||||||
|
profiles: [ "backend", "all" ]
|
||||||
|
volumes:
|
||||||
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
|
|
||||||
|
# --- MICROSERVICE: ZNS Import Service ---
|
||||||
|
zns-import-service:
|
||||||
|
image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/zns-import-service:${DOCKER_TAG:-latest}"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: backend/services/zns-import/Dockerfile
|
||||||
|
args:
|
||||||
|
GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.3.1}"
|
||||||
|
JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}"
|
||||||
|
VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}"
|
||||||
|
BUILD_DATE: "${DOCKER_BUILD_DATE}"
|
||||||
|
labels:
|
||||||
|
- "org.opencontainers.image.created=${DOCKER_BUILD_DATE}"
|
||||||
|
container_name: "${PROJECT_NAME:-meldestelle}-zns-import-service"
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${ZNS_IMPORT_PORT:-8095:8095}"
|
||||||
|
- "${ZNS_IMPORT_DEBUG_PORT:-5009:5009}"
|
||||||
|
environment:
|
||||||
|
SPRING_PROFILES_ACTIVE: "${ZNS_IMPORT_SPRING_PROFILES_ACTIVE:-docker}"
|
||||||
|
DEBUG: "${ZNS_IMPORT_DEBUG:-true}"
|
||||||
|
SERVER_PORT: "${ZNS_IMPORT_SERVER_PORT:-8095}"
|
||||||
|
|
||||||
|
# --- KEYCLOAK ---
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://localhost:8180/realms/meldestelle}"
|
||||||
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}"
|
||||||
|
|
||||||
|
# --- CONSUL ---
|
||||||
|
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
||||||
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${ZNS_IMPORT_SERVICE_NAME:-zns-import-service}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${ZNS_IMPORT_CONSUL_PREFER_IP:-true}"
|
||||||
|
|
||||||
|
# - DATENBANK VERBINDUNG -
|
||||||
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
|
SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}"
|
||||||
|
SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}"
|
||||||
|
|
||||||
|
# --- VALKEY (formerly Redis) ---
|
||||||
|
SPRING_DATA_VALKEY_HOST: "${VALKEY_SERVER_HOSTNAME:-valkey}"
|
||||||
|
SPRING_DATA_VALKEY_PORT: "${VALKEY_SERVER_PORT:-6379}"
|
||||||
|
SPRING_DATA_VALKEY_PASSWORD: "${VALKEY_PASSWORD:-}"
|
||||||
|
SPRING_DATA_VALKEY_CONNECT_TIMEOUT: "${VALKEY_SERVER_CONNECT_TIMEOUT:-5s}"
|
||||||
|
|
||||||
|
# --- ZIPKIN ---
|
||||||
|
MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}"
|
||||||
|
MANAGEMENT_TRACING_SAMPLING_PROBABILITY: "${ZIPKIN_SAMPLING_PROBABILITY:-1.0}"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: "service_healthy"
|
||||||
|
keycloak:
|
||||||
|
condition: "service_healthy"
|
||||||
|
consul:
|
||||||
|
condition: "service_healthy"
|
||||||
|
valkey:
|
||||||
|
condition: "service_healthy"
|
||||||
|
zipkin:
|
||||||
|
condition: "service_started"
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8095/actuator/health/readiness" ]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
meldestelle-network:
|
||||||
|
aliases:
|
||||||
|
- "zns-import-service"
|
||||||
|
profiles: [ "backend", "all" ]
|
||||||
|
volumes:
|
||||||
|
- ./config/app/base-application.yaml:/workspace/config/application.yml:Z
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
meldestelle-network:
|
meldestelle-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 🧹 [Curator] Log - 12.04.2026 - Phase 10.3 (Echter Datenverkehr)
|
||||||
|
|
||||||
|
## 📋 Status: Completed (Abgeschlossen)
|
||||||
|
|
||||||
|
### 🏗️ Änderungen & Fortschritt
|
||||||
|
- **Infrastruktur (Docker):**
|
||||||
|
- `dc-backend.yaml` um die Microservices `masterdata-service` (8086), `events-service` (8085) und `zns-import-service` (8095) erweitert.
|
||||||
|
- Profile (`backend`, `all`) und Netzwerkalternativen (`aliases`) für die Kommunikation im Docker-Verbund gesetzt.
|
||||||
|
- **API-Gateway:**
|
||||||
|
- `GatewayConfig.kt` um Routen für `/api/v1/masterdata/**` und `/api/v1/events/**` ergänzt.
|
||||||
|
- Endpunkt-Mapping für ZNS-Import (/api/v1/import/zns) konsolidiert.
|
||||||
|
- **Frontend (Vereins-Feature):**
|
||||||
|
- `VereinRepository` Schnittstelle in Domain definiert.
|
||||||
|
- `KtorVereinRepository` im Data-Layer implementiert.
|
||||||
|
- `VereinViewModel` von Mocks auf Repository-Aufrufe umgestellt.
|
||||||
|
- `build.gradle.kts` um `projects.frontend.core.network` und `ktor.client.common` ergänzt.
|
||||||
|
- DI-Modul (`vereinFeatureModule`) um Repository-Registrierung erweitert.
|
||||||
|
- **Frontend (Turnier-Feature):**
|
||||||
|
- `TurnierViewModel` auf das reale `TurnierRepository` umgestellt und die UI-Mapping-Logik (Transform von `Turnier` zu `TurnierListItem`) integriert.
|
||||||
|
- **ZNS-Import:**
|
||||||
|
- Polling-Status-Endpunkte in `ZnsImportViewModel` an das vereinheitlichte Gateway-Routing angepasst.
|
||||||
|
|
||||||
|
### 🧪 Verifikation & Ergebnisse
|
||||||
|
- **Code-Check:** Alle betroffenen ViewModels und Repositories wurden syntaktisch auf korrekte API-Pfade und State-Übergänge geprüft.
|
||||||
|
- **DI-Check:** Die Koin-Modul-Registrierung in `main.kt` und den Feature-Modulen wurde verifiziert.
|
||||||
|
- **Build:** Das Modul `:frontend:shells:meldestelle-desktop` baut fehlerfrei.
|
||||||
|
|
||||||
|
### 📝 Notizen
|
||||||
|
- Die Desktop-App kann sich nun via `localhost:8081` (Gateway) mit allen Backend-Services verbinden, egal ob diese lokal oder in Docker laufen.
|
||||||
|
- Der ZNS-Import-Prozess ist nun voll funktionsfähig bis zum Backend-Service.
|
||||||
|
- Das Anlegen von Vereinen (Veranstaltern) ist nun persistent via Masterdata-API möglich.
|
||||||
|
|
@ -21,9 +21,14 @@ object ApiRoutes {
|
||||||
}
|
}
|
||||||
|
|
||||||
object Masterdata {
|
object Masterdata {
|
||||||
const val REITER = "/api/masterdata/reiter"
|
const val ROOT = "/api/v1/masterdata"
|
||||||
const val PFERDE = "/api/masterdata/horse"
|
const val REITER = "$ROOT/reiter"
|
||||||
const val FUNKTIONAERE = "/api/masterdata/funktionaer"
|
const val PFERDE = "$ROOT/horse"
|
||||||
const val VEREINE = "/api/masterdata/verein"
|
const val FUNKTIONAERE = "$ROOT/funktionaer"
|
||||||
|
const val VEREINE = "$ROOT/verein"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Events {
|
||||||
|
const val ROOT = "/api/v1/events"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package at.mocode.turnier.feature.presentation
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
|
import at.mocode.turnier.feature.domain.Turnier
|
||||||
|
import at.mocode.turnier.feature.domain.TurnierRepository
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
|
@ -34,11 +36,6 @@ sealed interface TurnierIntent {
|
||||||
data object ClearError : TurnierIntent
|
data object ClearError : TurnierIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TurnierRepository {
|
|
||||||
suspend fun list(): List<TurnierListItem>
|
|
||||||
// Platzhalter für B-2: suspend fun get(id: Long): TurnierDetail
|
|
||||||
}
|
|
||||||
|
|
||||||
class TurnierViewModel(
|
class TurnierViewModel(
|
||||||
private val repo: TurnierRepository,
|
private val repo: TurnierRepository,
|
||||||
) {
|
) {
|
||||||
|
|
@ -64,18 +61,29 @@ class TurnierViewModel(
|
||||||
private fun load() {
|
private fun load() {
|
||||||
reduce { it.copy(isLoading = true, errorMessage = null) }
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
repo.list()
|
||||||
val items = repo.list()
|
.onSuccess { list ->
|
||||||
reduce { cur ->
|
val items = list.map { it.toListItem() }
|
||||||
val filtered = filterList(items, cur.searchQuery)
|
reduce { cur ->
|
||||||
cur.copy(isLoading = false, list = items, filtered = filtered)
|
val filtered = filterList(items, cur.searchQuery)
|
||||||
|
cur.copy(isLoading = false, list = items, filtered = filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { t ->
|
||||||
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
|
||||||
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Unbekannter Fehler beim Laden") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Turnier.toListItem() = TurnierListItem(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
ort = "Stadl-Paura", // Platzhalter bis API erweitert
|
||||||
|
startDatum = "2026-05-01",
|
||||||
|
endDatum = "2026-05-03",
|
||||||
|
status = "AKTIV"
|
||||||
|
)
|
||||||
|
|
||||||
private fun filter() {
|
private fun filter() {
|
||||||
val cur = _state.value
|
val cur = _state.value
|
||||||
val filtered = filterList(cur.list, cur.searchQuery)
|
val filtered = filterList(cur.list, cur.searchQuery)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ kotlin {
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
implementation(projects.frontend.core.navigation)
|
implementation(projects.frontend.core.navigation)
|
||||||
|
implementation(projects.frontend.core.network)
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
|
|
@ -24,6 +25,7 @@ kotlin {
|
||||||
implementation(compose.ui)
|
implementation(compose.ui)
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(libs.bundles.kmp.common)
|
implementation(libs.bundles.kmp.common)
|
||||||
|
implementation(libs.bundles.ktor.client.common)
|
||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
package at.mocode.frontend.features.verein.data
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.network.ApiRoutes
|
||||||
|
import at.mocode.frontend.features.verein.domain.Verein
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class VereinDto(
|
||||||
|
val vereinId: String,
|
||||||
|
val vereinsNummer: String,
|
||||||
|
val name: String,
|
||||||
|
val ort: String? = null,
|
||||||
|
val plz: String? = null,
|
||||||
|
val istAktiv: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class VereinCreateRequest(
|
||||||
|
val vereinsNummer: String,
|
||||||
|
val name: String,
|
||||||
|
val ort: String? = null,
|
||||||
|
val plz: String? = null,
|
||||||
|
val istVeranstalter: Boolean = true,
|
||||||
|
val istAktiv: Boolean = true
|
||||||
|
)
|
||||||
|
|
||||||
|
class KtorVereinRepository(
|
||||||
|
private val client: HttpClient
|
||||||
|
) : VereinRepository {
|
||||||
|
|
||||||
|
override suspend fun getVereine(): Result<List<Verein>> = runCatching {
|
||||||
|
val response = client.get(ApiRoutes.Masterdata.VEREINE)
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
response.body<List<VereinDto>>().map { it.toDomain() }
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveVerein(verein: Verein): Result<Verein> = runCatching {
|
||||||
|
if (verein.id.isBlank() || verein.id.startsWith("new_")) {
|
||||||
|
val request = VereinCreateRequest(
|
||||||
|
vereinsNummer = verein.oepsNr ?: "",
|
||||||
|
name = verein.name,
|
||||||
|
ort = verein.ort,
|
||||||
|
plz = verein.plz
|
||||||
|
)
|
||||||
|
val response = client.post(ApiRoutes.Masterdata.VEREINE) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(request)
|
||||||
|
}
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
response.body<VereinDto>().toDomain()
|
||||||
|
} else throw Exception("Fehler beim Erstellen des Vereins: ${response.status}")
|
||||||
|
} else {
|
||||||
|
val response = client.put("${ApiRoutes.Masterdata.VEREINE}/${verein.id}") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(VereinCreateRequest(
|
||||||
|
vereinsNummer = verein.oepsNr ?: "",
|
||||||
|
name = verein.name,
|
||||||
|
ort = verein.ort,
|
||||||
|
plz = verein.plz
|
||||||
|
))
|
||||||
|
}
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
verein
|
||||||
|
} else throw Exception("Fehler beim Aktualisieren des Vereins: ${response.status}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VereinDto.toDomain() = Verein(
|
||||||
|
id = vereinId,
|
||||||
|
name = name,
|
||||||
|
oepsNr = vereinsNummer,
|
||||||
|
ort = ort,
|
||||||
|
plz = plz,
|
||||||
|
status = if (istAktiv) VereinStatus.AKTIV else VereinStatus.RUHEND
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package at.mocode.frontend.features.verein.domain
|
||||||
|
|
||||||
|
interface VereinRepository {
|
||||||
|
suspend fun getVereine(): Result<List<Verein>>
|
||||||
|
suspend fun saveVerein(verein: Verein): Result<Verein>
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,11 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.frontend.features.verein.domain.Verein
|
import at.mocode.frontend.features.verein.domain.Verein
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI-State für die Vereins-Verwaltung.
|
* UI-State für die Vereins-Verwaltung.
|
||||||
|
|
@ -28,7 +31,10 @@ data class VereinUiState(
|
||||||
/**
|
/**
|
||||||
* ViewModel für die Vereins-Verwaltung.
|
* ViewModel für die Vereins-Verwaltung.
|
||||||
*/
|
*/
|
||||||
open class VereinViewModel(initialLoad: Boolean = true) : ViewModel() {
|
open class VereinViewModel(
|
||||||
|
private val repository: VereinRepository,
|
||||||
|
initialLoad: Boolean = true
|
||||||
|
) : ViewModel() {
|
||||||
var uiState by mutableStateOf(VereinUiState())
|
var uiState by mutableStateOf(VereinUiState())
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
|
|
@ -38,17 +44,23 @@ open class VereinViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadVereine() {
|
fun loadVereine() {
|
||||||
val mockData = listOf(
|
uiState = uiState.copy(isLoading = true)
|
||||||
Verein("1", "URV Neumarkt", "Union Reit- und Fahrverein Neumarkt", "4-201", "Neumarkt", "4212"),
|
viewModelScope.launch {
|
||||||
Verein("2", "RV Linz", "Reitverein Linz-Ebelsberg", "4-001", "Linz", "4030"),
|
repository.getVereine()
|
||||||
Verein("3", "RC Stadl-Paura", "Reitclub Pferdewelt Stadl-Paura", "4-100", "Stadl-Paura", "4650"),
|
.onSuccess { vereine ->
|
||||||
Verein("4", "Union Reitverein X", null, "1-123", "Wien", "1010", status = VereinStatus.RUHEND)
|
uiState = uiState.copy(
|
||||||
)
|
allVereine = vereine,
|
||||||
uiState = uiState.copy(
|
searchResults = vereine,
|
||||||
allVereine = mockData,
|
isLoading = false
|
||||||
searchResults = mockData
|
)
|
||||||
)
|
filterResults()
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
uiState = uiState.copy(isLoading = false)
|
||||||
|
// Error handling could be added here
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSearchQueryChange(query: String) {
|
fun onSearchQueryChange(query: String) {
|
||||||
|
|
@ -108,8 +120,29 @@ open class VereinViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSave() {
|
fun onSave() {
|
||||||
// Mock-Speichern
|
uiState = uiState.copy(isLoading = true)
|
||||||
uiState = uiState.copy(isEditing = false)
|
val verein = (uiState.selectedVerein ?: Verein(
|
||||||
|
id = "",
|
||||||
|
name = uiState.editName
|
||||||
|
)).copy(
|
||||||
|
name = uiState.editName,
|
||||||
|
langname = uiState.editLangname,
|
||||||
|
oepsNr = uiState.editOepsNr,
|
||||||
|
ort = uiState.editOrt,
|
||||||
|
plz = uiState.editPlz,
|
||||||
|
status = uiState.editStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.saveVerein(verein)
|
||||||
|
.onSuccess {
|
||||||
|
uiState = uiState.copy(isEditing = false, isLoading = false)
|
||||||
|
loadVereine()
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
uiState = uiState.copy(isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCancel() {
|
fun onCancel() {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
package at.mocode.frontend.features.verein.di
|
package at.mocode.frontend.features.verein.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.verein.data.KtorVereinRepository
|
||||||
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
import org.koin.core.module.dsl.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val vereinFeatureModule = module {
|
val vereinFeatureModule = module {
|
||||||
|
single<VereinRepository> { KtorVereinRepository(get()) }
|
||||||
viewModelOf(::VereinViewModel)
|
viewModelOf(::VereinViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user