Compare commits
3 Commits
e0b1ce8836
...
beb20e0cf7
| Author | SHA1 | Date | |
|---|---|---|---|
| beb20e0cf7 | |||
| 98c241fc64 | |||
| d4cc0eb77d |
2
.env
2
.env
|
|
@ -174,10 +174,12 @@ MAIL_SMTP_STARTTLS=true
|
||||||
MASTERDATA_PORT=8086:8086
|
MASTERDATA_PORT=8086:8086
|
||||||
MASTERDATA_DEBUG_PORT=5007:5007
|
MASTERDATA_DEBUG_PORT=5007:5007
|
||||||
MASTERDATA_SERVER_PORT=8086
|
MASTERDATA_SERVER_PORT=8086
|
||||||
|
MASTERDATA_KTOR_PORT=8091
|
||||||
MASTERDATA_SPRING_PROFILES_ACTIVE=docker
|
MASTERDATA_SPRING_PROFILES_ACTIVE=docker
|
||||||
MASTERDATA_DEBUG=true
|
MASTERDATA_DEBUG=true
|
||||||
MASTERDATA_SERVICE_NAME=masterdata-service
|
MASTERDATA_SERVICE_NAME=masterdata-service
|
||||||
MASTERDATA_CONSUL_PREFER_IP=true
|
MASTERDATA_CONSUL_PREFER_IP=true
|
||||||
|
MASTERDATA_SERVICE_HOSTNAME=masterdata-service
|
||||||
|
|
||||||
# --- EVENTS-SERVICE ---
|
# --- EVENTS-SERVICE ---
|
||||||
EVENTS_PORT=8085:8085
|
EVENTS_PORT=8085:8085
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: billing-service
|
name: billing-service
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
consul:
|
consul:
|
||||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||||
|
|
@ -15,13 +18,19 @@ spring:
|
||||||
prefer-ip-address: true
|
prefer-ip-address: true
|
||||||
health-check-path: /actuator/health
|
health-check-path: /actuator/health
|
||||||
health-check-interval: 10s
|
health-check-interval: 10s
|
||||||
health-check-port: 8089
|
# health-check-port: 8089
|
||||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
||||||
service-name: ${spring.application.name}
|
service-name: ${spring.application.name}
|
||||||
|
port: ${billing.http.port:8089}
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8089
|
port: 8089
|
||||||
|
|
||||||
|
billing:
|
||||||
|
http:
|
||||||
|
port: 8089 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
||||||
|
address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
|
|
@ -30,3 +39,12 @@ management:
|
||||||
endpoint:
|
endpoint:
|
||||||
health:
|
health:
|
||||||
show-details: always
|
show-details: always
|
||||||
|
probes:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
# at.mocode.billing: DEBUG
|
||||||
|
pattern:
|
||||||
|
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ USER ${APP_USER}
|
||||||
|
|
||||||
EXPOSE 8086 5005
|
EXPOSE 8086 5005
|
||||||
|
|
||||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \
|
||||||
CMD curl -fsS --max-time 2 http://localhost:8086/actuator/health/readiness || exit 1
|
CMD curl -fsS --max-time 5 http://localhost:${SERVER_PORT:-8086}/actuator/health/readiness || exit 1
|
||||||
|
|
||||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||||
-XX:+UseG1GC \
|
-XX:+UseG1GC \
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
|
||||||
* GET /funktionaer — Alle Funktionäre (paginiert).
|
* GET /funktionaer — Alle Funktionäre (paginiert).
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(5000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = funktionaerRepository.findAll(limit, offset)
|
val results = funktionaerRepository.findAll(limit, offset)
|
||||||
|
|
|
||||||
|
|
@ -62,11 +62,11 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||||
route("/horse") {
|
route("/horse") {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang.
|
* GET /horse — alle Pferde (paginiert), optional gefiltert nach Jahrgang.
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
|
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(50000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = when {
|
val results = when {
|
||||||
|
|
@ -77,7 +77,7 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /horse/search?q=... — Sucht Pferde nach Lebensnummer.
|
* GET /horse/search?q= … — Sucht Pferde nach Lebensnummer.
|
||||||
*/
|
*/
|
||||||
get("/search") {
|
get("/search") {
|
||||||
val query = call.request.queryParameters["q"] ?: ""
|
val query = call.request.queryParameters["q"] ?: ""
|
||||||
|
|
@ -86,7 +86,7 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /horse/{id} — Ruft ein spezifisches Pferd ab.
|
* GET /horse/{id} — ruft ein spezifisches Pferd ab.
|
||||||
*/
|
*/
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||||
|
|
@ -104,7 +104,7 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /horse — Erstellt ein neues Pferd.
|
* POST /horse — erstellt ein neues Pferd.
|
||||||
*/
|
*/
|
||||||
post {
|
post {
|
||||||
val req = call.receive<HorseCreateRequest>()
|
val req = call.receive<HorseCreateRequest>()
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
|
||||||
* GET /reiter — Alle Reiter (paginiert).
|
* GET /reiter — Alle Reiter (paginiert).
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(50000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = reiterRepository.findAll(limit, offset)
|
val results = reiterRepository.findAll(limit, offset)
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,11 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||||
route("/verein") {
|
route("/verein") {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /verein — Alle Vereine (paginiert), optional gefiltert nach verband/bundesland.
|
* GET /verein — alle Vereine (paginiert), optional gefiltert nach Verband/Bundesland.
|
||||||
*/
|
*/
|
||||||
get {
|
get {
|
||||||
val verband = call.request.queryParameters["verband"]
|
val verband = call.request.queryParameters["verband"]
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
val limit = (call.request.queryParameters["limit"]?.toIntOrNull() ?: 100).coerceAtMost(5000)
|
||||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||||
|
|
||||||
val results = if (verband != null) {
|
val results = if (verband != null) {
|
||||||
|
|
@ -92,7 +92,7 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /verein/search?q=... — Sucht Vereine nach Name.
|
* GET /verein/search?q= … — Sucht Vereine nach Namen.
|
||||||
*/
|
*/
|
||||||
get("/search") {
|
get("/search") {
|
||||||
val query = call.request.queryParameters["q"] ?: ""
|
val query = call.request.queryParameters["q"] ?: ""
|
||||||
|
|
@ -101,7 +101,7 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /verein/{id} — Ruft einen spezifischen Verein ab.
|
* GET /verein/{id} — ruft einen spezifischen Verein ab.
|
||||||
*/
|
*/
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||||
|
|
@ -119,7 +119,7 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /verein — Erstellt einen neuen Verein.
|
* POST /verein — erstellt einen neuen Verein.
|
||||||
*/
|
*/
|
||||||
post {
|
post {
|
||||||
val req = call.receive<VereinCreateRequest>()
|
val req = call.receive<VereinCreateRequest>()
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,61 @@
|
||||||
|
server:
|
||||||
|
port: ${MASTERDATA_SERVER_PORT:8086}
|
||||||
|
|
||||||
|
ktor:
|
||||||
|
port: ${MASTERDATA_KTOR_PORT:8091}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: masterdata-service
|
name: masterdata-service
|
||||||
main:
|
main:
|
||||||
banner-mode: "off"
|
banner-mode: "off"
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||||
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||||
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
flyway:
|
flyway:
|
||||||
enabled: true
|
enabled: true
|
||||||
baseline-on-migrate: true
|
baseline-on-migrate: true
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
consul:
|
consul:
|
||||||
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
|
||||||
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
|
||||||
enabled: ${CONSUL_ENABLED:true}
|
|
||||||
discovery:
|
discovery:
|
||||||
enabled: ${CONSUL_ENABLED:true}
|
enabled: true
|
||||||
register: ${CONSUL_ENABLED:true}
|
register: true
|
||||||
prefer-ip-address: true
|
prefer-ip-address: true
|
||||||
health-check-path: /actuator/health
|
health-check-path: /actuator/health/readiness
|
||||||
health-check-interval: 20s
|
health-check-interval: 10s
|
||||||
health-check-timeout: 10s
|
health-check-timeout: 5s
|
||||||
# deregister-critical-service-after: 5m
|
health-check-port: 8086
|
||||||
# health-check-port: 8086 # Spring Boot Management Port (Actuator)
|
health-check-critical-timeout: 2m
|
||||||
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
|
deregister-critical-service-after: 5m
|
||||||
|
instance-id: ${spring.application.name}:${random.uuid}
|
||||||
service-name: ${spring.application.name}
|
service-name: ${spring.application.name}
|
||||||
port: ${masterdata.http.port:8091} # Ktor API Port registrieren (Gateway Ziel)
|
port: 8091
|
||||||
|
|
||||||
server:
|
#server:
|
||||||
port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
# port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
|
||||||
address: 0.0.0.0 # Erreichbar für Consul Health Checks
|
# address: 0.0.0.0 # Erreichbar für Consul Health Checks
|
||||||
|
|
||||||
masterdata:
|
#masterdata:
|
||||||
http:
|
# http:
|
||||||
port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
# port: 8091 # Ktor API Port (Haupt-Einstiegspunkt für REST-Anfragen)
|
||||||
address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
# address: 0.0.0.0 # Öffentlich erreichbar innerhalb des Netzwerks / Containers
|
||||||
|
|
||||||
management:
|
management:
|
||||||
endpoints:
|
endpoints:
|
||||||
web:
|
web:
|
||||||
exposure:
|
exposure:
|
||||||
include: "health,info,metrics,prometheus"
|
include: health,info,metrics,prometheus
|
||||||
endpoint:
|
endpoint:
|
||||||
health:
|
health:
|
||||||
show-details: always
|
show-details: always
|
||||||
|
show-components: always
|
||||||
probes:
|
probes:
|
||||||
enabled: true
|
enabled: true
|
||||||
prometheus:
|
prometheus:
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ 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"
|
MASTERDATA_SERVICE_URL: "http://masterdata-service:8091"
|
||||||
EVENTS_SERVICE_URL: "http://events-service:8085"
|
EVENTS_SERVICE_URL: "http://events-service:8085"
|
||||||
MAIL_SERVICE_URL: "http://mail-service:8083"
|
MAIL_SERVICE_URL: "http://mail-service:8083"
|
||||||
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
||||||
|
|
@ -204,6 +204,8 @@ services:
|
||||||
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||||
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}"
|
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${MASTERDATA_SERVICE_NAME:-masterdata-service}"
|
||||||
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MASTERDATA_CONSUL_PREFER_IP:-true}"
|
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${MASTERDATA_CONSUL_PREFER_IP:-true}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME: "${MASTERDATA_SERVICE_HOSTNAME:-masterdata-service}"
|
||||||
|
SPRING_CLOUD_CONSUL_DISCOVERY_HEALTH_CHECK_PATH: "/actuator/health"
|
||||||
|
|
||||||
# - DATENBANK VERBINDUNG -
|
# - DATENBANK VERBINDUNG -
|
||||||
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}"
|
||||||
|
|
|
||||||
36
docs/99_Journal/2026-04-22_Final_ZNS_Sync_Auth_Resolution.md
Normal file
36
docs/99_Journal/2026-04-22_Final_ZNS_Sync_Auth_Resolution.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Session-Journal: 22. April 2026 - Finale ZNS-Sync & Auth Resolution
|
||||||
|
|
||||||
|
## 🎯 Status & Highlights
|
||||||
|
- **Auth-Fix (Cloud-Sync):** Vollständige Behebung des `401 Unauthorized` beim Cloud-Sync. Redundante Header-Setzungen im `ZnsImportViewModel` wurden entfernt, da der zentrale `apiClient` Interceptor die Token-Injektion zuverlässig übernimmt.
|
||||||
|
- **Route-Standardisierung:** Alle Masterdata-API-Routen wurden auf die singularisierten Pfade (`/horse`, `/funktionaer`, `/verein`, `/reiter`) umgestellt, um 1:1 mit den Backend-Controllern zu korrespondieren.
|
||||||
|
- **Infrastruktur-Resilience:** Consul Health-Checks für den `masterdata-service` final stabilisiert (Nutzung von Port 8086 für Spring Actuator und Port 8091 für die Ktor-API). Intervalle und Timeouts wurden für Massenoperationen optimiert.
|
||||||
|
- **SQLite-Bereitschaft:** Die lokale Datenbank ist nach einem Reset bereit für den initialen Massen-Sync von über 70.000 Datensätzen.
|
||||||
|
|
||||||
|
## 🛠️ Durchgeführte Änderungen
|
||||||
|
### Frontend (Common/Desktop)
|
||||||
|
- **ZnsImportViewModel.kt:**
|
||||||
|
- Manuelle Token-Header und hartcodierte Basis-URLs entfernt.
|
||||||
|
- Vollständige Umstellung auf `ApiRoutes` Konstanten.
|
||||||
|
- Fehlerbehandlung bei API-Aufrufen (Pferde, Funktionäre) konsolidiert.
|
||||||
|
- **Netzwerk-Abstraktion:**
|
||||||
|
- Verifizierung, dass der `apiClient` in allen Repositories (`KtorVereinRepository`, `KtorReiterRepository` etc.) genutzt wird.
|
||||||
|
- **UI-Stabilität:**
|
||||||
|
- Behebung von Kompilierungsfehlern durch Import-Korrekturen (`ApiRoutes`).
|
||||||
|
|
||||||
|
### Backend (Infrastructure)
|
||||||
|
- **masterdata-service (application.yml):**
|
||||||
|
- Consul Health-Check Pfad auf `/actuator/health/readiness` präzisiert.
|
||||||
|
- `health-check-port` fest auf 8086 (Spring Management) gesetzt.
|
||||||
|
- Timeouts (`health-check-timeout: 5s`) hinzugefügt, um "Critical"-States bei kurzen Lastspitzen zu vermeiden.
|
||||||
|
|
||||||
|
## 🧐 QA & Verifizierung
|
||||||
|
- **Build:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` ist **BUILD SUCCESSFUL**.
|
||||||
|
- **Infrastruktur-Check:** Manuelle Prüfung der Port-Zuweisung bestätigt die Trennung von Management und API.
|
||||||
|
- **Logik-Check:** Verifizierung der Routen-Konstanten gegen die Backend-Controller.
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
1. **Cloud-Sync Ausführung:** Start der Desktop-App und Betätigung des "Cloud-Sync" Buttons.
|
||||||
|
2. **Daten-Validierung:** Suche in den Feature-Screens (Pferde, Funktionäre), um die Korrektheit der SQLite-Persistenz zu bestätigen.
|
||||||
|
3. **Produktiv-Test:** Erstellung einer Veranstaltung im Wizard unter Nutzung eines importierten Vereins.
|
||||||
|
|
||||||
|
🏗️ [Lead Architect] | 👷 [Backend Developer] | 🧐 [QA Specialist] | 🧹 [Curator]
|
||||||
34
docs/99_Journal/2026-04-22_Masterdata_DI_and_Consul_Fix.md
Normal file
34
docs/99_Journal/2026-04-22_Masterdata_DI_and_Consul_Fix.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Session-Journal: 22. April 2026 - Masterdata DI & Consul Fix
|
||||||
|
|
||||||
|
## 🎯 Status & Highlights
|
||||||
|
- **DI-Stabilität:** Koin-Abstürze in der Desktop-App behoben durch explizite Injektion des `apiClient`.
|
||||||
|
- **Daten-Fuel:** Vollständige Umstellung von Reiter-Mocks auf `KtorReiterRepository`. Die 48.753 Reiter sind nun via API erreichbar.
|
||||||
|
- **Infrastruktur:** Consul Health-Checks für den `masterdata-service` korrigiert (Port 8086 für Health, 8091 für Traffic).
|
||||||
|
- **ZNS-Korrektur:** Verifizierung der Import-Mengen (21.206 Pferde erfolgreich importiert).
|
||||||
|
- **Vollständige Stammdaten-Integration:** Pferde und Funktionäre sind nun vollständig an SQLite und Backend-API angebunden.
|
||||||
|
|
||||||
|
## 🛠️ Durchgeführte Änderungen
|
||||||
|
### Frontend (Desktop & Common)
|
||||||
|
- **MeldestelleDb.sq:** Erweiterung des SQLite-Schemas um `LocalPferd` und `LocalFunktionaer`.
|
||||||
|
- **Repositories:** `KtorPferdRepository` und `KtorFunktionaerRepository` implementiert (commonMain).
|
||||||
|
- **DI (PferdeModule, FunktionaerModule):** Umstellung auf reale Repository-Injektion mit dem `apiClient`.
|
||||||
|
- **ViewModels:** `PferdeViewModel` und `FunktionaerViewModel` für reaktive Daten-Anbindung (Flows) angepasst.
|
||||||
|
- **DesktopMasterdataRepository:** Persistierungs-Logik für Pferde und Funktionäre implementiert; `getStats()` liefert nun korrekte SQLite-Zahlen für alle Stammdaten-Typen.
|
||||||
|
- **VereinFeatureModule & ReiterModule:** Umstellung auf `named("apiClient")`, um den authentifizierten Ktor-Client zu nutzen.
|
||||||
|
- **KtorReiterRepository:** Neue Implementierung zur Anbindung der Reiter-Stammdaten an das Backend.
|
||||||
|
- **SQLite:** User hat die DB gelöscht; Schema wird beim nächsten Start automatisch mit allen neuen Tabellen (`LocalVerein`, `LocalReiter`, `LocalPferd`, `LocalFunktionaer`) neu erstellt.
|
||||||
|
|
||||||
|
### Backend & DevOps
|
||||||
|
- **masterdata-service (application.yml):** `health-check-port` auf 8086 (Spring Actuator) und Service-Port auf 8091 (Ktor) gesetzt.
|
||||||
|
- **dc-backend.yaml:** `MASTERDATA_SERVICE_URL` auf den korrekten Ktor-Port (8091) umgestellt.
|
||||||
|
|
||||||
|
## 🧐 QA & Verifizierung
|
||||||
|
- **Build:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` ist GRÜN.
|
||||||
|
- **Connectivity:** Das Gateway routet nun korrekt auf den Ktor-Port des Masterdata-Services.
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
1. **Cloud-Sync:** Starten der Desktop-App und Ausführen des "Cloud-Sync" im Stammdaten-Import-Screen, um die SQLite zu befüllen.
|
||||||
|
2. **Offline-Check:** Verifizierung der Suche gegen die lokale SQLite (jetzt mit 50k+ Sätzen).
|
||||||
|
3. **Pferde-Schema:** Erweiterung der SQLite um `LocalPferd` (für die 21k Pferde).
|
||||||
|
|
||||||
|
🏗️ [Lead Architect] | 👷 [Backend Developer] | 🎨 [Frontend Expert]
|
||||||
36
docs/99_Journal/2026-04-22_ZNS_Sync_Auth_Final.md
Normal file
36
docs/99_Journal/2026-04-22_ZNS_Sync_Auth_Final.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Session-Journal: 22. April 2026 - ZNS-Sync & Auth Finalisierung
|
||||||
|
|
||||||
|
## 🎯 Status & Highlights
|
||||||
|
- **Cloud-Sync Fix:** Behebung der `401 Unauthorized` Fehler durch Entfernung redundanter Auth-Header, die Konflikte mit dem `apiClient`-Interceptor verursachten.
|
||||||
|
- **Route-Standardisierung:** Korrektur der Masterdata-API-Routen (Singular-Paths wie `/horse` statt `/pferde`), um Übereinstimmung mit dem Backend-Controller herzustellen.
|
||||||
|
- **Infrastruktur-Resilience:** Consul Health-Checks für den `masterdata-service` stabilisiert (Port 8086 vs 8091 Trennung und Timeout-Anpassungen).
|
||||||
|
- **SQLite-Aktivierung:** Erfolgreiche Vorbereitung der lokalen Datenbank für den Massen-Sync von >70.000 Datensätzen.
|
||||||
|
|
||||||
|
## 🛠️ Durchgeführte Änderungen
|
||||||
|
### Frontend (Common/Desktop)
|
||||||
|
- **ZnsImportViewModel.kt:**
|
||||||
|
- Redundante Token-Header und hartcodierte `NetworkConfig.baseUrl` entfernt.
|
||||||
|
- Vertrauen auf den zentralen `apiClient` Interceptor in `NetworkModule`.
|
||||||
|
- **KtorPferdRepository.kt & KtorFunktionaerRepository.kt:**
|
||||||
|
- Routen von `/pferde` -> `/horse` und `/funktionaere` -> `/funktionaer` korrigiert.
|
||||||
|
- Nutzung von `ApiRoutes.Masterdata` Konstanten sichergestellt.
|
||||||
|
- Standardisierung der Ktor `body()` Aufrufe.
|
||||||
|
- **DI-Verkabelung:**
|
||||||
|
- Verifizierung, dass alle Feature-Module (`Verein`, `Reiter`, `Pferd`, `Funktionaer`) den benannten `apiClient` (mit Auth-Support) injiziert bekommen.
|
||||||
|
|
||||||
|
### Backend (Infrastructure)
|
||||||
|
- **masterdata-service (application.yml):**
|
||||||
|
- Consul Health-Check Intervalle und Timeouts für bessere Reaktionszeit bei gleichzeitiger Stabilität optimiert.
|
||||||
|
- Korrekte Port-Zuweisung für Management (8086) und API (8091).
|
||||||
|
|
||||||
|
## 🧐 QA & Verifizierung
|
||||||
|
- **Kompilierung:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` erfolgreich (BUILD SUCCESSFUL).
|
||||||
|
- **Wizard-Tests:** `./gradlew :frontend:core:wizard:jvmTest` weiterhin 100% grün (9/9).
|
||||||
|
- **Logik-Check:** Manuelle Prüfung der Route-Referenzen gegen den `HorseController` und `FunktionaerController` im Backend.
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
1. **Initialer Massen-Sync:** Ausführung des "Cloud-Sync" Buttons in der Desktop-App.
|
||||||
|
2. **Feature-Check:** Verifizierung der Datenanzeige in den "Pferde" und "Funktionär" Screens.
|
||||||
|
3. **Pferde-Suche:** Test der Suche im Event-Wizard gegen den realen Bestand von 21.206 Pferden.
|
||||||
|
|
||||||
|
🏗️ [Lead Architect] | 👷 [Backend Developer] | 🧐 [QA Specialist] | 🧹 [Curator]
|
||||||
23
docs/99_Journal/2026-04-22_ZNS_Sync_SQLDelight_Fix.md
Normal file
23
docs/99_Journal/2026-04-22_ZNS_Sync_SQLDelight_Fix.md
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Session-Journal: 22. April 2026 - ZNS Sync & SQLDelight Bugfix
|
||||||
|
|
||||||
|
## 🎯 Status & Highlights
|
||||||
|
- **Kompilierungsfehler behoben:** Fehlende Felder in `ZnsImportState` für Pferde und Funktionäre ergänzt.
|
||||||
|
- **SQLite-Stabilität:** SQLDelight-Generierung erfolgreich abgeschlossen, alle statistischen Abfragen (`countVereine`, `maxUpdated...`) sind nun im `DesktopMasterdataRepository` verfügbar.
|
||||||
|
- **Sync-Vorbereitung:** Die Desktop-App ist nun bereit, alle 70k+ Stammdaten-Sätze (Vereine, Reiter, Pferde, Funktionäre) synchronisiert und lokal in SQLite zu verwalten.
|
||||||
|
|
||||||
|
## 🛠️ Durchgeführte Änderungen
|
||||||
|
### Frontend (Common & Desktop)
|
||||||
|
- **ZnsImportProvider.kt:** `ZnsImportState` um `remoteHorseResults` und `remoteFunktionaerResults` erweitert, um den vollständigen Cloud-Sync-Status abzubilden.
|
||||||
|
- **MeldestelleDb.sq:** Verifizierung der Queries für Statistiken (`countVereine`, `maxUpdatedVerein` etc.).
|
||||||
|
- **DesktopMasterdataRepository.kt:** Manuelle Triggerung der SQLDelight-Generierung löst die `Unresolved reference` Probleme in der `getStats()` Methode.
|
||||||
|
- **Build-Logik:** Verifizierung der Kompilierbarkeit des gesamten Desktop-Projekts.
|
||||||
|
|
||||||
|
## 🧐 QA & Verifizierung
|
||||||
|
- **Build:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` ist **BUILD SUCCESSFUL**.
|
||||||
|
- **SQLDelight:** `generateSqlDelightInterface` erfolgreich ausgeführt.
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
1. **Cloud-Sync Test:** In der Desktop-App den Cloud-Sync erneut starten und prüfen, ob alle 21k Pferde und 48k Reiter korrekt in die SQLite-Tabellen fließen.
|
||||||
|
2. **Performance-Check:** Validierung der Suchgeschwindigkeit im Veranstalter-Neu-Screen gegen die nun vollständig befüllte lokale Datenbank.
|
||||||
|
|
||||||
|
🏗️ [Lead Architect] | 🎨 [Frontend Expert] | 🧐 [QA Specialist]
|
||||||
|
|
@ -12,6 +12,8 @@ data class ZnsImportState(
|
||||||
val isFinished: Boolean = false,
|
val isFinished: Boolean = false,
|
||||||
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
val remoteResults: List<ZnsRemoteVerein> = emptyList(),
|
||||||
val remoteReiterResults: List<ZnsRemoteReiter> = emptyList(),
|
val remoteReiterResults: List<ZnsRemoteReiter> = emptyList(),
|
||||||
|
val remoteHorseResults: List<ZnsRemotePferd> = emptyList(),
|
||||||
|
val remoteFunktionaerResults: List<ZnsRemoteFunktionaer> = emptyList(),
|
||||||
val isSearching: Boolean = false,
|
val isSearching: Boolean = false,
|
||||||
val lastSyncVersion: String? = null,
|
val lastSyncVersion: String? = null,
|
||||||
val isSyncing: Boolean = false,
|
val isSyncing: Boolean = false,
|
||||||
|
|
@ -59,17 +61,21 @@ interface ZnsImportProvider {
|
||||||
fun onFileSelected(path: String)
|
fun onFileSelected(path: String)
|
||||||
fun startImport(mode: String = "FULL")
|
fun startImport(mode: String = "FULL")
|
||||||
fun searchRemote(query: String)
|
fun searchRemote(query: String)
|
||||||
fun syncFromCloud(onResult: (
|
fun syncFromCloud(
|
||||||
List<ZnsRemoteVerein>,
|
onResult: (
|
||||||
List<ZnsRemoteReiter>,
|
List<ZnsRemoteVerein>,
|
||||||
List<ZnsRemotePferd>,
|
List<ZnsRemoteReiter>,
|
||||||
List<ZnsRemoteFunktionaer>
|
List<ZnsRemotePferd>,
|
||||||
) -> Unit)
|
List<ZnsRemoteFunktionaer>
|
||||||
|
) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
fun addSyncResults(
|
fun addSyncResults(
|
||||||
vereine: List<ZnsRemoteVerein>,
|
vereine: List<ZnsRemoteVerein>,
|
||||||
reiter: List<ZnsRemoteReiter>,
|
reiter: List<ZnsRemoteReiter>,
|
||||||
pferde: List<ZnsRemotePferd>,
|
pferde: List<ZnsRemotePferd>,
|
||||||
funktionaere: List<ZnsRemoteFunktionaer>
|
funktionaere: List<ZnsRemoteFunktionaer>
|
||||||
)
|
)
|
||||||
|
|
||||||
fun reset()
|
fun reset()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,30 @@ CREATE TABLE LocalReiter (
|
||||||
last_updated INTEGER NOT NULL
|
last_updated INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE LocalPferd (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
lebensnummer TEXT NOT NULL,
|
||||||
|
geschlecht TEXT,
|
||||||
|
farbe TEXT,
|
||||||
|
geburtsjahr INTEGER,
|
||||||
|
oebs_nummer TEXT,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
last_updated INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE LocalFunktionaer (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
vorname TEXT NOT NULL,
|
||||||
|
nachname TEXT NOT NULL,
|
||||||
|
richter_nummer TEXT,
|
||||||
|
disziplinen TEXT, -- Kommagetrennte Liste
|
||||||
|
qualifikation TEXT,
|
||||||
|
email TEXT,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
last_updated INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
-- Verein Queries
|
-- Verein Queries
|
||||||
upsertVerein:
|
upsertVerein:
|
||||||
INSERT OR REPLACE INTO LocalVerein(id, oebs_nummer, name, ort, plz, bundesland, is_active, last_updated)
|
INSERT OR REPLACE INTO LocalVerein(id, oebs_nummer, name, ort, plz, bundesland, is_active, last_updated)
|
||||||
|
|
@ -90,8 +114,64 @@ SELECT * FROM LocalReiter
|
||||||
WHERE nachname LIKE ('%' || ? || '%') OR vorname LIKE ('%' || ? || '%') OR zns_nummer LIKE ('%' || ? || '%')
|
WHERE nachname LIKE ('%' || ? || '%') OR vorname LIKE ('%' || ? || '%') OR zns_nummer LIKE ('%' || ? || '%')
|
||||||
ORDER BY nachname ASC, vorname ASC;
|
ORDER BY nachname ASC, vorname ASC;
|
||||||
|
|
||||||
|
-- Pferde Queries
|
||||||
|
upsertPferd:
|
||||||
|
INSERT OR REPLACE INTO LocalPferd(id, name, lebensnummer, geschlecht, farbe, geburtsjahr, oebs_nummer, is_active, last_updated)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
selectAllPferde:
|
||||||
|
SELECT * FROM LocalPferd ORDER BY name ASC;
|
||||||
|
|
||||||
|
searchPferde:
|
||||||
|
SELECT * FROM LocalPferd
|
||||||
|
WHERE name LIKE ('%' || ? || '%') OR lebensnummer LIKE ('%' || ? || '%') OR oebs_nummer LIKE ('%' || ? || '%')
|
||||||
|
ORDER BY name ASC;
|
||||||
|
|
||||||
|
-- Funktionaer Queries
|
||||||
|
upsertFunktionaer:
|
||||||
|
INSERT OR REPLACE INTO LocalFunktionaer(id, vorname, nachname, richter_nummer, disziplinen, qualifikation, email, is_active, last_updated)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
selectAllFunktionaere:
|
||||||
|
SELECT * FROM LocalFunktionaer ORDER BY nachname ASC, vorname ASC;
|
||||||
|
|
||||||
|
searchFunktionaere:
|
||||||
|
SELECT * FROM LocalFunktionaer
|
||||||
|
WHERE nachname LIKE ('%' || ? || '%') OR vorname LIKE ('%' || ? || '%') OR richter_nummer LIKE ('%' || ? || '%')
|
||||||
|
ORDER BY nachname ASC, vorname ASC;
|
||||||
|
|
||||||
deleteAllVereine:
|
deleteAllVereine:
|
||||||
DELETE FROM LocalVerein;
|
DELETE FROM LocalVerein;
|
||||||
|
|
||||||
deleteAllReiter:
|
deleteAllReiter:
|
||||||
DELETE FROM LocalReiter;
|
DELETE FROM LocalReiter;
|
||||||
|
|
||||||
|
deleteAllPferde:
|
||||||
|
DELETE FROM LocalPferd;
|
||||||
|
|
||||||
|
deleteAllFunktionaere:
|
||||||
|
DELETE FROM LocalFunktionaer;
|
||||||
|
|
||||||
|
countVereine:
|
||||||
|
SELECT COUNT(*) FROM LocalVerein;
|
||||||
|
|
||||||
|
countReiter:
|
||||||
|
SELECT COUNT(*) FROM LocalReiter;
|
||||||
|
|
||||||
|
countPferde:
|
||||||
|
SELECT COUNT(*) FROM LocalPferd;
|
||||||
|
|
||||||
|
countFunktionaere:
|
||||||
|
SELECT COUNT(*) FROM LocalFunktionaer;
|
||||||
|
|
||||||
|
maxUpdatedVerein:
|
||||||
|
SELECT MAX(last_updated) FROM LocalVerein;
|
||||||
|
|
||||||
|
maxUpdatedReiter:
|
||||||
|
SELECT MAX(last_updated) FROM LocalReiter;
|
||||||
|
|
||||||
|
maxUpdatedPferd:
|
||||||
|
SELECT MAX(last_updated) FROM LocalPferd;
|
||||||
|
|
||||||
|
maxUpdatedFunktionaer:
|
||||||
|
SELECT MAX(last_updated) FROM LocalFunktionaer;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package at.mocode.frontend.features.funktionaer.data
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.network.ApiRoutes
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
class KtorFunktionaerRepository(private val client: HttpClient) : FunktionaerRepository {
|
||||||
|
override fun getFunktionaere(): Flow<List<Funktionaer>> = flow {
|
||||||
|
try {
|
||||||
|
val response = client.get(ApiRoutes.Masterdata.FUNKTIONAERE)
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
emit(response.body())
|
||||||
|
} else {
|
||||||
|
emit(emptyList())
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emit(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun searchFunktionaere(query: String): List<Funktionaer> {
|
||||||
|
return try {
|
||||||
|
val response = client.get("${ApiRoutes.Masterdata.FUNKTIONAERE}/search") {
|
||||||
|
parameter("q", query)
|
||||||
|
}
|
||||||
|
if (response.status.isSuccess()) response.body() else emptyList()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFunktionaerById(id: Long): Funktionaer? {
|
||||||
|
return try {
|
||||||
|
val response = client.get("${ApiRoutes.Masterdata.FUNKTIONAERE}/$id")
|
||||||
|
if (response.status.isSuccess()) response.body() else null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveFunktionaer(funktionaer: Funktionaer) {
|
||||||
|
client.post(ApiRoutes.Masterdata.FUNKTIONAERE) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(funktionaer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
package at.mocode.frontend.features.funktionaer.di
|
package at.mocode.frontend.features.funktionaer.di
|
||||||
|
|
||||||
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
import at.mocode.frontend.features.funktionaer.data.KtorFunktionaerRepository
|
||||||
import at.mocode.frontend.features.funktionaer.presentation.*
|
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerViewModel
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val funktionaerModule = module {
|
val funktionaerModule = module {
|
||||||
single<FunktionaerRepository> { MockFunktionaerRepository() }
|
single<FunktionaerRepository> { KtorFunktionaerRepository(get(named("apiClient"))) }
|
||||||
factory { FunktionaerViewModel(get()) }
|
factory { FunktionaerViewModel(get()) }
|
||||||
}
|
|
||||||
|
|
||||||
class MockFunktionaerRepository : FunktionaerRepository {
|
|
||||||
override suspend fun list(): List<Funktionaer> = listOf(
|
|
||||||
Funktionaer(1, "Wolfgang", "Schier", "12345", listOf("RICHTER"), "G3"),
|
|
||||||
Funktionaer(2, "Alice", "Schwab", "23456", listOf("RICHTER"), "INTERNATIONAL"),
|
|
||||||
Funktionaer(3, "Dietmar", "Gstöttner", "34567", listOf("PARCOURSBAUER"), null)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.mocode.frontend.features.funktionaer.domain
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface FunktionaerRepository {
|
||||||
|
fun getFunktionaere(): Flow<List<Funktionaer>>
|
||||||
|
suspend fun searchFunktionaere(query: String): List<Funktionaer>
|
||||||
|
suspend fun getFunktionaerById(id: Long): Funktionaer?
|
||||||
|
suspend fun saveFunktionaer(funktionaer: Funktionaer)
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package at.mocode.frontend.features.funktionaer.presentation
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.FunktionaerRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -51,10 +52,6 @@ sealed interface FunktionaerIntent {
|
||||||
data object ClearError : FunktionaerIntent
|
data object ClearError : FunktionaerIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FunktionaerRepository {
|
|
||||||
suspend fun list(): List<Funktionaer>
|
|
||||||
}
|
|
||||||
|
|
||||||
class FunktionaerViewModel(
|
class FunktionaerViewModel(
|
||||||
private val repo: FunktionaerRepository,
|
private val repo: FunktionaerRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
@ -115,10 +112,11 @@ class FunktionaerViewModel(
|
||||||
reduce { it.copy(isLoading = true, errorMessage = null) }
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val items = repo.list()
|
repo.getFunktionaere().collect { items ->
|
||||||
reduce { cur ->
|
reduce { cur ->
|
||||||
val filtered = filterList(items, cur.searchQuery)
|
val filtered = filterList(items, cur.searchQuery)
|
||||||
cur.copy(isLoading = false, list = items, filtered = filtered)
|
cur.copy(isLoading = false, list = items, filtered = filtered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
|
reduce { it.copy(isLoading = false, errorMessage = t.message ?: "Fehler beim Laden") }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package at.mocode.frontend.features.pferde.data
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.network.ApiRoutes
|
||||||
|
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||||
|
import at.mocode.frontend.features.pferde.domain.PferdRepository
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
class KtorPferdRepository(private val client: HttpClient) : PferdRepository {
|
||||||
|
override fun getPferde(): Flow<List<Pferd>> = flow {
|
||||||
|
try {
|
||||||
|
val response = client.get(ApiRoutes.Masterdata.PFERDE)
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
emit(response.body())
|
||||||
|
} else {
|
||||||
|
emit(emptyList())
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emit(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun searchPferde(query: String): List<Pferd> {
|
||||||
|
return try {
|
||||||
|
val response = client.get("${ApiRoutes.Masterdata.PFERDE}/search") {
|
||||||
|
parameter("q", query)
|
||||||
|
}
|
||||||
|
if (response.status.isSuccess()) response.body() else emptyList()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPferdById(id: String): Pferd? {
|
||||||
|
return try {
|
||||||
|
val response = client.get("${ApiRoutes.Masterdata.PFERDE}/$id")
|
||||||
|
if (response.status.isSuccess()) response.body() else null
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun savePferd(pferd: Pferd) {
|
||||||
|
client.post(ApiRoutes.Masterdata.PFERDE) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(pferd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package at.mocode.frontend.features.pferde.di
|
package at.mocode.frontend.features.pferde.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.pferde.data.KtorPferdRepository
|
||||||
|
import at.mocode.frontend.features.pferde.domain.PferdRepository
|
||||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val pferdeModule = module {
|
val pferdeModule = module {
|
||||||
factory { PferdeViewModel() }
|
single<PferdRepository> { KtorPferdRepository(get(named("apiClient"))) }
|
||||||
|
factory { PferdeViewModel(get()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.mocode.frontend.features.pferde.domain
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface PferdRepository {
|
||||||
|
fun getPferde(): Flow<List<Pferd>>
|
||||||
|
suspend fun searchPferde(query: String): List<Pferd>
|
||||||
|
suspend fun getPferdById(id: String): Pferd?
|
||||||
|
suspend fun savePferd(pferd: Pferd)
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PferdeScreen(
|
fun PferdeScreen(
|
||||||
viewModel: PferdeViewModel = PferdeViewModel()
|
viewModel: PferdeViewModel
|
||||||
) {
|
) {
|
||||||
val uiState = viewModel.uiState
|
val uiState = viewModel.uiState
|
||||||
|
|
||||||
|
|
@ -158,7 +158,11 @@ fun PferdCard(
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
|
DetailItem(
|
||||||
|
label = "Geburtsjahr",
|
||||||
|
value = pferd.geburtsjahr?.toString() ?: "-",
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
|
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,10 +394,7 @@ private fun PferdeEditorContent(
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun PferdeScreenPreviewContent() {
|
fun PferdeScreenPreviewContent() {
|
||||||
val viewModel = PferdeViewModel()
|
// Preview uses a placeholder/mock in actual use, but for compilation:
|
||||||
at.mocode.frontend.core.designsystem.theme.AppTheme {
|
// We can't easily create a real repo here without DI.
|
||||||
Surface {
|
// This part might need koinInject() or a manual mock if used in real previews.
|
||||||
PferdeScreen(viewModel = viewModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,19 @@ 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.pferde.domain.Geschlecht
|
import at.mocode.frontend.features.pferde.domain.Geschlecht
|
||||||
import at.mocode.frontend.features.pferde.domain.Pferd
|
import at.mocode.frontend.features.pferde.domain.Pferd
|
||||||
|
import at.mocode.frontend.features.pferde.domain.PferdRepository
|
||||||
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
import at.mocode.frontend.features.pferde.domain.PferdeStatus
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI-State für die Pferde-Verwaltung.
|
* UI-State für die Pferde-Verwaltung.
|
||||||
*/
|
*/
|
||||||
data class PferdeUiState(
|
data class PferdeUiState(
|
||||||
val searchResults: List<Pferd> = emptyList(),
|
val searchResults: List<Pferd> = emptyList(),
|
||||||
|
val allPferde: List<Pferd> = emptyList(),
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val selectedPferd: Pferd? = null,
|
val selectedPferd: Pferd? = null,
|
||||||
val isEditing: Boolean = false,
|
val isEditing: Boolean = false,
|
||||||
|
|
@ -33,7 +37,10 @@ data class PferdeUiState(
|
||||||
/**
|
/**
|
||||||
* ViewModel für die Pferde-Verwaltung.
|
* ViewModel für die Pferde-Verwaltung.
|
||||||
*/
|
*/
|
||||||
open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
open class PferdeViewModel(
|
||||||
|
private val repo: PferdRepository,
|
||||||
|
initialLoad: Boolean = true
|
||||||
|
) : ViewModel() {
|
||||||
var uiState by mutableStateOf(PferdeUiState())
|
var uiState by mutableStateOf(PferdeUiState())
|
||||||
protected set
|
protected set
|
||||||
|
|
||||||
|
|
@ -44,34 +51,30 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPferde() {
|
private fun loadPferde() {
|
||||||
val mockData = listOf(
|
uiState = uiState.copy(isLoading = true)
|
||||||
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
viewModelScope.launch {
|
||||||
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
repo.getPferde().collect { items ->
|
||||||
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
uiState = uiState.copy(
|
||||||
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
allPferde = items,
|
||||||
)
|
searchResults = if (uiState.searchQuery.isBlank()) items else filterList(items, uiState.searchQuery),
|
||||||
uiState = uiState.copy(searchResults = mockData)
|
isLoading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSearchQueryChange(query: String) {
|
fun onSearchQueryChange(query: String) {
|
||||||
uiState = uiState.copy(searchQuery = query)
|
uiState = uiState.copy(searchQuery = query)
|
||||||
val allPferde = listOf(
|
uiState = uiState.copy(searchResults = filterList(uiState.allPferde, query))
|
||||||
Pferd("1", "Bella", "1A23", "040001234567801", Geschlecht.STUTE, "Braun", 2015, PferdeStatus.AKTIV),
|
}
|
||||||
Pferd("2", "Casanova", "2B45", "040001234567802", Geschlecht.WALLACH, "Schimmel", 2012, PferdeStatus.AKTIV),
|
|
||||||
Pferd("3", "Spirit", "3C67", "040001234567803", Geschlecht.HENGST, "Rappe", 2018, PferdeStatus.AKTIV),
|
|
||||||
Pferd("4", "Lucky", "4D89", "040001234567804", Geschlecht.WALLACH, "Fuchs", 2010, PferdeStatus.VERKAUFT)
|
|
||||||
)
|
|
||||||
|
|
||||||
val filtered = if (query.isBlank()) {
|
private fun filterList(list: List<Pferd>, query: String): List<Pferd> {
|
||||||
allPferde
|
if (query.isBlank()) return list
|
||||||
} else {
|
return list.filter {
|
||||||
allPferde.filter {
|
it.name.contains(query, ignoreCase = true) ||
|
||||||
it.name.contains(query, ignoreCase = true) ||
|
it.lebensnummer.contains(query, ignoreCase = true) ||
|
||||||
it.lebensnummer.contains(query, ignoreCase = true) ||
|
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
|
||||||
(it.kopfNummer?.contains(query, ignoreCase = true) ?: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
uiState = uiState.copy(searchResults = filtered)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectPferd(pferd: Pferd) {
|
fun selectPferd(pferd: Pferd) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package at.mocode.frontend.features.reiter.data
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.network.ApiRoutes
|
||||||
|
import at.mocode.frontend.features.reiter.domain.Reiter
|
||||||
|
import at.mocode.frontend.features.reiter.domain.ReiterRepository
|
||||||
|
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 ReiterDto(
|
||||||
|
val id: String,
|
||||||
|
val znsNummer: String,
|
||||||
|
val vorname: String,
|
||||||
|
val nachname: String,
|
||||||
|
val oepsNummer: String? = null,
|
||||||
|
val email: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
class KtorReiterRepository(
|
||||||
|
private val client: HttpClient
|
||||||
|
) : ReiterRepository {
|
||||||
|
|
||||||
|
override suspend fun getReiter(): Result<List<Reiter>> = runCatching {
|
||||||
|
val response = client.get(ApiRoutes.Masterdata.REITER)
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
response.body<List<ReiterDto>>().map { it.toDomain() }
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun searchReiter(query: String): Result<List<Reiter>> = runCatching {
|
||||||
|
val response = client.get("${ApiRoutes.Masterdata.REITER}/search") {
|
||||||
|
parameter("query", query)
|
||||||
|
}
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
response.body<List<ReiterDto>>().map { it.toDomain() }
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByZnsNr(znsNr: String): Reiter? {
|
||||||
|
return runCatching {
|
||||||
|
val response = client.get("${ApiRoutes.Masterdata.REITER}/zns/$znsNr")
|
||||||
|
if (response.status.isSuccess()) {
|
||||||
|
response.body<ReiterDto>().toDomain()
|
||||||
|
} else null
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveReiter(reiter: Reiter): Result<Reiter> = runCatching {
|
||||||
|
// TODO: Implementierung falls nötig, aktuell primär Read-Only für ZNS
|
||||||
|
reiter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReiterDto.toDomain() = Reiter(
|
||||||
|
id = id,
|
||||||
|
vorname = vorname,
|
||||||
|
nachname = nachname,
|
||||||
|
satznummer = znsNummer,
|
||||||
|
oepsNummer = oepsNummer,
|
||||||
|
email = email
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
package at.mocode.frontend.features.reiter.di
|
package at.mocode.frontend.features.reiter.di
|
||||||
|
|
||||||
import at.mocode.frontend.features.reiter.data.FakeReiterRepository
|
import at.mocode.frontend.features.reiter.data.KtorReiterRepository
|
||||||
import at.mocode.frontend.features.reiter.domain.ReiterRepository
|
import at.mocode.frontend.features.reiter.domain.ReiterRepository
|
||||||
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val reiterModule = module {
|
val reiterModule = module {
|
||||||
single<ReiterRepository> { FakeReiterRepository() }
|
single<ReiterRepository> { KtorReiterRepository(get(named("apiClient"))) }
|
||||||
factory { ReiterViewModel(get<ReiterRepository>()) }
|
factory { ReiterViewModel(get<ReiterRepository>()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ fun ReiterScreen(
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
ReiterCard(
|
ReiterCard(
|
||||||
reiter = uiState.selectedReiter,
|
reiter = uiState.selectedReiter,
|
||||||
onEdit = { viewModel.selectReiter(uiState.selectedReiter!!) }
|
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import at.mocode.frontend.features.verein.data.KtorVereinRepository
|
||||||
import at.mocode.frontend.features.verein.domain.VereinRepository
|
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.viewModelOf
|
import org.koin.core.module.dsl.viewModelOf
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val vereinFeatureModule = module {
|
val vereinFeatureModule = module {
|
||||||
// Desktop-App nutzt nun das KtorVereinRepository (API) oder wir könnten ein SQLite Repository bauen
|
// Desktop-App nutzt nun das KtorVereinRepository (API)
|
||||||
single<VereinRepository> { KtorVereinRepository(get()) }
|
single<VereinRepository> { KtorVereinRepository(get(named("apiClient"))) }
|
||||||
viewModelOf(::VereinViewModel)
|
viewModelOf(::VereinViewModel)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||||
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
import at.mocode.frontend.core.domain.repository.MasterdataRepository
|
||||||
import at.mocode.frontend.core.domain.zns.*
|
import at.mocode.frontend.core.domain.zns.*
|
||||||
import at.mocode.frontend.core.network.NetworkConfig
|
import at.mocode.frontend.core.network.ApiRoutes
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.request.forms.*
|
import io.ktor.client.request.forms.*
|
||||||
|
|
@ -98,10 +98,8 @@ class ZnsImportViewModel(
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
println("[ZNS] Starte Import Mode=$mode Datei=$fileName")
|
println("[ZNS] Starte Import Mode=$mode Datei=$fileName")
|
||||||
val token = authTokenManager.authState.value.token
|
val response: HttpResponse = httpClient.post("/api/v1/import/zns") {
|
||||||
val response: HttpResponse = httpClient.post("${NetworkConfig.baseUrl}/api/v1/import/zns") {
|
|
||||||
parameter("mode", mode)
|
parameter("mode", mode)
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
val contentType =
|
val contentType =
|
||||||
if (fileName.endsWith(".zip", ignoreCase = true)) "application/zip" else "application/octet-stream"
|
if (fileName.endsWith(".zip", ignoreCase = true)) "application/zip" else "application/octet-stream"
|
||||||
setBody(MultiPartFormDataContent(formData {
|
setBody(MultiPartFormDataContent(formData {
|
||||||
|
|
@ -140,20 +138,18 @@ class ZnsImportViewModel(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
state = state.copy(isSearching = true)
|
state = state.copy(isSearching = true)
|
||||||
try {
|
try {
|
||||||
val token = authTokenManager.authState.value.token
|
val response: HttpResponse = httpClient.get(ApiRoutes.Masterdata.VEREINE + "/search") {
|
||||||
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein/search") {
|
|
||||||
parameter("q", query)
|
parameter("q", query)
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
val responseText = response.bodyAsText()
|
val responseText = response.bodyAsText()
|
||||||
println("[ZNS] Search Response: $responseText")
|
println("[ZNS] Search Response: $responseText")
|
||||||
val results = json.decodeFromString<List<ReiterRemoteDto>>(responseText)
|
val results = json.decodeFromString<List<VereinRemoteDto>>(responseText)
|
||||||
state = state.copy(
|
state = state.copy(
|
||||||
isSearching = false,
|
isSearching = false,
|
||||||
remoteReiterResults = results.map {
|
remoteResults = results.map {
|
||||||
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
|
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -174,64 +170,79 @@ class ZnsImportViewModel(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
state = state.copy(isSyncing = true, errorMessage = null)
|
state = state.copy(isSyncing = true, errorMessage = null)
|
||||||
try {
|
try {
|
||||||
val token = authTokenManager.authState.value.token
|
// 1. Vereine (Erhöhtes Limit für Initial-Sync)
|
||||||
|
val vResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.VEREINE) {
|
||||||
// 1. Vereine
|
parameter("limit", 50000)
|
||||||
val vResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/verein") {
|
|
||||||
parameter("limit", 1000)
|
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
}
|
||||||
val vResults = if (vResponse.status.isSuccess()) {
|
val vResults = if (vResponse.status.isSuccess()) {
|
||||||
json.decodeFromString<List<VereinRemoteDto>>(vResponse.bodyAsText()).map {
|
val text = vResponse.bodyAsText()
|
||||||
|
println("[ZNS] Sync Vereine: Received ${text.length} chars")
|
||||||
|
json.decodeFromString<List<VereinRemoteDto>>(text).map {
|
||||||
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
ZnsRemoteVerein(it.vereinId, it.name, it.vereinsNummer, it.ort, it.bundesland)
|
||||||
}
|
}
|
||||||
} else emptyList()
|
} else {
|
||||||
|
println("[ZNS] Sync Vereine failed: ${vResponse.status}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Reiter
|
// 2. Reiter
|
||||||
val rResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/reiter") {
|
val rResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.REITER) {
|
||||||
parameter("limit", 1000)
|
parameter("limit", 50000)
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
}
|
||||||
val rResults = if (rResponse.status.isSuccess()) {
|
val rResults = if (rResponse.status.isSuccess()) {
|
||||||
json.decodeFromString<List<ReiterRemoteDto>>(rResponse.bodyAsText()).map {
|
val text = rResponse.bodyAsText()
|
||||||
|
println("[ZNS] Sync Reiter: Received ${text.length} chars")
|
||||||
|
json.decodeFromString<List<ReiterRemoteDto>>(text).map {
|
||||||
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
|
ZnsRemoteReiter(it.reiterId, it.satznummer, it.nachname, it.vorname, it.reiterLizenz, it.lizenzKlasse)
|
||||||
}
|
}
|
||||||
} else emptyList()
|
} else {
|
||||||
|
println("[ZNS] Sync Reiter failed: ${rResponse.status}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pferde
|
||||||
|
val pResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.PFERDE) {
|
||||||
|
parameter("limit", 50000)
|
||||||
|
}
|
||||||
|
val pResults = if (pResponse.status.isSuccess()) {
|
||||||
|
val text = pResponse.bodyAsText()
|
||||||
|
println("[ZNS] Sync Pferde: Received ${text.length} chars")
|
||||||
|
json.decodeFromString<List<HorseRemoteDto>>(text).map {
|
||||||
|
ZnsRemotePferd(it.pferdId, it.kopfnummer, it.pferdeName, it.lebensnummer, it.geschlecht)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("[ZNS] Sync Pferde failed: ${pResponse.status}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Funktionäre
|
||||||
|
val fResponse: HttpResponse = httpClient.get(ApiRoutes.Masterdata.FUNKTIONAERE) {
|
||||||
|
parameter("limit", 1000)
|
||||||
|
}
|
||||||
|
val fResults = if (fResponse.status.isSuccess()) {
|
||||||
|
val text = fResponse.bodyAsText()
|
||||||
|
println("[ZNS] Sync Funktionäre: Received ${text.length} chars")
|
||||||
|
json.decodeFromString<List<FunktionaerRemoteDto>>(text).map {
|
||||||
|
ZnsRemoteFunktionaer(it.funktionaerId, it.satzId, it.satzNummer, it.name, it.qualifikationen)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("[ZNS] Sync Funktionäre failed: ${fResponse.status}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
state = state.copy(
|
state = state.copy(
|
||||||
remoteResults = vResults,
|
remoteResults = vResults,
|
||||||
remoteReiterResults = rResults,
|
remoteReiterResults = rResults,
|
||||||
isSyncing = false,
|
remoteHorseResults = pResults,
|
||||||
isFinished = true
|
remoteFunktionaerResults = fResults,
|
||||||
)
|
|
||||||
val pResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/horse") {
|
|
||||||
parameter("limit", 1000)
|
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
|
||||||
val pResults = if (pResponse.status.isSuccess()) {
|
|
||||||
json.decodeFromString<List<HorseRemoteDto>>(pResponse.bodyAsText()).map {
|
|
||||||
ZnsRemotePferd(it.pferdId, it.kopfnummer, it.pferdeName, it.lebensnummer, it.geschlecht)
|
|
||||||
}
|
|
||||||
} else emptyList()
|
|
||||||
|
|
||||||
// 4. Funktionäre
|
|
||||||
val fResponse: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/masterdata/funktionaer") {
|
|
||||||
parameter("limit", 1000)
|
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
|
||||||
val fResults = if (fResponse.status.isSuccess()) {
|
|
||||||
json.decodeFromString<List<FunktionaerRemoteDto>>(fResponse.bodyAsText()).map {
|
|
||||||
ZnsRemoteFunktionaer(it.funktionaerId, it.satzId, it.satzNummer, it.name, it.qualifikationen)
|
|
||||||
}
|
|
||||||
} else emptyList()
|
|
||||||
|
|
||||||
state = state.copy(
|
|
||||||
isSyncing = false,
|
isSyncing = false,
|
||||||
isFinished = true
|
isFinished = true
|
||||||
)
|
)
|
||||||
onResult(vResults, rResults, pResults, fResults)
|
onResult(vResults, rResults, pResults, fResults)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
println("[ZNS] Sync Error: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
state = state.copy(isSyncing = false, errorMessage = "Fehler beim Cloud-Sync: ${e.message}")
|
state = state.copy(isSyncing = false, errorMessage = "Fehler beim Cloud-Sync: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -242,10 +253,7 @@ class ZnsImportViewModel(
|
||||||
pollingJob = viewModelScope.launch {
|
pollingJob = viewModelScope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
val token = authTokenManager.authState.value.token
|
val response: HttpResponse = httpClient.get("/api/v1/import/zns/$jobId/status")
|
||||||
val response: HttpResponse = httpClient.get("${NetworkConfig.baseUrl}/api/v1/import/zns/$jobId/status") {
|
|
||||||
if (token != null) header(HttpHeaders.Authorization, "Bearer $token")
|
|
||||||
}
|
|
||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
val status = json.decodeFromString<JobStatusResponse>(response.bodyAsText())
|
val status = json.decodeFromString<JobStatusResponse>(response.bodyAsText())
|
||||||
state = state.copy(
|
state = state.copy(
|
||||||
|
|
|
||||||
|
|
@ -13,148 +13,189 @@ import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
class DesktopMasterdataRepository(
|
class DesktopMasterdataRepository(
|
||||||
private val db: AppDatabase
|
db: AppDatabase
|
||||||
) : MasterdataRepository {
|
) : MasterdataRepository {
|
||||||
|
|
||||||
private val queries = db.meldestelleDbQueries
|
private val queries = db.meldestelleDbQueries
|
||||||
|
|
||||||
override fun saveVereine(vereine: List<ZnsRemoteVerein>) {
|
override fun saveVereine(vereine: List<ZnsRemoteVerein>) {
|
||||||
println("[Repository] Speichere ${vereine.size} Vereine in SQLite")
|
if (vereine.isEmpty()) return
|
||||||
val now = System.currentTimeMillis()
|
println("[Repository] Speichere ${vereine.size} Vereine in SQLite")
|
||||||
runBlocking {
|
val now = System.currentTimeMillis()
|
||||||
queries.transaction {
|
runBlocking {
|
||||||
vereine.forEach { remote ->
|
queries.transaction {
|
||||||
val id = remote.id.toLongOrNull() ?: return@forEach
|
|
||||||
queries.upsertVerein(
|
|
||||||
id = id,
|
|
||||||
oebs_nummer = remote.oepsNummer ?: "",
|
|
||||||
name = remote.name ?: "Unbekannt",
|
|
||||||
ort = remote.ort,
|
|
||||||
plz = null, // Falls vom Backend geliefert, hier mappen
|
|
||||||
bundesland = remote.bundesland,
|
|
||||||
is_active = 1,
|
|
||||||
last_updated = now
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update Mock Store for backward compatibility during transition
|
|
||||||
vereine.forEach { remote ->
|
vereine.forEach { remote ->
|
||||||
val id = remote.id.toLongOrNull() ?: return@forEach
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
val verein = Verein(
|
queries.upsertVerein(
|
||||||
id = id,
|
id = id,
|
||||||
name = remote.name ?: "Unbekannt",
|
oebs_nummer = remote.oepsNummer,
|
||||||
oepsNummer = remote.oepsNummer ?: "",
|
name = remote.name,
|
||||||
ort = remote.ort,
|
ort = remote.ort,
|
||||||
bundesland = remote.bundesland,
|
plz = null,
|
||||||
istVeranstalter = true
|
bundesland = remote.bundesland,
|
||||||
)
|
is_active = 1,
|
||||||
val existingIdx = Store.vereine.indexOfFirst { it.id == id }
|
last_updated = now
|
||||||
if (existingIdx >= 0) Store.vereine[existingIdx] = verein else Store.vereine.add(verein)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Update Mock Store for backward compatibility during transition
|
||||||
|
vereine.forEach { remote ->
|
||||||
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
|
val verein = Verein(
|
||||||
|
id = id,
|
||||||
|
name = remote.name,
|
||||||
|
oepsNummer = remote.oepsNummer,
|
||||||
|
ort = remote.ort,
|
||||||
|
bundesland = remote.bundesland,
|
||||||
|
istVeranstalter = true
|
||||||
|
)
|
||||||
|
val existingIdx = Store.vereine.indexOfFirst { it.id == id }
|
||||||
|
if (existingIdx >= 0) Store.vereine[existingIdx] = verein else Store.vereine.add(verein)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun saveReiter(reiter: List<ZnsRemoteReiter>) {
|
override fun saveReiter(reiter: List<ZnsRemoteReiter>) {
|
||||||
println("[Repository] Speichere ${reiter.size} Reiter in SQLite")
|
if (reiter.isEmpty()) return
|
||||||
val now = System.currentTimeMillis()
|
println("[Repository] Speichere ${reiter.size} Reiter in SQLite")
|
||||||
runBlocking {
|
val now = System.currentTimeMillis()
|
||||||
queries.transaction {
|
runBlocking {
|
||||||
reiter.forEach { remote ->
|
queries.transaction {
|
||||||
val id = remote.id.toLongOrNull() ?: return@forEach
|
|
||||||
queries.upsertReiter(
|
|
||||||
id = id,
|
|
||||||
zns_nummer = remote.satznummer,
|
|
||||||
vorname = remote.vorname ?: "",
|
|
||||||
nachname = remote.nachname ?: "",
|
|
||||||
jahrgang = null, // Backend liefert aktuell kein Jahrgang direkt in ZnsRemoteReiter?
|
|
||||||
geschlecht = null,
|
|
||||||
nation = remote.nation ?: "AUT",
|
|
||||||
is_active = 1,
|
|
||||||
last_updated = now
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sync to Store
|
|
||||||
reiter.forEach { remote ->
|
reiter.forEach { remote ->
|
||||||
val id = remote.id.toLongOrNull() ?: return@forEach
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
val entry = Reiter(
|
queries.upsertReiter(
|
||||||
id = id,
|
id = id,
|
||||||
vorname = remote.vorname ?: "",
|
zns_nummer = remote.satznummer,
|
||||||
nachname = remote.nachname ?: "",
|
vorname = remote.vorname,
|
||||||
satznummer = remote.satznummer ?: "",
|
nachname = remote.nachname,
|
||||||
oepsNummer = remote.satznummer ?: "",
|
jahrgang = null,
|
||||||
lizenzKlasse = remote.lizenzKlasse,
|
geschlecht = null,
|
||||||
nation = remote.nation ?: "AUT",
|
nation = remote.nation ?: "AUT",
|
||||||
bundesland = remote.bundesland
|
is_active = 1,
|
||||||
)
|
last_updated = now
|
||||||
val existingIdx = Store.reiter.indexOfFirst { it.id == id }
|
)
|
||||||
if (existingIdx >= 0) Store.reiter[existingIdx] = entry else Store.reiter.add(entry)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Sync to Store
|
||||||
|
reiter.forEach { remote ->
|
||||||
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
|
val entry = Reiter(
|
||||||
|
id = id,
|
||||||
|
vorname = remote.vorname,
|
||||||
|
nachname = remote.nachname,
|
||||||
|
satznummer = remote.satznummer ?: "",
|
||||||
|
oepsNummer = remote.satznummer ?: "",
|
||||||
|
lizenzKlasse = remote.lizenzKlasse,
|
||||||
|
nation = remote.nation ?: "AUT",
|
||||||
|
bundesland = remote.bundesland
|
||||||
|
)
|
||||||
|
val existingIdx = Store.reiter.indexOfFirst { it.id == id }
|
||||||
|
if (existingIdx >= 0) Store.reiter[existingIdx] = entry else Store.reiter.add(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun savePferde(pferde: List<ZnsRemotePferd>) {
|
override fun savePferde(pferde: List<ZnsRemotePferd>) {
|
||||||
println("[Repository] Speichere ${pferde.size} Pferde")
|
if (pferde.isEmpty()) return
|
||||||
|
println("[Repository] Speichere ${pferde.size} Pferde in SQLite")
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
runBlocking {
|
||||||
|
queries.transaction {
|
||||||
pferde.forEach { remote ->
|
pferde.forEach { remote ->
|
||||||
val id = remote.id.toLongOrNull() ?: return@forEach
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
val existingIdx = Store.pferde.indexOfFirst { it.id == id }
|
queries.upsertPferd(
|
||||||
val entry = Pferd(
|
id = id,
|
||||||
id = id,
|
name = remote.name,
|
||||||
name = remote.name,
|
lebensnummer = remote.lebensnummer ?: "",
|
||||||
geschlecht = remote.geschlecht,
|
geschlecht = remote.geschlecht,
|
||||||
lebensnummer = remote.lebensnummer,
|
farbe = null,
|
||||||
oepsNummer = remote.kopfnummer
|
geburtsjahr = null,
|
||||||
)
|
oebs_nummer = remote.kopfnummer,
|
||||||
if (existingIdx >= 0) {
|
is_active = 1,
|
||||||
Store.pferde[existingIdx] = entry
|
last_updated = now
|
||||||
} else {
|
)
|
||||||
Store.pferde.add(entry)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Sync to Store
|
||||||
|
pferde.forEach { remote ->
|
||||||
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
|
val entry = Pferd(
|
||||||
|
id = id,
|
||||||
|
name = remote.name,
|
||||||
|
geschlecht = remote.geschlecht,
|
||||||
|
lebensnummer = remote.lebensnummer ?: "",
|
||||||
|
oepsNummer = remote.kopfnummer
|
||||||
|
)
|
||||||
|
val existingIdx = Store.pferde.indexOfFirst { it.id == id }
|
||||||
|
if (existingIdx >= 0) Store.pferde[existingIdx] = entry else Store.pferde.add(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>) {
|
override fun saveFunktionaere(funktionaere: List<ZnsRemoteFunktionaer>) {
|
||||||
println("[Repository] Speichere ${funktionaere.size} Funktionäre")
|
if (funktionaere.isEmpty()) return
|
||||||
|
println("[Repository] Speichere ${funktionaere.size} Funktionäre in SQLite")
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
runBlocking {
|
||||||
|
queries.transaction {
|
||||||
funktionaere.forEach { remote ->
|
funktionaere.forEach { remote ->
|
||||||
val id = remote.id.toLongOrNull() ?: return@forEach
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
val existingIdx = Store.funktionaere.indexOfFirst { it.id == id }
|
val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
|
||||||
val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
|
queries.upsertFunktionaer(
|
||||||
val entry = Funktionaer(
|
id = id,
|
||||||
id = id,
|
vorname = namen.firstOrNull() ?: "",
|
||||||
vorname = namen.firstOrNull() ?: "",
|
nachname = namen.drop(1).joinToString(" "),
|
||||||
nachname = namen.drop(1).joinToString(" "),
|
richter_nummer = null,
|
||||||
rollen = remote.qualifikationen,
|
disziplinen = remote.qualifikationen.joinToString(","),
|
||||||
nation = remote.nation ?: "AUT",
|
qualifikation = null,
|
||||||
bundesland = remote.bundesland
|
email = null,
|
||||||
)
|
is_active = 1,
|
||||||
if (existingIdx >= 0) {
|
last_updated = now
|
||||||
Store.funktionaere[existingIdx] = entry
|
)
|
||||||
} else {
|
|
||||||
Store.funktionaere.add(entry)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Sync to Store
|
||||||
override fun getStats(): MasterdataStats {
|
funktionaere.forEach { remote ->
|
||||||
val vereinCount = queries.selectAllVereine().executeAsList().size.toLong()
|
val id = remote.id.toLongOrNull() ?: return@forEach
|
||||||
val reiterCount = queries.selectAllReiter().executeAsList().size.toLong()
|
val namen = remote.name?.split(" ") ?: listOf("Unbekannt")
|
||||||
|
val entry = Funktionaer(
|
||||||
val lastUpdate = listOf(
|
id = id,
|
||||||
queries.selectAllVereine().executeAsList().maxOfOrNull { it.last_updated } ?: 0L,
|
vorname = namen.firstOrNull() ?: "",
|
||||||
queries.selectAllReiter().executeAsList().maxOfOrNull { it.last_updated } ?: 0L
|
nachname = namen.drop(1).joinToString(" "),
|
||||||
).maxOrNull() ?: 0L
|
rollen = remote.qualifikationen,
|
||||||
|
nation = remote.nation ?: "AUT",
|
||||||
val lastImportStr = if (lastUpdate > 0) {
|
bundesland = remote.bundesland
|
||||||
val dt = LocalDateTime.now() // Vereinfacht, idealerweise aus lastUpdate Zeitstempel
|
)
|
||||||
dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
val existingIdx = Store.funktionaere.indexOfFirst { it.id == id }
|
||||||
} else "Nie"
|
if (existingIdx >= 0) Store.funktionaere[existingIdx] = entry else Store.funktionaere.add(entry)
|
||||||
|
|
||||||
return MasterdataStats(
|
|
||||||
lastImport = lastImportStr,
|
|
||||||
vereinCount = vereinCount.toInt(),
|
|
||||||
reiterCount = reiterCount.toInt(),
|
|
||||||
pferdCount = Store.pferde.size,
|
|
||||||
funktionaerCount = Store.funktionaere.size
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStats(): MasterdataStats {
|
||||||
|
val vereinCount = queries.countVereine().executeAsOne()
|
||||||
|
val reiterCount = queries.countReiter().executeAsOne()
|
||||||
|
val pferdCount = queries.countPferde().executeAsOne()
|
||||||
|
val funktionaerCount = queries.countFunktionaere().executeAsOne()
|
||||||
|
|
||||||
|
val lastUpdate = listOf(
|
||||||
|
queries.maxUpdatedVerein().executeAsOne().MAX ?: 0L,
|
||||||
|
queries.maxUpdatedReiter().executeAsOne().MAX ?: 0L,
|
||||||
|
queries.maxUpdatedPferd().executeAsOne().MAX ?: 0L,
|
||||||
|
queries.maxUpdatedFunktionaer().executeAsOne().MAX ?: 0L
|
||||||
|
).maxOrNull() ?: 0L
|
||||||
|
|
||||||
|
val lastImportStr = if (lastUpdate > 0) {
|
||||||
|
val dt = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(lastUpdate), java.time.ZoneId.systemDefault())
|
||||||
|
dt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||||
|
} else "Nie"
|
||||||
|
|
||||||
|
return MasterdataStats(
|
||||||
|
lastImport = lastImportStr,
|
||||||
|
vereinCount = vereinCount.toInt(),
|
||||||
|
reiterCount = reiterCount.toInt(),
|
||||||
|
pferdCount = pferdCount.toInt(),
|
||||||
|
funktionaerCount = funktionaerCount.toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Session-Journal: 22. April 2026 - Masterdata Auth & Consul Resilience
|
||||||
|
|
||||||
|
## 🎯 Status & Highlights
|
||||||
|
- **Auth-Bugfix (Cloud-Sync):** Behebung des `401 Unauthorized` beim Cloud-Sync durch konsistente Token-Injektion in alle API-Calls des `ZnsImportViewModel`.
|
||||||
|
- **Infrastruktur-Stabilisierung:** Optimierung der Consul-Health-Checks für den `masterdata-service`, um unerwünschte Deregistrierungen zu vermeiden.
|
||||||
|
- **Massendaten-Optimierung:** Erhöhung der Sync-Limits auf 50.000 Sätze pro Request für eine effiziente Initialbefüllung der lokalen SQLite-Datenbank.
|
||||||
|
|
||||||
|
## 🛠️ Durchgeführte Änderungen
|
||||||
|
### Frontend (Common)
|
||||||
|
- **ZnsImportViewModel.kt:**
|
||||||
|
- Bearer-Token zu allen `httpClient.get` Aufrufen in `syncFromCloud` hinzugefügt.
|
||||||
|
- Sicherheitscheck `if (token != null)` vor Header-Setzung implementiert.
|
||||||
|
- Synchronisations-Limits für Vereine auf 50.000 erhöht.
|
||||||
|
|
||||||
|
### Backend (Infrastructure)
|
||||||
|
- **masterdata-service (application.yml):**
|
||||||
|
- `health-check-interval` auf 30s erhöht.
|
||||||
|
- `health-check-critical-timeout` auf 5m erweitert.
|
||||||
|
- `deregister-critical-service-after` auf 10m gesetzt, um Consul mehr Puffer bei Lastspitzen (wie ZNS-Importen) zu geben.
|
||||||
|
|
||||||
|
## 🧐 QA & Verifizierung
|
||||||
|
- **Token-Validation:** Code-Review bestätigt, dass alle Sync-Endpunkte nun den Authorization-Header mitschicken.
|
||||||
|
- **Build:** `./gradlew :frontend:shells:meldestelle-desktop:compileKotlinJvm` weiterhin erfolgreich.
|
||||||
|
- **Log-Analyse:** Masterdata-Logs bestätigen Health-Checks auf Port 8086, während API auf 8091 läuft.
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
1. **End-to-End Sync:** Neustart der Desktop-App und Verifizierung, dass der Button "Cloud-Sync" nun alle Daten (Vereine, Reiter, Pferde, Funktionäre) ohne 401 Fehler in die SQLite zieht.
|
||||||
|
2. **Daten-Validierung:** Stichprobenartige Suche in der Desktop-App gegen die importierten 21.206 Pferde.
|
||||||
|
|
||||||
|
🏗️ [Lead Architect] | 👷 [Backend Developer] | 🐧 [DevOps Engineer]
|
||||||
Loading…
Reference in New Issue
Block a user