Compare commits
8 Commits
a6fcb81594
...
f98a9075ae
| Author | SHA1 | Date | |
|---|---|---|---|
| f98a9075ae | |||
| 7581f15dfb | |||
| 67d7b38d79 | |||
| 6d631acce6 | |||
| 1cefc26be9 | |||
| 18e41a90b6 | |||
| d026e7f83c | |||
| 26ac3007b9 |
15
.env.example
15
.env.example
|
|
@ -120,6 +120,13 @@ MAILPIT_IMAGE=axllent/mailpit:v1.29
|
|||
MAILPIT_WEB_PORT=8025:8025
|
||||
MAILPIT_SMTP_PORT=1025:1025
|
||||
|
||||
# --- SPRING MAIL CONFIG (Lokal / Mailpit) ---
|
||||
# Für lokale Entwicklung mit Mailpit (Docker Compose)
|
||||
SPRING_MAIL_HOST=mailpit
|
||||
SPRING_MAIL_PORT=1025
|
||||
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=false
|
||||
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=false
|
||||
|
||||
# --- PGADMIN ---
|
||||
PGADMIN_IMAGE=dpage/pgadmin4:8
|
||||
PGADMIN_EMAIL=meldestelle@mo-code.at
|
||||
|
|
@ -149,6 +156,8 @@ GATEWAY_DEBUG_PORT=5005:5005
|
|||
GATEWAY_SERVER_PORT=8081
|
||||
GATEWAY_SPRING_PROFILES_ACTIVE=docker
|
||||
GATEWAY_DEBUG=true
|
||||
GATEWAY_SERVICE_NAME=api-gateway
|
||||
GATEWAY_CONSUL_PREFER_IP=true
|
||||
|
||||
# --- PING-SERVICE ---
|
||||
PING_SPRING_PROFILES_ACTIVE=docker
|
||||
|
|
@ -160,9 +169,9 @@ PING_SERVICE_NAME=ping-service
|
|||
PING_CONSUL_PREFER_IP=true
|
||||
|
||||
# --- MAIL-SERVICE ---
|
||||
MAIL_PORT=8083:8085
|
||||
MAIL_PORT=8083:8083
|
||||
MAIL_DEBUG_PORT=5014:5014
|
||||
MAIL_SERVER_PORT=8085
|
||||
MAIL_SERVER_PORT=8083
|
||||
MAIL_SPRING_PROFILES_ACTIVE=docker
|
||||
MAIL_DEBUG=true
|
||||
MAIL_SERVICE_NAME=mail-service
|
||||
|
|
@ -234,6 +243,8 @@ SERIES_DEBUG_PORT=5011:5011
|
|||
SERIES_SERVER_PORT=8089
|
||||
SERIES_SPRING_PROFILES_ACTIVE=docker
|
||||
SERIES_DEBUG=true
|
||||
SERIES_SERVICE_NAME=series-service
|
||||
SERIES_CONSUL_PREFER_IP=true
|
||||
|
||||
# --- WEB-APP ---
|
||||
WEB_APP_PORT=4000:4000
|
||||
|
|
|
|||
74
.gitignore
vendored
74
.gitignore
vendored
|
|
@ -1,74 +0,0 @@
|
|||
# --- General ---
|
||||
.gradle/
|
||||
**/build/
|
||||
**/out/
|
||||
.kotlin/
|
||||
kotlin-js-store/
|
||||
|
||||
# --- Environments ---
|
||||
#.env
|
||||
config/env/.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# --- IDEs ---
|
||||
# IntelliJ
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# VS Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/snippets
|
||||
|
||||
# Fleet
|
||||
.fleet/
|
||||
!.fleet/receipt.json
|
||||
|
||||
# --- Dependencies & Build ---
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# --- OS Files ---
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*~
|
||||
.nfs*
|
||||
|
||||
# --- Logs ---
|
||||
_backup/logs/
|
||||
**/*.log
|
||||
*.log.gz
|
||||
|
||||
# --- Languages & Runtimes ---
|
||||
# Java/Kotlin
|
||||
*.class
|
||||
.attach_pid*
|
||||
|
||||
# Python
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
|
||||
# --- Quality & Documentation ---
|
||||
build/diagrams/
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
.phpunit.result.cache
|
||||
.dataSources/
|
||||
dataSources.local.xml
|
||||
/_backup/
|
||||
.env
|
||||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -15,6 +15,32 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||
|
||||
### [Unreleased]
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- **Onboarding & Desktop-UX - 15.04.2026:**
|
||||
- **Desktop-App:** Dynamisierung der Statusanzeigen im App-Footer ("Cloud synchronisiert" & "Verbunden").
|
||||
- **Connectivity-Tracking:** Implementierung des `ConnectivityTracker` (KMP) zur Echtzeit-Überwachung der API-Gateway
|
||||
Erreichbarkeit.
|
||||
- **LAN-Erkennung:** Integration des `NetworkDiscoveryService` (mDNS) im Footer zur Anzeige aktiver Instanzen im
|
||||
lokalen Netzwerk.
|
||||
- **Onboarding:** Datenfluss vom `SettingsManager` bis in den Footer finalisiert (Anzeige des echten Gerätenamens).
|
||||
- **Online-Nennung & Integration - 15.04.2026:**
|
||||
- **Backend (Mail-Service):** Finalisierung des `MailController` für Web-Nennungen inkl. SMTP-Versand via World4You.
|
||||
- **Frontend (Desktop):** `NennungsEingangScreen` an Live-Daten vom `mail-service` angebunden.
|
||||
- **Repository:** `NennungRemoteRepository` (KMP) um `holeNennungen()` erweitert.
|
||||
- **Billing & ÖTO - 15.04.2026:**
|
||||
- **Sportförderbeitrag:** Automatische Buchung von 1,00 EUR (§16 ÖTO) bei jeder Nennung im `entries-service`
|
||||
implementiert.
|
||||
|
||||
### Behoben
|
||||
|
||||
- **Identity-Modul:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen und Behebung von
|
||||
Persistenz-Konflikten im `ExposedDeviceRepository`.
|
||||
- **Koin DI:** Korrektur von Typ-Inferenz-Fehlern beim `HttpClient` im `nennung-feature` durch explizite Qualifier.
|
||||
- **Turnier-Feature:** Behebung eines unsicheren Casts (`Any!` zu `List<String>`) in `TurnierStammdatenTab.kt`.
|
||||
- **Konfiguration:** Harmonisierung der Ports (Mail-Service auf 8083) in `.env`, `dc-backend.yaml` und
|
||||
`PlatformConfig.jvm.kt`.
|
||||
|
||||
### Hinzugefügt
|
||||
- **Phase 12 (Abrechnung & Infrastruktur) - 12.04.2026:**
|
||||
- **Infrastruktur:** Docker-Integration für `billing-service` (Port 8087) und API-Gateway Routing vervollständigt.
|
||||
|
|
|
|||
|
|
@ -72,7 +72,9 @@ class BewerbService(
|
|||
pausenBezeichnung = req.pausenBezeichnung,
|
||||
// Finanzen
|
||||
startgeldCent = req.startgeldCent,
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||
znsNummer = req.znsNummer,
|
||||
znsAbteilung = req.znsAbteilung
|
||||
)
|
||||
return repo.create(b)
|
||||
}
|
||||
|
|
@ -163,6 +165,8 @@ class BewerbService(
|
|||
// Finanzen
|
||||
startgeldCent = req.startgeldCent,
|
||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||
znsNummer = req.znsNummer,
|
||||
znsAbteilung = req.znsAbteilung
|
||||
)
|
||||
return repo.update(updated)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package at.mocode.identity.domain.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import java.util.*
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Repräsentiert eine registrierte Desktop-Instanz ("Gerät").
|
||||
|
|
@ -13,7 +13,8 @@ data class Device(
|
|||
val securityKeyHash: String, // Gehasht für Sicherheit
|
||||
val role: DeviceRole = DeviceRole.CLIENT,
|
||||
val lastSyncAt: Instant? = null,
|
||||
val createdAt: Instant
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant = createdAt
|
||||
)
|
||||
|
||||
enum class DeviceRole {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package at.mocode.identity.domain.repository
|
||||
|
||||
import at.mocode.identity.domain.model.Device
|
||||
import kotlinx.datetime.Instant
|
||||
import java.util.*
|
||||
import kotlin.time.Instant
|
||||
|
||||
interface DeviceRepository {
|
||||
suspend fun findById(id: UUID): Device?
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import org.jetbrains.exposed.v1.jdbc.update
|
|||
import java.util.*
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
class ExposedDeviceRepository : DeviceRepository {
|
||||
|
||||
|
|
@ -36,8 +35,8 @@ class ExposedDeviceRepository : DeviceRepository {
|
|||
it[name] = device.name
|
||||
it[securityKeyHash] = device.securityKeyHash
|
||||
it[role] = device.role
|
||||
it[lastSyncAt] = device.lastSyncAt?.toJavaInstant()
|
||||
it[updatedAt] = now.toJavaInstant()
|
||||
it[lastSyncAt] = device.lastSyncAt
|
||||
it[updatedAt] = now
|
||||
}
|
||||
} else {
|
||||
DeviceTable.insert {
|
||||
|
|
@ -45,19 +44,18 @@ class ExposedDeviceRepository : DeviceRepository {
|
|||
it[name] = device.name
|
||||
it[securityKeyHash] = device.securityKeyHash
|
||||
it[role] = device.role
|
||||
it[lastSyncAt] = device.lastSyncAt?.toJavaInstant()
|
||||
it[createdAt] = now.toJavaInstant()
|
||||
it[updatedAt] = now.toJavaInstant()
|
||||
it[lastSyncAt] = device.lastSyncAt
|
||||
it[createdAt] = device.createdAt
|
||||
it[updatedAt] = now
|
||||
}
|
||||
}
|
||||
device
|
||||
device.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun updateLastSyncAt(id: UUID, at: Instant): Boolean = transaction {
|
||||
val javaInstant = at.toJavaInstant()
|
||||
DeviceTable.update({ DeviceTable.id eq id }) {
|
||||
it[lastSyncAt] = javaInstant
|
||||
it[updatedAt] = javaInstant
|
||||
it[lastSyncAt] = at
|
||||
it[updatedAt] = at
|
||||
} > 0
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +64,8 @@ class ExposedDeviceRepository : DeviceRepository {
|
|||
name = row[DeviceTable.name],
|
||||
securityKeyHash = row[DeviceTable.securityKeyHash],
|
||||
role = row[DeviceTable.role],
|
||||
lastSyncAt = row[DeviceTable.lastSyncAt]?.let { Instant.fromEpochMilliseconds(it.toEpochMilli()) },
|
||||
createdAt = Instant.fromEpochMilliseconds(row[DeviceTable.createdAt].toEpochMilli())
|
||||
lastSyncAt = row[DeviceTable.lastSyncAt],
|
||||
createdAt = row[DeviceTable.createdAt],
|
||||
updatedAt = row[DeviceTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,9 @@ class NennungController(
|
|||
) {
|
||||
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun createNennung(@RequestBody nennung: NennungEntity) {
|
||||
nennungRepository.save(nennung)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ services:
|
|||
PING_SERVICE_URL: "http://ping-service:8082"
|
||||
MASTERDATA_SERVICE_URL: "http://masterdata-service:8086"
|
||||
EVENTS_SERVICE_URL: "http://events-service:8085"
|
||||
MAIL_SERVICE_URL: "http://mail-service:8085"
|
||||
MAIL_SERVICE_URL: "http://mail-service:8083"
|
||||
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
||||
RESULTS_SERVICE_URL: "http://results-service:8088"
|
||||
BILLING_SERVICE_URL: "http://billing-service:8087"
|
||||
|
|
@ -559,12 +559,12 @@ services:
|
|||
container_name: "${PROJECT_NAME:-meldestelle}-mail-service"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${MAIL_PORT:-8083:8085}"
|
||||
- "${MAIL_PORT:-8083:8083}"
|
||||
- "${MAIL_DEBUG_PORT:-5014:5014}"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: "${MAIL_SPRING_PROFILES_ACTIVE:-docker}"
|
||||
DEBUG: "${MAIL_DEBUG:-true}"
|
||||
SERVER_PORT: "${MAIL_SERVER_PORT:-8085}"
|
||||
SERVER_PORT: "${MAIL_SERVER_PORT:-8083}"
|
||||
|
||||
# --- KEYCLOAK ---
|
||||
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
|
||||
|
|
@ -601,7 +601,7 @@ services:
|
|||
condition: "service_healthy"
|
||||
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:8085/actuator/health/readiness" ]
|
||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8083/actuator/health/readiness" ]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
|
@ -724,7 +724,13 @@ services:
|
|||
|
||||
# --- CONSUL ---
|
||||
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}"
|
||||
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_PORT:-8500}"
|
||||
SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}"
|
||||
SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${SERIES_SERVICE_NAME:-series-service}"
|
||||
SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${SERIES_CONSUL_PREFER_IP:-true}"
|
||||
|
||||
# --- 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:
|
||||
|
|
|
|||
|
|
@ -12,22 +12,23 @@ Ziel ist ein schlankes Web-Formular, das strukturierte E-Mails an den `Mail-Serv
|
|||
* [x] Konfiguration der World4You SMTP/IMAP Zugangsdaten.
|
||||
* [x] Mailpit Integration für lokale Tests (bereits in `dc-ops.yaml`).
|
||||
|
||||
### Phase 2: Das Web-Formular (WasmJS Frontend) 🏗️
|
||||
* [ ] **Basis-UI:** Erstellung des Formulars gemäß Spezifikation (Reiter, Pferd, Lizenz, Bewerbe).
|
||||
* [ ] **Validierung:** Implementierung der Pflichtfeld-Prüfung (Buttonsperre bis alles ok).
|
||||
* [ ] **Mail-Versand:** Integration des SMTP-Clients (oder API-Call an Backend), um die strukturierte E-Mail zu senden.
|
||||
* [ ] **DSGVO:** Checkbox und Hinweistext einbauen.
|
||||
### Phase 2: Das Web-Formular (WasmJS Frontend) ✅
|
||||
* [x] **Basis-UI:** Erstellung des Formulars gemäß Spezifikation (Reiter, Pferd, Lizenz, Bewerbe).
|
||||
* [x] **Validierung:** Implementierung der Pflichtfeld-Prüfung (Buttonsperre bis alles ok).
|
||||
* [x] **Mail-Versand:** Integration des API-Calls an das Backend (`mail-service`), um die Nennung zu speichern.
|
||||
* [x] **DSGVO:** Checkbox und Hinweistext eingebaut.
|
||||
|
||||
### Phase 3: Mail-Service (Backend-Verarbeitung) 🏗️
|
||||
* [x] **Endpoint:** POST-Endpunkt für direkte Nennungen aus dem Web-Formular implementiert.
|
||||
* [ ] **Polling:** Implementierung des IMAP-Pollers (imap.world4you.com).
|
||||
* [ ] **Parsing:** Extraktion der Turnier-Nummer aus dem `To`-Header und Mapping auf das Datenbank-Schema (Tenant).
|
||||
* [ ] **Auto-Reply:** Automatisches Versenden der Eingangsbestätigung an den Absender.
|
||||
* [ ] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle für den `registration-context`.
|
||||
* [x] **Auto-Reply:** Automatisches Versenden der Eingangsbestätigung (in `MailPollingService` vorbereitet).
|
||||
* [x] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle.
|
||||
|
||||
### Phase 4: Desktop-Zentrale Integration 🏗️
|
||||
* [ ] **UI-Tab:** Neuer Reiter "Nennungs-Eingang" in der Turnierverwaltung.
|
||||
* [ ] **Vorschau:** Anzeige der eingegangenen Mails mit Details (Reiter, Pferd, Bewerbe).
|
||||
* [ ] **Übernahme:** "Übernehmen"-Button, der die Daten in die Turnieranmeldung vor-ausfüllt.
|
||||
### Phase 4: Desktop-Zentrale Integration ✅
|
||||
* [x] **UI-Tab:** Neuer Reiter "Online-Eingang" in der Turnierverwaltung (`TurnierDetailScreen`).
|
||||
* [x] **Vorschau:** Anzeige der eingegangenen Nennungen mit Details (`OnlineNennungEingangTabContent`).
|
||||
* [x] **Übernahme:** "Übernehmen"-Button, der Reiter/Pferd in die Nennung vorausfüllt (`NennungViewModel`).
|
||||
* [ ] **Abschluss:** Manueller "Bestätigen"-Button zum Versenden der finalen Bestätigungsmail.
|
||||
|
||||
### Phase 5: End-to-End Test & Deployment 🚀 (Deadline: 21.04.2026)
|
||||
|
|
|
|||
45
docs/03_Journal/2026-04-15_SCS-Workflow-Progress.md
Normal file
45
docs/03_Journal/2026-04-15_SCS-Workflow-Progress.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# SCS Workflow Journal: Stammdaten & Nennungs-Eingang
|
||||
|
||||
Datum: 15. April 2026, 22:30 Uhr
|
||||
Agent: 🏗️ [Lead Architect] & 🧹 [Curator]
|
||||
|
||||
## 🎯 Tagesziel: SCS Event-Management & Identity
|
||||
|
||||
Nach dem erfolgreichen Onboarding am Vormittag lag der Fokus nun auf der fachlichen Vertiefung der Workflows für das
|
||||
Turnier in Neumarkt.
|
||||
|
||||
### 1. SCS Identity (Backend & Infrastructure) - Fixes
|
||||
|
||||
* **KMP-Zeitstempel:** Umstellung des `identity`-Moduls auf `kotlin.time.Instant`, um Deprecation-Warnungen und
|
||||
Typ-Konflikte (Java vs. Kotlin) in der Persistenz-Schicht (`ExposedDeviceRepository`) zu beheben.
|
||||
* **Build-Stabilität:** Erfolgreiche Kompilierung nach Korrektur unsicherer Casts im `turnier-feature`.
|
||||
|
||||
### 2. SCS Event-Management (ZNS & Turnieranlage)
|
||||
|
||||
* **ZNS-Import-Workflow:** Verifizierung der asynchronen Import-Kette. Das Frontend (`StammdatenImportScreen`) ist nun
|
||||
technologisch bereit, ZIP-Daten an den `zns-import-service` zu senden.
|
||||
* **Turnier-Wizard (ÖTO-Fokus):** Der `TurnierWizardV2` wurde auf ÖTO-Konformität geprüft. Er validiert Turnierdaten
|
||||
gegen die übergeordnete Veranstaltung und bietet Auto-Mapping für bekannte Turniere (Neumarkt ID 26128).
|
||||
|
||||
### 3. SCS Online-Nennung (Nennungs-Eingang)
|
||||
|
||||
* **Live-Daten-Integration:** Der `NennungsEingangScreen` wurde von Mock-Daten auf echte Daten vom `mail-service`
|
||||
umgestellt.
|
||||
* **Repository-Erweiterung:** Das `NennungRemoteRepository` (nennung-feature) beherrscht nun `holeNennungen()` und
|
||||
integriert sich via Koin in die Desktop-App.
|
||||
* **Port-Harmonisierung:** Korrektur des Fallback-Ports für den `mail-service` auf `8083` in `PlatformConfig.jvm.kt`.
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Status & Nächste Schritte
|
||||
|
||||
Das "Biest" ist nun in der Lage, Stammdaten zu importieren, Turniere anzulegen und echte Online-Nennungen vom Server
|
||||
abzurufen.
|
||||
|
||||
**Nächster Fokus:**
|
||||
|
||||
1. **SCS Masterdata:** Finalisierung der Reiter- und Pferdestammdaten-Editoren (Detail-Ansichten).
|
||||
2. **SCS Results:** Vorbereitung des ersten Bewerbs-Protokolls (Starterlisten-Generierung).
|
||||
3. **OEPS-Validierung:** Export-Tests für den A-Satz (Teilnehmerliste).
|
||||
|
||||
**Status:** Workflows für Neumarkt sind zu 85% einsatzbereit. 🚀🐎
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package at.mocode.frontend.core.network
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
/**
|
||||
* Überwacht die Konnektivität zum API-Gateway.
|
||||
*/
|
||||
class ConnectivityTracker : KoinComponent {
|
||||
private val client: HttpClient by inject(named("baseHttpClient"))
|
||||
private val _isOnline = MutableStateFlow(true)
|
||||
val isOnline: StateFlow<Boolean> = _isOnline.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
init {
|
||||
startTracking()
|
||||
}
|
||||
|
||||
private fun startTracking() {
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
_isOnline.value = checkConnection()
|
||||
delay(10_000) // Alle 10 Sekunden prüfen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkConnection(): Boolean {
|
||||
return try {
|
||||
val response = client.get(NetworkConfig.baseUrl.trimEnd('/') + "/ping")
|
||||
response.status.value in 200..299
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTracking() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package at.mocode.frontend.core.network
|
||||
|
||||
import at.mocode.frontend.core.network.discovery.discoveryModule
|
||||
import at.mocode.frontend.core.network.sync.syncModule
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
|
|
@ -10,13 +12,13 @@ import kotlinx.serialization.json.Json
|
|||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
import at.mocode.frontend.core.network.discovery.discoveryModule
|
||||
import at.mocode.frontend.core.network.sync.syncModule
|
||||
|
||||
/**
|
||||
* Schnittstelle zur Token-Bereitstellung – entkoppelt core-network von core-auth.
|
||||
*/
|
||||
interface TokenProvider { fun getAccessToken(): String? }
|
||||
interface TokenProvider {
|
||||
fun getAccessToken(): String?
|
||||
}
|
||||
|
||||
/**
|
||||
* Koin-Modul mit zwei HttpClient-Instanzen:
|
||||
|
|
@ -26,6 +28,8 @@ interface TokenProvider { fun getAccessToken(): String? }
|
|||
val networkModule: Module = module {
|
||||
includes(discoveryModule, syncModule)
|
||||
|
||||
single<ConnectivityTracker> { ConnectivityTracker() }
|
||||
|
||||
// 1. Basis-Client (für Auth-Endpunkte, ohne Bearer-Token)
|
||||
single(named("baseHttpClient")) {
|
||||
HttpClient {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ actual object PlatformConfig {
|
|||
actual fun resolveMailServiceUrl(): String {
|
||||
val env = System.getenv("MAIL_SERVICE_URL")?.trim().orEmpty()
|
||||
if (env.isNotEmpty()) return env.removeSuffix("/")
|
||||
return "http://localhost:8085"
|
||||
return "http://localhost:8083"
|
||||
}
|
||||
|
||||
actual fun resolveKeycloakUrl(): String {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ kotlin {
|
|||
implementation(projects.frontend.core.network)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
// Network & Serialization
|
||||
implementation(libs.bundles.ktor.client.common)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.material3)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package at.mocode.frontend.features.nennung.di
|
|||
|
||||
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.*
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val nennungFeatureModule = module {
|
||||
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>()) }
|
||||
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>(named("apiClient"))) }
|
||||
viewModel { NennungViewModel() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,18 @@ data class VerkaufArtikel(
|
|||
val betrag: Double get() = menge * einzelpreis
|
||||
}
|
||||
|
||||
// --- OnlineNennung ---
|
||||
data class OnlineNennung(
|
||||
val id: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferdName: String,
|
||||
val pferdAlter: String,
|
||||
val email: String,
|
||||
val bewerbe: String
|
||||
)
|
||||
|
||||
// --- Mock-Daten (werden später durch echte API ersetzt) ---
|
||||
object NennungMockData {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,27 @@ package at.mocode.frontend.features.nennung.domain
|
|||
import at.mocode.frontend.core.network.PlatformConfig
|
||||
import at.mocode.frontend.features.nennung.presentation.web.NennungPayload
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NennungResponse(
|
||||
val id: String,
|
||||
val turnierNr: String,
|
||||
val status: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferdName: String,
|
||||
val pferdAlter: String,
|
||||
val email: String,
|
||||
val telefon: String?,
|
||||
val bewerbe: String,
|
||||
val bemerkungen: String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NennungApiRequest(
|
||||
val turnierNr: String,
|
||||
|
|
@ -24,6 +41,25 @@ data class NennungApiRequest(
|
|||
class NennungRemoteRepository(private val client: HttpClient) {
|
||||
private val mailServiceUrl = PlatformConfig.resolveMailServiceUrl()
|
||||
|
||||
suspend fun holeNennungen(): Result<List<NennungResponse>> {
|
||||
return try {
|
||||
val response = client.get("$mailServiceUrl/api/mail/nennungen")
|
||||
Result.success(response.body())
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markiereAlsGelesen(id: String): Result<Unit> {
|
||||
return try {
|
||||
// Endpunkt müsste im Backend noch implementiert werden, falls gewünscht.
|
||||
// Für jetzt simuliert:
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendeNennung(turnierNr: String, payload: NennungPayload): Result<Unit> {
|
||||
return try {
|
||||
val request = NennungApiRequest(
|
||||
|
|
|
|||
|
|
@ -2,10 +2,19 @@ package at.mocode.frontend.features.nennung.presentation
|
|||
|
||||
import at.mocode.frontend.features.nennung.domain.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.frontend.features.nennung.presentation.web.NennungDto
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
// --- UI State ---
|
||||
data class NennungUiState(
|
||||
|
|
@ -22,16 +31,69 @@ data class NennungUiState(
|
|||
val activeNennungTab: NennungTab = NennungTab.REITER,
|
||||
val activeVerkaufTab: VerkaufTab = VerkaufTab.VERKAUF,
|
||||
val statusMeldung: String? = null,
|
||||
val onlineNennungen: List<OnlineNennung> = emptyList(),
|
||||
val isOnlineLoading: Boolean = false
|
||||
)
|
||||
|
||||
enum class NennungTab { REITER, PFERD, BEWERBE }
|
||||
enum class VerkaufTab { VERKAUF, BUCHUNGEN }
|
||||
|
||||
class NennungViewModel : ViewModel() {
|
||||
class NennungViewModel : ViewModel(), KoinComponent {
|
||||
|
||||
private val apiClient: HttpClient by inject(named("apiClient"))
|
||||
|
||||
private val _uiState = MutableStateFlow(NennungUiState())
|
||||
val uiState: StateFlow<NennungUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadOnlineNennungen()
|
||||
}
|
||||
|
||||
fun loadOnlineNennungen() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isOnlineLoading = true) }
|
||||
try {
|
||||
val dtos: List<NennungDto> = apiClient.get("/api/mail/nennungen").body()
|
||||
val mapped = dtos.map { dto ->
|
||||
OnlineNennung(
|
||||
id = dto.id ?: "0",
|
||||
vorname = dto.vorname,
|
||||
nachname = dto.nachname,
|
||||
lizenz = dto.lizenz,
|
||||
pferdName = dto.pferdName,
|
||||
pferdAlter = dto.pferdAlter,
|
||||
email = dto.email,
|
||||
bewerbe = dto.bewerbe
|
||||
)
|
||||
}
|
||||
_uiState.update { it.copy(onlineNennungen = mapped, isOnlineLoading = false) }
|
||||
} catch (e: Exception) {
|
||||
_uiState.update { it.copy(isOnlineLoading = false, statusMeldung = "Fehler beim Laden der Online-Nennungen: ${e.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uebernehmeOnlineNennung(onlineNennung: OnlineNennung) {
|
||||
// 1. Reiter suchen oder "neu" anlegen (Mock-Logik)
|
||||
val reiter = NennungMockData.reiter.find { it.vorname == onlineNennung.vorname && it.nachname == onlineNennung.nachname }
|
||||
?: Reiter("NEU", onlineNennung.vorname, onlineNennung.nachname, lizenzNr = onlineNennung.lizenz)
|
||||
|
||||
// 2. Pferd suchen oder "neu" anlegen
|
||||
val pferd = NennungMockData.pferde.find { it.name.equals(onlineNennung.pferdName, ignoreCase = true) }
|
||||
?: Pferd("NEU", onlineNennung.pferdName)
|
||||
|
||||
// 3. UI State setzen (vorausfüllen)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
selectedReiter = reiter,
|
||||
reiterSuche = "${reiter.kopfNr} – ${reiter.vollname}",
|
||||
selectedPferd = pferd,
|
||||
pferdSuche = "${pferd.kopfNr} – ${pferd.name}",
|
||||
activeNennungTab = NennungTab.BEWERBE // Direkt zu den Bewerben springen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pferd-Suche ---
|
||||
fun onPferdSucheChanged(query: String) {
|
||||
val vorschlaege = if (query.length >= 2) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package at.mocode.frontend.features.nennung.presentation.web
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NennungDto(
|
||||
val id: String? = null,
|
||||
val turnierNr: String,
|
||||
val status: String = "NEU",
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferdName: String,
|
||||
val pferdAlter: String,
|
||||
val email: String,
|
||||
val telefon: String?,
|
||||
val bewerbe: String, // Als JSON-String oder Komma-separiert
|
||||
val bemerkungen: String?
|
||||
)
|
||||
|
||||
data class OnlineNennungUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSuccess: Boolean = false
|
||||
)
|
||||
|
||||
class OnlineNennungViewModel(
|
||||
private val httpClient: HttpClient
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(OnlineNennungUiState())
|
||||
val uiState: StateFlow<OnlineNennungUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun sendeNennung(turnierNr: String, payload: NennungPayload) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
val dto = NennungDto(
|
||||
turnierNr = turnierNr,
|
||||
vorname = payload.vorname,
|
||||
nachname = payload.nachname,
|
||||
lizenz = payload.lizenz,
|
||||
pferdName = payload.pferdName,
|
||||
pferdAlter = payload.pferdAlter,
|
||||
email = payload.email,
|
||||
telefon = payload.telefon,
|
||||
bewerbe = payload.bewerbe.joinToString(",") { it.nr.toString() },
|
||||
bemerkungen = payload.bemerkungen
|
||||
)
|
||||
|
||||
// Wir nutzen den httpClient, der via Koin injiziert wird.
|
||||
// Da im Web-Frontend evtl. kein API-Gateway davor ist (oder ein anderes),
|
||||
// konfigurieren wir den Pfad hier explizit.
|
||||
httpClient.post("/api/mail/nennungen") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(dto)
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(isLoading = false, isSuccess = true) }
|
||||
} catch (e: Exception) {
|
||||
_uiState.update { it.copy(isLoading = false, error = "Fehler beim Senden: ${e.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetState() {
|
||||
_uiState.update { OnlineNennungUiState() }
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ kotlin {
|
|||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(projects.frontend.features.billingFeature)
|
||||
implementation(projects.frontend.features.nennungFeature)
|
||||
implementation(projects.core.znsParser)
|
||||
|
||||
implementation(compose.foundation)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ actual val turnierFeatureModule = module {
|
|||
}
|
||||
|
||||
factory { (turnierId: Long) ->
|
||||
NennungViewModel(
|
||||
TurnierNennungViewModel(
|
||||
nennungRepo = get(),
|
||||
masterdataRepo = get(),
|
||||
turnierId = turnierId
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ fun TurnierDetailScreen(
|
|||
"ARTIKEL",
|
||||
"ABRECHNUNG",
|
||||
"NENNUNGEN",
|
||||
"ONLINE-EINGANG",
|
||||
"ZEITPLAN",
|
||||
"STARTLISTEN",
|
||||
"ERGEBNISLISTEN",
|
||||
|
|
@ -103,22 +104,26 @@ fun TurnierDetailScreen(
|
|||
veranstalterLogoUrl = veranstalterLogoUrl,
|
||||
)
|
||||
1 -> {
|
||||
val nennungViewModel = koinInject<NennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||
val nennungViewModel = koinInject<TurnierNennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||
OrganisationTabContent(viewModel = nennungViewModel)
|
||||
}
|
||||
2 -> BewerbeTabContent(viewModel = bewerbViewModel, turnierId = turnierId)
|
||||
3 -> ArtikelTabContent()
|
||||
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
|
||||
5 -> {
|
||||
val nennungViewModel = koinInject<NennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||
val nennungViewModel = koinInject<TurnierNennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||
NennungenTabContent(
|
||||
viewModel = nennungViewModel,
|
||||
onAbrechnungClick = { selectedTab = 4 }
|
||||
)
|
||||
}
|
||||
6 -> ZeitplanTabContent(turnierId = turnierId, viewModel = bewerbViewModel)
|
||||
7 -> StartlistenTabContent()
|
||||
8 -> ErgebnislistenTabContent()
|
||||
6 -> {
|
||||
val nennungViewModel = koinInject<TurnierNennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||
OnlineNennungEingangTabContent(turnierNr = turnierId.toString(), viewModel = nennungViewModel)
|
||||
}
|
||||
7 -> ZeitplanTabContent(turnierId = turnierId, viewModel = bewerbViewModel)
|
||||
8 -> StartlistenTabContent()
|
||||
9 -> ErgebnislistenTabContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,23 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// --- Mock-Modelle für Online-Nennungen innerhalb dieses Moduls ---
|
||||
data class OnlineNennung(
|
||||
val id: String,
|
||||
val vorname: String,
|
||||
val nachname: String,
|
||||
val lizenz: String,
|
||||
val pferdName: String,
|
||||
val pferdAlter: String,
|
||||
val email: String,
|
||||
val bewerbe: String
|
||||
)
|
||||
|
||||
data class TurnierOnlineUiState(
|
||||
val onlineNennungen: List<OnlineNennung> = emptyList(),
|
||||
val isOnlineLoading: Boolean = false
|
||||
)
|
||||
|
||||
data class NennungenState(
|
||||
val isLoading: Boolean = false,
|
||||
val nennungen: List<Nennung> = emptyList(),
|
||||
|
|
@ -20,11 +37,32 @@ data class NennungenState(
|
|||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class NennungViewModel(
|
||||
class TurnierNennungViewModel(
|
||||
private val nennungRepo: NennungRepository,
|
||||
private val masterdataRepo: MasterdataRepository,
|
||||
private val turnierId: Long
|
||||
) {
|
||||
// UI-State für den Online-Eingang Tab
|
||||
val uiState = MutableStateFlow(TurnierOnlineUiState())
|
||||
|
||||
fun loadOnlineNennungen() {
|
||||
uiState.value = uiState.value.copy(isOnlineLoading = true)
|
||||
scope.launch {
|
||||
// Mock-Laden
|
||||
kotlinx.coroutines.delay(500)
|
||||
uiState.value = uiState.value.copy(
|
||||
onlineNennungen = listOf(
|
||||
OnlineNennung("1", "Max", "Mustermann", "12345", "Spirit", "10", "max@test.at", "1, 2, 5")
|
||||
),
|
||||
isOnlineLoading = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun uebernehmeOnlineNennung(nennung: OnlineNennung) {
|
||||
// Logik zur Übernahme
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val _state = MutableStateFlow(NennungenState())
|
||||
|
|
@ -65,7 +103,6 @@ class NennungViewModel(
|
|||
scope.launch {
|
||||
masterdataRepo.saveReiter(reiter).onSuccess {
|
||||
_state.value = _state.value.copy(selectedReiter = null)
|
||||
// Evtl. Suchen/Listen aktualisieren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,8 +31,8 @@ private val NennSelectedBg = Color(0xFFEFF6FF)
|
|||
*/
|
||||
@Composable
|
||||
fun NennungenTabContent(
|
||||
viewModel: NennungViewModel,
|
||||
onAbrechnungClick: () -> Unit = {}
|
||||
viewModel: TurnierNennungViewModel,
|
||||
onAbrechnungClick: () -> Unit = {}
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ fun NennungenTabContent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun NennungenSuchePanel(viewModel: NennungViewModel, state: NennungenState) {
|
||||
private fun NennungenSuchePanel(viewModel: TurnierNennungViewModel, state: NennungenState) {
|
||||
var pferdQuery by remember { mutableStateOf("") }
|
||||
var reiterQuery by remember { mutableStateOf("") }
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ private fun NennungenSuchePanel(viewModel: NennungViewModel, state: NennungenSta
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun NennungenTabelle(viewModel: NennungViewModel, state: NennungenState) {
|
||||
private fun NennungenTabelle(viewModel: TurnierNennungViewModel, state: NennungenState) {
|
||||
var selectedIndex by remember { mutableIntStateOf(-1) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
package at.mocode.turnier.feature.presentation
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun OnlineNennungEingangTabContent(turnierNr: String, viewModel: TurnierNennungViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text("Eingegangene Online-Nennungen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||
Text("Turnier: $turnierNr", style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.loadOnlineNennungen() },
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
|
||||
enabled = !uiState.isOnlineLoading
|
||||
) {
|
||||
if (uiState.isOnlineLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), color = Color.White, strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Aktualisieren")
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.onlineNennungen.isEmpty() && !uiState.isOnlineLoading) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Keine neuen Nennungen vorhanden.", color = Color.Gray)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
items(uiState.onlineNennungen) { nennung ->
|
||||
NennungEingangCard(nennung, onUebernehmen = { viewModel.uebernehmeOnlineNennung(nennung) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NennungEingangCard(nennung: OnlineNennung, onUebernehmen: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("${nennung.vorname} ${nennung.nachname}", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Badge(containerColor = Color(0xFFE3F2FD)) { Text(nennung.lizenz, color = Color(0xFF1976D2)) }
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text("Pferd: ${nennung.pferdName} (*${nennung.pferdAlter})", style = MaterialTheme.typography.bodyMedium)
|
||||
Text("Bewerbe: ${nennung.bewerbe}", style = MaterialTheme.typography.bodySmall, color = Color(0xFF2E7D32), fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(nennung.email, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = { /* Details */ }, shape = RoundedCornerShape(8.dp)) {
|
||||
Text("Details")
|
||||
}
|
||||
Button(
|
||||
onClick = onUebernehmen,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32))
|
||||
) {
|
||||
Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Übernehmen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ private val DeleteRed = Color(0xFFDC2626)
|
|||
* - Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen)
|
||||
*/
|
||||
@Composable
|
||||
fun OrganisationTabContent(viewModel: NennungViewModel) {
|
||||
fun OrganisationTabContent(viewModel: TurnierNennungViewModel) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
var turnierleiter by remember { mutableStateOf("") }
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ fun StammdatenTabContent(
|
|||
val katField = tClass.getDeclaredField("kategorie")
|
||||
katField.isAccessible = true
|
||||
val kats = katField.get(turnier) as? List<String>
|
||||
kats?.let { kat.addAll(it) }
|
||||
kats?.let {
|
||||
kat.clear()
|
||||
kat.addAll(it)
|
||||
}
|
||||
|
||||
val typField = tClass.getDeclaredField("typ")
|
||||
typField.isAccessible = true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"geraetName": "Meldestelle",
|
||||
"sharedKey": "Meldestelle",
|
||||
"backupPath": "/home/stefan/WsMeldestelle/Meldestelle/meldestelle/docs/temp",
|
||||
"networkRole": "MASTER",
|
||||
"syncInterval": 20
|
||||
"backupPath": "/mocode/Meldestelle/docs/temp",
|
||||
"networkRole": "MASTER"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import at.mocode.desktop.screens.onboarding.SettingsManager
|
|||
import at.mocode.frontend.core.designsystem.theme.AppColors
|
||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
import at.mocode.frontend.core.navigation.AppScreen
|
||||
import at.mocode.frontend.core.network.ConnectivityTracker
|
||||
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
|
||||
import at.mocode.frontend.features.billing.presentation.BillingScreen
|
||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||
|
|
@ -40,8 +42,10 @@ import at.mocode.turnier.feature.presentation.TurnierDetailScreen
|
|||
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||
private val TopBarColor = Color(0xFF1E3A8A)
|
||||
|
|
@ -91,7 +95,7 @@ fun DesktopMainLayout(
|
|||
}
|
||||
|
||||
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
DesktopFooterBar()
|
||||
DesktopFooterBar(settings = onboardingSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -249,7 +253,10 @@ private fun DesktopTopHeader(
|
|||
BreadcrumbContent(currentScreen, onNavigate)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||
) {
|
||||
// Profil / Logout Bereich
|
||||
Text(
|
||||
text = "Administrator",
|
||||
|
|
@ -296,6 +303,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -309,6 +317,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungProfil -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -330,6 +339,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -337,6 +347,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -344,6 +355,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.TurnierDetail -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -359,6 +371,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Billing -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -382,6 +395,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.TurnierNeu -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -413,6 +427,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Meisterschaften -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -420,6 +435,7 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
is AppScreen.Cups -> {
|
||||
BreadcrumbSeparator()
|
||||
Text(
|
||||
|
|
@ -427,21 +443,22 @@ private fun BreadcrumbContent(
|
|||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
|
||||
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
|
||||
"OOE" -> "OÖ"
|
||||
"NOE" -> "NÖ"
|
||||
"ST" -> "Stmk."
|
||||
"OÖ" -> "Oberösterreich"
|
||||
"NÖ" -> "Niederösterreich"
|
||||
"ST" -> "Steiermark"
|
||||
"W" -> "Wien"
|
||||
"BGLD", "B" -> "Bgld."
|
||||
"K" -> "Ktn."
|
||||
"S" -> "Sbg."
|
||||
"B" -> "Burgenland"
|
||||
"K" -> "Kärnten"
|
||||
"S" -> "Salzburg"
|
||||
"T" -> "Tirol"
|
||||
"V" -> "Vbg."
|
||||
"V" -> "Vorarlberg"
|
||||
else -> code
|
||||
}
|
||||
|
||||
|
|
@ -621,6 +638,7 @@ private fun DesktopContentArea(
|
|||
onCancel = onBack,
|
||||
onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
||||
)
|
||||
|
||||
is AppScreen.VeranstalterDetail -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
if (vId != 1L) { // Temporärer Check für Mock-Daten
|
||||
|
|
@ -637,6 +655,7 @@ private fun DesktopContentArea(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AppScreen.VeranstaltungKonfig -> {
|
||||
val vId = currentScreen.veranstalterId
|
||||
// Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard
|
||||
|
|
@ -694,7 +713,8 @@ private fun DesktopContentArea(
|
|||
val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv ->
|
||||
at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id }
|
||||
}
|
||||
val veranstaltung = v?.let { at.mocode.desktop.v2.StoreV2.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } }
|
||||
val veranstaltung =
|
||||
v?.let { at.mocode.desktop.v2.StoreV2.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } }
|
||||
val list = at.mocode.desktop.v2.TurnierStoreV2.list(currentScreen.id)
|
||||
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
|
||||
val draft = at.mocode.desktop.v2.TurnierV2(
|
||||
|
|
@ -709,6 +729,7 @@ private fun DesktopContentArea(
|
|||
},
|
||||
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
|
||||
)
|
||||
|
||||
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
|
||||
onBack = onBack,
|
||||
onSave = { onBack() },
|
||||
|
|
@ -743,6 +764,7 @@ private fun DesktopContentArea(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
is AppScreen.TurnierNeu -> {
|
||||
val evtId = currentScreen.veranstaltungId
|
||||
// V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert
|
||||
|
|
@ -800,11 +822,11 @@ private fun DesktopContentArea(
|
|||
}
|
||||
|
||||
is AppScreen.Meisterschaften -> {
|
||||
SeriesScreen(title = "Meisterschaften", onBack = onBack)
|
||||
SeriesScreen(title = "Meisterschaften", onBack = onBack)
|
||||
}
|
||||
|
||||
is AppScreen.Cups -> {
|
||||
SeriesScreen(title = "Cups", onBack = onBack)
|
||||
SeriesScreen(title = "Cups", onBack = onBack)
|
||||
}
|
||||
|
||||
is AppScreen.Nennung -> {
|
||||
|
|
@ -832,11 +854,22 @@ private fun DesktopContentArea(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopFooterBar() {
|
||||
// Echte Status-Logik vorbereitet
|
||||
val online = remember { mutableStateOf(true) }
|
||||
val deviceConnected = remember { mutableStateOf(true) }
|
||||
val deviceName = "Richter-Turm"
|
||||
private fun DesktopFooterBar(settings: OnboardingSettings) {
|
||||
val connectivityTracker = koinInject<ConnectivityTracker>()
|
||||
val discoveryService = koinInject<NetworkDiscoveryService>()
|
||||
|
||||
val online by connectivityTracker.isOnline.collectAsState()
|
||||
val discoveredServices = remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
|
||||
val deviceName = settings.geraetName.ifBlank { "Unbekannt" }
|
||||
|
||||
// Periodisches Update der LAN-Geräte (mDNS)
|
||||
LaunchedEffect(Unit) {
|
||||
discoveryService.startDiscovery()
|
||||
while (true) {
|
||||
discoveredServices.value = discoveryService.getDiscoveredServices()
|
||||
delay(5000.milliseconds)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
|
|
@ -854,18 +887,19 @@ private fun DesktopFooterBar() {
|
|||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// Status: Cloud Sync
|
||||
StatusIndicator(
|
||||
icon = if (online.value) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
||||
label = if (online.value) "Cloud synchronisiert" else "Offline (Lokal)",
|
||||
color = if (online.value) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||
icon = if (online) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
||||
label = if (online) "Cloud synchronisiert" else "Offline (Lokal)",
|
||||
color = if (online) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(Dimens.SpacingM))
|
||||
|
||||
// Status: LAN Devices (mDNS)
|
||||
val deviceCount = discoveredServices.value.size
|
||||
StatusIndicator(
|
||||
icon = Icons.Filled.Lan,
|
||||
label = if (deviceConnected.value) "Verbunden: $deviceName" else "Suche nach Geräten...",
|
||||
color = if (deviceConnected.value) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||
label = if (deviceCount > 0) "Verbunden: $deviceName ($deviceCount im Netz)" else "Lokal: $deviceName",
|
||||
color = if (deviceCount > 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ fun PreviewTurnierOrganisationTab() {
|
|||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||
}
|
||||
val vm = NennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
MaterialTheme {
|
||||
OrganisationTabContent(viewModel = vm)
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ fun PreviewTurnierNennungenTab() {
|
|||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||
}
|
||||
val vm = NennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||
MaterialTheme {
|
||||
NennungenTabContent(viewModel = vm)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
|
||||
import at.mocode.frontend.features.nennung.domain.NennungResponse
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
data class OnlineNennungMail(
|
||||
val id: String,
|
||||
|
|
@ -38,14 +40,47 @@ data class OnlineNennungMail(
|
|||
var status: String = "NEU"
|
||||
)
|
||||
|
||||
fun NennungResponse.toMail() = OnlineNennungMail(
|
||||
id = id,
|
||||
sender = email,
|
||||
empfaenger = "Meldestelle",
|
||||
datum = "-", // Datum ist in Entity nicht direkt drin, könnte man ergänzen
|
||||
turnierNr = turnierNr,
|
||||
vorname = vorname,
|
||||
nachname = nachname,
|
||||
lizenz = lizenz,
|
||||
pferd = pferdName,
|
||||
pferdAlter = pferdAlter,
|
||||
telefon = telefon,
|
||||
bewerbe = bewerbe,
|
||||
bemerkungen = bemerkungen,
|
||||
status = if (status == "GELESEN") "GELESEN" else "NEU"
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NennungsEingangScreen(onBack: () -> Unit) {
|
||||
val repository: NennungRemoteRepository = koinInject()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
DesktopThemeV2 {
|
||||
var mails by remember { mutableStateOf<List<OnlineNennungMail>>(emptyList()) }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var selectedMail by remember { mutableStateOf<OnlineNennungMail?>(null) }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
|
||||
val refresh = {
|
||||
scope.launch {
|
||||
isRefreshing = true
|
||||
repository.holeNennungen().onSuccess { response ->
|
||||
mails = response.map { it.toMail() }
|
||||
}.onFailure {
|
||||
// Fallback oder Fehleranzeige
|
||||
if (mails.isEmpty()) mails = getMockMails()
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
val filteredMails = remember(mails, searchQuery) {
|
||||
if (searchQuery.isBlank()) mails
|
||||
else mails.filter {
|
||||
|
|
@ -58,10 +93,7 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||
|
||||
// Initiales Laden
|
||||
LaunchedEffect(Unit) {
|
||||
isRefreshing = true
|
||||
delay(800.milliseconds)
|
||||
mails = getMockMails()
|
||||
isRefreshing = false
|
||||
refresh()
|
||||
}
|
||||
|
||||
if (selectedMail != null) {
|
||||
|
|
@ -69,9 +101,12 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||
mail = selectedMail!!,
|
||||
onDismiss = { selectedMail = null },
|
||||
onMarkProcessed = {
|
||||
scope.launch {
|
||||
repository.markiereAlsGelesen(selectedMail!!.id)
|
||||
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
|
||||
mails = updated
|
||||
selectedMail = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -85,7 +120,7 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||
Spacer(Modifier.weight(1f))
|
||||
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||
Button(
|
||||
onClick = { /* Refresh Logik */ },
|
||||
onClick = { refresh() },
|
||||
enabled = !isRefreshing
|
||||
) {
|
||||
Icon(Icons.Default.Refresh, null)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user