Compare commits
8 Commits
a6fcb81594
...
f98a9075ae
| Author | SHA1 | Date | |
|---|---|---|---|
| f98a9075ae | |||
| 7581f15dfb | |||
| 67d7b38d79 | |||
| 6d631acce6 | |||
| 1cefc26be9 | |||
| 18e41a90b6 | |||
| d026e7f83c | |||
| 26ac3007b9 |
+13
-2
@@ -120,6 +120,13 @@ MAILPIT_IMAGE=axllent/mailpit:v1.29
|
|||||||
MAILPIT_WEB_PORT=8025:8025
|
MAILPIT_WEB_PORT=8025:8025
|
||||||
MAILPIT_SMTP_PORT=1025:1025
|
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 ---
|
||||||
PGADMIN_IMAGE=dpage/pgadmin4:8
|
PGADMIN_IMAGE=dpage/pgadmin4:8
|
||||||
PGADMIN_EMAIL=meldestelle@mo-code.at
|
PGADMIN_EMAIL=meldestelle@mo-code.at
|
||||||
@@ -149,6 +156,8 @@ GATEWAY_DEBUG_PORT=5005:5005
|
|||||||
GATEWAY_SERVER_PORT=8081
|
GATEWAY_SERVER_PORT=8081
|
||||||
GATEWAY_SPRING_PROFILES_ACTIVE=docker
|
GATEWAY_SPRING_PROFILES_ACTIVE=docker
|
||||||
GATEWAY_DEBUG=true
|
GATEWAY_DEBUG=true
|
||||||
|
GATEWAY_SERVICE_NAME=api-gateway
|
||||||
|
GATEWAY_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
# --- PING-SERVICE ---
|
# --- PING-SERVICE ---
|
||||||
PING_SPRING_PROFILES_ACTIVE=docker
|
PING_SPRING_PROFILES_ACTIVE=docker
|
||||||
@@ -160,9 +169,9 @@ PING_SERVICE_NAME=ping-service
|
|||||||
PING_CONSUL_PREFER_IP=true
|
PING_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
# --- MAIL-SERVICE ---
|
# --- MAIL-SERVICE ---
|
||||||
MAIL_PORT=8083:8085
|
MAIL_PORT=8083:8083
|
||||||
MAIL_DEBUG_PORT=5014:5014
|
MAIL_DEBUG_PORT=5014:5014
|
||||||
MAIL_SERVER_PORT=8085
|
MAIL_SERVER_PORT=8083
|
||||||
MAIL_SPRING_PROFILES_ACTIVE=docker
|
MAIL_SPRING_PROFILES_ACTIVE=docker
|
||||||
MAIL_DEBUG=true
|
MAIL_DEBUG=true
|
||||||
MAIL_SERVICE_NAME=mail-service
|
MAIL_SERVICE_NAME=mail-service
|
||||||
@@ -234,6 +243,8 @@ SERIES_DEBUG_PORT=5011:5011
|
|||||||
SERIES_SERVER_PORT=8089
|
SERIES_SERVER_PORT=8089
|
||||||
SERIES_SPRING_PROFILES_ACTIVE=docker
|
SERIES_SPRING_PROFILES_ACTIVE=docker
|
||||||
SERIES_DEBUG=true
|
SERIES_DEBUG=true
|
||||||
|
SERIES_SERVICE_NAME=series-service
|
||||||
|
SERIES_CONSUL_PREFER_IP=true
|
||||||
|
|
||||||
# --- WEB-APP ---
|
# --- WEB-APP ---
|
||||||
WEB_APP_PORT=4000:4000
|
WEB_APP_PORT=4000:4000
|
||||||
|
|||||||
-74
@@ -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
|
|
||||||
@@ -15,6 +15,32 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
### [Unreleased]
|
### [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
|
### Hinzugefügt
|
||||||
- **Phase 12 (Abrechnung & Infrastruktur) - 12.04.2026:**
|
- **Phase 12 (Abrechnung & Infrastruktur) - 12.04.2026:**
|
||||||
- **Infrastruktur:** Docker-Integration für `billing-service` (Port 8087) und API-Gateway Routing vervollständigt.
|
- **Infrastruktur:** Docker-Integration für `billing-service` (Port 8087) und API-Gateway Routing vervollständigt.
|
||||||
|
|||||||
+5
-1
@@ -72,7 +72,9 @@ class BewerbService(
|
|||||||
pausenBezeichnung = req.pausenBezeichnung,
|
pausenBezeichnung = req.pausenBezeichnung,
|
||||||
// Finanzen
|
// Finanzen
|
||||||
startgeldCent = req.startgeldCent,
|
startgeldCent = req.startgeldCent,
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt
|
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||||
|
znsNummer = req.znsNummer,
|
||||||
|
znsAbteilung = req.znsAbteilung
|
||||||
)
|
)
|
||||||
return repo.create(b)
|
return repo.create(b)
|
||||||
}
|
}
|
||||||
@@ -163,6 +165,8 @@ class BewerbService(
|
|||||||
// Finanzen
|
// Finanzen
|
||||||
startgeldCent = req.startgeldCent,
|
startgeldCent = req.startgeldCent,
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
||||||
|
znsNummer = req.znsNummer,
|
||||||
|
znsAbteilung = req.znsAbteilung
|
||||||
)
|
)
|
||||||
return repo.update(updated)
|
return repo.update(updated)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
package at.mocode.identity.domain.model
|
package at.mocode.identity.domain.model
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repräsentiert eine registrierte Desktop-Instanz ("Gerät").
|
* Repräsentiert eine registrierte Desktop-Instanz ("Gerät").
|
||||||
@@ -13,7 +13,8 @@ data class Device(
|
|||||||
val securityKeyHash: String, // Gehasht für Sicherheit
|
val securityKeyHash: String, // Gehasht für Sicherheit
|
||||||
val role: DeviceRole = DeviceRole.CLIENT,
|
val role: DeviceRole = DeviceRole.CLIENT,
|
||||||
val lastSyncAt: Instant? = null,
|
val lastSyncAt: Instant? = null,
|
||||||
val createdAt: Instant
|
val createdAt: Instant,
|
||||||
|
val updatedAt: Instant = createdAt
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class DeviceRole {
|
enum class DeviceRole {
|
||||||
|
|||||||
+1
-1
@@ -1,8 +1,8 @@
|
|||||||
package at.mocode.identity.domain.repository
|
package at.mocode.identity.domain.repository
|
||||||
|
|
||||||
import at.mocode.identity.domain.model.Device
|
import at.mocode.identity.domain.model.Device
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
interface DeviceRepository {
|
interface DeviceRepository {
|
||||||
suspend fun findById(id: UUID): Device?
|
suspend fun findById(id: UUID): Device?
|
||||||
|
|||||||
+11
-12
@@ -11,7 +11,6 @@ import org.jetbrains.exposed.v1.jdbc.update
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlin.time.toJavaInstant
|
|
||||||
|
|
||||||
class ExposedDeviceRepository : DeviceRepository {
|
class ExposedDeviceRepository : DeviceRepository {
|
||||||
|
|
||||||
@@ -36,8 +35,8 @@ class ExposedDeviceRepository : DeviceRepository {
|
|||||||
it[name] = device.name
|
it[name] = device.name
|
||||||
it[securityKeyHash] = device.securityKeyHash
|
it[securityKeyHash] = device.securityKeyHash
|
||||||
it[role] = device.role
|
it[role] = device.role
|
||||||
it[lastSyncAt] = device.lastSyncAt?.toJavaInstant()
|
it[lastSyncAt] = device.lastSyncAt
|
||||||
it[updatedAt] = now.toJavaInstant()
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
DeviceTable.insert {
|
DeviceTable.insert {
|
||||||
@@ -45,19 +44,18 @@ class ExposedDeviceRepository : DeviceRepository {
|
|||||||
it[name] = device.name
|
it[name] = device.name
|
||||||
it[securityKeyHash] = device.securityKeyHash
|
it[securityKeyHash] = device.securityKeyHash
|
||||||
it[role] = device.role
|
it[role] = device.role
|
||||||
it[lastSyncAt] = device.lastSyncAt?.toJavaInstant()
|
it[lastSyncAt] = device.lastSyncAt
|
||||||
it[createdAt] = now.toJavaInstant()
|
it[createdAt] = device.createdAt
|
||||||
it[updatedAt] = now.toJavaInstant()
|
it[updatedAt] = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
device
|
device.copy(updatedAt = now)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateLastSyncAt(id: UUID, at: Instant): Boolean = transaction {
|
override suspend fun updateLastSyncAt(id: UUID, at: Instant): Boolean = transaction {
|
||||||
val javaInstant = at.toJavaInstant()
|
|
||||||
DeviceTable.update({ DeviceTable.id eq id }) {
|
DeviceTable.update({ DeviceTable.id eq id }) {
|
||||||
it[lastSyncAt] = javaInstant
|
it[lastSyncAt] = at
|
||||||
it[updatedAt] = javaInstant
|
it[updatedAt] = at
|
||||||
} > 0
|
} > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +64,8 @@ class ExposedDeviceRepository : DeviceRepository {
|
|||||||
name = row[DeviceTable.name],
|
name = row[DeviceTable.name],
|
||||||
securityKeyHash = row[DeviceTable.securityKeyHash],
|
securityKeyHash = row[DeviceTable.securityKeyHash],
|
||||||
role = row[DeviceTable.role],
|
role = row[DeviceTable.role],
|
||||||
lastSyncAt = row[DeviceTable.lastSyncAt]?.let { Instant.fromEpochMilliseconds(it.toEpochMilli()) },
|
lastSyncAt = row[DeviceTable.lastSyncAt],
|
||||||
createdAt = Instant.fromEpochMilliseconds(row[DeviceTable.createdAt].toEpochMilli())
|
createdAt = row[DeviceTable.createdAt],
|
||||||
|
updatedAt = row[DeviceTable.updatedAt]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -26,4 +26,9 @@ class NennungController(
|
|||||||
) {
|
) {
|
||||||
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
fun createNennung(@RequestBody nennung: NennungEntity) {
|
||||||
|
nennungRepository.save(nennung)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -61,7 +61,7 @@ services:
|
|||||||
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:8086"
|
||||||
EVENTS_SERVICE_URL: "http://events-service:8085"
|
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"
|
ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095"
|
||||||
RESULTS_SERVICE_URL: "http://results-service:8088"
|
RESULTS_SERVICE_URL: "http://results-service:8088"
|
||||||
BILLING_SERVICE_URL: "http://billing-service:8087"
|
BILLING_SERVICE_URL: "http://billing-service:8087"
|
||||||
@@ -559,12 +559,12 @@ services:
|
|||||||
container_name: "${PROJECT_NAME:-meldestelle}-mail-service"
|
container_name: "${PROJECT_NAME:-meldestelle}-mail-service"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "${MAIL_PORT:-8083:8085}"
|
- "${MAIL_PORT:-8083:8083}"
|
||||||
- "${MAIL_DEBUG_PORT:-5014:5014}"
|
- "${MAIL_DEBUG_PORT:-5014:5014}"
|
||||||
environment:
|
environment:
|
||||||
SPRING_PROFILES_ACTIVE: "${MAIL_SPRING_PROFILES_ACTIVE:-docker}"
|
SPRING_PROFILES_ACTIVE: "${MAIL_SPRING_PROFILES_ACTIVE:-docker}"
|
||||||
DEBUG: "${MAIL_DEBUG:-true}"
|
DEBUG: "${MAIL_DEBUG:-true}"
|
||||||
SERVER_PORT: "${MAIL_SERVER_PORT:-8085}"
|
SERVER_PORT: "${MAIL_SERVER_PORT:-8083}"
|
||||||
|
|
||||||
# --- KEYCLOAK ---
|
# --- KEYCLOAK ---
|
||||||
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
|
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://keycloak:8080/realms/meldestelle}"
|
||||||
@@ -601,7 +601,7 @@ services:
|
|||||||
condition: "service_healthy"
|
condition: "service_healthy"
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "curl", "-f", "http://localhost:8085/actuator/health/readiness" ]
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8083/actuator/health/readiness" ]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -724,7 +724,13 @@ services:
|
|||||||
|
|
||||||
# --- CONSUL ---
|
# --- CONSUL ---
|
||||||
SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-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:
|
depends_on:
|
||||||
postgres:
|
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] Konfiguration der World4You SMTP/IMAP Zugangsdaten.
|
||||||
* [x] Mailpit Integration für lokale Tests (bereits in `dc-ops.yaml`).
|
* [x] Mailpit Integration für lokale Tests (bereits in `dc-ops.yaml`).
|
||||||
|
|
||||||
### Phase 2: Das Web-Formular (WasmJS Frontend) 🏗️
|
### Phase 2: Das Web-Formular (WasmJS Frontend) ✅
|
||||||
* [ ] **Basis-UI:** Erstellung des Formulars gemäß Spezifikation (Reiter, Pferd, Lizenz, Bewerbe).
|
* [x] **Basis-UI:** Erstellung des Formulars gemäß Spezifikation (Reiter, Pferd, Lizenz, Bewerbe).
|
||||||
* [ ] **Validierung:** Implementierung der Pflichtfeld-Prüfung (Buttonsperre bis alles ok).
|
* [x] **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.
|
* [x] **Mail-Versand:** Integration des API-Calls an das Backend (`mail-service`), um die Nennung zu speichern.
|
||||||
* [ ] **DSGVO:** Checkbox und Hinweistext einbauen.
|
* [x] **DSGVO:** Checkbox und Hinweistext eingebaut.
|
||||||
|
|
||||||
### Phase 3: Mail-Service (Backend-Verarbeitung) 🏗️
|
### 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).
|
* [ ] **Polling:** Implementierung des IMAP-Pollers (imap.world4you.com).
|
||||||
* [ ] **Parsing:** Extraktion der Turnier-Nummer aus dem `To`-Header und Mapping auf das Datenbank-Schema (Tenant).
|
* [ ] **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.
|
* [x] **Auto-Reply:** Automatisches Versenden der Eingangsbestätigung (in `MailPollingService` vorbereitet).
|
||||||
* [ ] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle für den `registration-context`.
|
* [x] **Persistence:** Speichern der eingegangenen "Nennungs-Mails" in einer temporären Tabelle.
|
||||||
|
|
||||||
### Phase 4: Desktop-Zentrale Integration 🏗️
|
### Phase 4: Desktop-Zentrale Integration ✅
|
||||||
* [ ] **UI-Tab:** Neuer Reiter "Nennungs-Eingang" in der Turnierverwaltung.
|
* [x] **UI-Tab:** Neuer Reiter "Online-Eingang" in der Turnierverwaltung (`TurnierDetailScreen`).
|
||||||
* [ ] **Vorschau:** Anzeige der eingegangenen Mails mit Details (Reiter, Pferd, Bewerbe).
|
* [x] **Vorschau:** Anzeige der eingegangenen Nennungen mit Details (`OnlineNennungEingangTabContent`).
|
||||||
* [ ] **Übernahme:** "Übernehmen"-Button, der die Daten in die Turnieranmeldung vor-ausfüllt.
|
* [x] **Übernahme:** "Übernehmen"-Button, der Reiter/Pferd in die Nennung vorausfüllt (`NennungViewModel`).
|
||||||
* [ ] **Abschluss:** Manueller "Bestätigen"-Button zum Versenden der finalen Bestätigungsmail.
|
* [ ] **Abschluss:** Manueller "Bestätigen"-Button zum Versenden der finalen Bestätigungsmail.
|
||||||
|
|
||||||
### Phase 5: End-to-End Test & Deployment 🚀 (Deadline: 21.04.2026)
|
### Phase 5: End-to-End Test & Deployment 🚀 (Deadline: 21.04.2026)
|
||||||
|
|||||||
@@ -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. 🚀🐎
|
||||||
+48
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-3
@@ -1,5 +1,7 @@
|
|||||||
package at.mocode.frontend.core.network
|
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.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
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.module.Module
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
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.
|
* 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:
|
* Koin-Modul mit zwei HttpClient-Instanzen:
|
||||||
@@ -26,6 +28,8 @@ interface TokenProvider { fun getAccessToken(): String? }
|
|||||||
val networkModule: Module = module {
|
val networkModule: Module = module {
|
||||||
includes(discoveryModule, syncModule)
|
includes(discoveryModule, syncModule)
|
||||||
|
|
||||||
|
single<ConnectivityTracker> { ConnectivityTracker() }
|
||||||
|
|
||||||
// 1. Basis-Client (für Auth-Endpunkte, ohne Bearer-Token)
|
// 1. Basis-Client (für Auth-Endpunkte, ohne Bearer-Token)
|
||||||
single(named("baseHttpClient")) {
|
single(named("baseHttpClient")) {
|
||||||
HttpClient {
|
HttpClient {
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ actual object PlatformConfig {
|
|||||||
actual fun resolveMailServiceUrl(): String {
|
actual fun resolveMailServiceUrl(): String {
|
||||||
val env = System.getenv("MAIL_SERVICE_URL")?.trim().orEmpty()
|
val env = System.getenv("MAIL_SERVICE_URL")?.trim().orEmpty()
|
||||||
if (env.isNotEmpty()) return env.removeSuffix("/")
|
if (env.isNotEmpty()) return env.removeSuffix("/")
|
||||||
return "http://localhost:8085"
|
return "http://localhost:8083"
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun resolveKeycloakUrl(): String {
|
actual fun resolveKeycloakUrl(): String {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ kotlin {
|
|||||||
implementation(projects.frontend.core.network)
|
implementation(projects.frontend.core.network)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
|
// Network & Serialization
|
||||||
|
implementation(libs.bundles.ktor.client.common)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
|||||||
+3
-2
@@ -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.domain.NennungRemoteRepository
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
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.module.dsl.viewModel
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val nennungFeatureModule = module {
|
val nennungFeatureModule = module {
|
||||||
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>()) }
|
single<NennungRemoteRepository> { NennungRemoteRepository(get<HttpClient>(named("apiClient"))) }
|
||||||
viewModel { NennungViewModel() }
|
viewModel { NennungViewModel() }
|
||||||
}
|
}
|
||||||
|
|||||||
+12
@@ -62,6 +62,18 @@ data class VerkaufArtikel(
|
|||||||
val betrag: Double get() = menge * einzelpreis
|
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) ---
|
// --- Mock-Daten (werden später durch echte API ersetzt) ---
|
||||||
object NennungMockData {
|
object NennungMockData {
|
||||||
|
|
||||||
|
|||||||
+36
@@ -3,10 +3,27 @@ package at.mocode.frontend.features.nennung.domain
|
|||||||
import at.mocode.frontend.core.network.PlatformConfig
|
import at.mocode.frontend.core.network.PlatformConfig
|
||||||
import at.mocode.frontend.features.nennung.presentation.web.NennungPayload
|
import at.mocode.frontend.features.nennung.presentation.web.NennungPayload
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import kotlinx.serialization.Serializable
|
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
|
@Serializable
|
||||||
data class NennungApiRequest(
|
data class NennungApiRequest(
|
||||||
val turnierNr: String,
|
val turnierNr: String,
|
||||||
@@ -24,6 +41,25 @@ data class NennungApiRequest(
|
|||||||
class NennungRemoteRepository(private val client: HttpClient) {
|
class NennungRemoteRepository(private val client: HttpClient) {
|
||||||
private val mailServiceUrl = PlatformConfig.resolveMailServiceUrl()
|
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> {
|
suspend fun sendeNennung(turnierNr: String, payload: NennungPayload): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
val request = NennungApiRequest(
|
val request = NennungApiRequest(
|
||||||
|
|||||||
+63
-1
@@ -2,10 +2,19 @@ package at.mocode.frontend.features.nennung.presentation
|
|||||||
|
|
||||||
import at.mocode.frontend.features.nennung.domain.*
|
import at.mocode.frontend.features.nennung.domain.*
|
||||||
import androidx.lifecycle.ViewModel
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
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 ---
|
// --- UI State ---
|
||||||
data class NennungUiState(
|
data class NennungUiState(
|
||||||
@@ -22,16 +31,69 @@ data class NennungUiState(
|
|||||||
val activeNennungTab: NennungTab = NennungTab.REITER,
|
val activeNennungTab: NennungTab = NennungTab.REITER,
|
||||||
val activeVerkaufTab: VerkaufTab = VerkaufTab.VERKAUF,
|
val activeVerkaufTab: VerkaufTab = VerkaufTab.VERKAUF,
|
||||||
val statusMeldung: String? = null,
|
val statusMeldung: String? = null,
|
||||||
|
val onlineNennungen: List<OnlineNennung> = emptyList(),
|
||||||
|
val isOnlineLoading: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class NennungTab { REITER, PFERD, BEWERBE }
|
enum class NennungTab { REITER, PFERD, BEWERBE }
|
||||||
enum class VerkaufTab { VERKAUF, BUCHUNGEN }
|
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())
|
private val _uiState = MutableStateFlow(NennungUiState())
|
||||||
val uiState: StateFlow<NennungUiState> = _uiState.asStateFlow()
|
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 ---
|
// --- Pferd-Suche ---
|
||||||
fun onPferdSucheChanged(query: String) {
|
fun onPferdSucheChanged(query: String) {
|
||||||
val vorschlaege = if (query.length >= 2) {
|
val vorschlaege = if (query.length >= 2) {
|
||||||
|
|||||||
+79
@@ -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.network)
|
||||||
implementation(projects.frontend.core.navigation)
|
implementation(projects.frontend.core.navigation)
|
||||||
implementation(projects.frontend.features.billingFeature)
|
implementation(projects.frontend.features.billingFeature)
|
||||||
|
implementation(projects.frontend.features.nennungFeature)
|
||||||
implementation(projects.core.znsParser)
|
implementation(projects.core.znsParser)
|
||||||
|
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ actual val turnierFeatureModule = module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
factory { (turnierId: Long) ->
|
factory { (turnierId: Long) ->
|
||||||
NennungViewModel(
|
TurnierNennungViewModel(
|
||||||
nennungRepo = get(),
|
nennungRepo = get(),
|
||||||
masterdataRepo = get(),
|
masterdataRepo = get(),
|
||||||
turnierId = turnierId
|
turnierId = turnierId
|
||||||
|
|||||||
+10
-5
@@ -57,6 +57,7 @@ fun TurnierDetailScreen(
|
|||||||
"ARTIKEL",
|
"ARTIKEL",
|
||||||
"ABRECHNUNG",
|
"ABRECHNUNG",
|
||||||
"NENNUNGEN",
|
"NENNUNGEN",
|
||||||
|
"ONLINE-EINGANG",
|
||||||
"ZEITPLAN",
|
"ZEITPLAN",
|
||||||
"STARTLISTEN",
|
"STARTLISTEN",
|
||||||
"ERGEBNISLISTEN",
|
"ERGEBNISLISTEN",
|
||||||
@@ -103,22 +104,26 @@ fun TurnierDetailScreen(
|
|||||||
veranstalterLogoUrl = veranstalterLogoUrl,
|
veranstalterLogoUrl = veranstalterLogoUrl,
|
||||||
)
|
)
|
||||||
1 -> {
|
1 -> {
|
||||||
val nennungViewModel = koinInject<NennungViewModel>(parameters = { parametersOf(turnierId) })
|
val nennungViewModel = koinInject<TurnierNennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||||
OrganisationTabContent(viewModel = nennungViewModel)
|
OrganisationTabContent(viewModel = nennungViewModel)
|
||||||
}
|
}
|
||||||
2 -> BewerbeTabContent(viewModel = bewerbViewModel, turnierId = turnierId)
|
2 -> BewerbeTabContent(viewModel = bewerbViewModel, turnierId = turnierId)
|
||||||
3 -> ArtikelTabContent()
|
3 -> ArtikelTabContent()
|
||||||
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
|
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
|
||||||
5 -> {
|
5 -> {
|
||||||
val nennungViewModel = koinInject<NennungViewModel>(parameters = { parametersOf(turnierId) })
|
val nennungViewModel = koinInject<TurnierNennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||||
NennungenTabContent(
|
NennungenTabContent(
|
||||||
viewModel = nennungViewModel,
|
viewModel = nennungViewModel,
|
||||||
onAbrechnungClick = { selectedTab = 4 }
|
onAbrechnungClick = { selectedTab = 4 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
6 -> ZeitplanTabContent(turnierId = turnierId, viewModel = bewerbViewModel)
|
6 -> {
|
||||||
7 -> StartlistenTabContent()
|
val nennungViewModel = koinInject<TurnierNennungViewModel>(parameters = { parametersOf(turnierId) })
|
||||||
8 -> ErgebnislistenTabContent()
|
OnlineNennungEingangTabContent(turnierNr = turnierId.toString(), viewModel = nennungViewModel)
|
||||||
|
}
|
||||||
|
7 -> ZeitplanTabContent(turnierId = turnierId, viewModel = bewerbViewModel)
|
||||||
|
8 -> StartlistenTabContent()
|
||||||
|
9 -> ErgebnislistenTabContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-2
@@ -9,6 +9,23 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
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(
|
data class NennungenState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val nennungen: List<Nennung> = emptyList(),
|
val nennungen: List<Nennung> = emptyList(),
|
||||||
@@ -20,11 +37,32 @@ data class NennungenState(
|
|||||||
val errorMessage: String? = null
|
val errorMessage: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
class NennungViewModel(
|
class TurnierNennungViewModel(
|
||||||
private val nennungRepo: NennungRepository,
|
private val nennungRepo: NennungRepository,
|
||||||
private val masterdataRepo: MasterdataRepository,
|
private val masterdataRepo: MasterdataRepository,
|
||||||
private val turnierId: Long
|
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 scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
private val _state = MutableStateFlow(NennungenState())
|
private val _state = MutableStateFlow(NennungenState())
|
||||||
@@ -65,7 +103,6 @@ class NennungViewModel(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
masterdataRepo.saveReiter(reiter).onSuccess {
|
masterdataRepo.saveReiter(reiter).onSuccess {
|
||||||
_state.value = _state.value.copy(selectedReiter = null)
|
_state.value = _state.value.copy(selectedReiter = null)
|
||||||
// Evtl. Suchen/Listen aktualisieren
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -31,8 +31,8 @@ private val NennSelectedBg = Color(0xFFEFF6FF)
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun NennungenTabContent(
|
fun NennungenTabContent(
|
||||||
viewModel: NennungViewModel,
|
viewModel: TurnierNennungViewModel,
|
||||||
onAbrechnungClick: () -> Unit = {}
|
onAbrechnungClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ fun NennungenTabContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NennungenSuchePanel(viewModel: NennungViewModel, state: NennungenState) {
|
private fun NennungenSuchePanel(viewModel: TurnierNennungViewModel, state: NennungenState) {
|
||||||
var pferdQuery by remember { mutableStateOf("") }
|
var pferdQuery by remember { mutableStateOf("") }
|
||||||
var reiterQuery by remember { mutableStateOf("") }
|
var reiterQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ private fun NennungenSuchePanel(viewModel: NennungViewModel, state: NennungenSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NennungenTabelle(viewModel: NennungViewModel, state: NennungenState) {
|
private fun NennungenTabelle(viewModel: TurnierNennungViewModel, state: NennungenState) {
|
||||||
var selectedIndex by remember { mutableIntStateOf(-1) }
|
var selectedIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|||||||
+108
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -29,7 +29,7 @@ private val DeleteRed = Color(0xFFDC2626)
|
|||||||
* - Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen)
|
* - Austragungsplätze: dynamische Liste (Sparte, Größe, Bezeichnung, Löschen)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun OrganisationTabContent(viewModel: NennungViewModel) {
|
fun OrganisationTabContent(viewModel: TurnierNennungViewModel) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
var turnierleiter by remember { mutableStateOf("") }
|
var turnierleiter by remember { mutableStateOf("") }
|
||||||
|
|||||||
+4
-1
@@ -100,7 +100,10 @@ fun StammdatenTabContent(
|
|||||||
val katField = tClass.getDeclaredField("kategorie")
|
val katField = tClass.getDeclaredField("kategorie")
|
||||||
katField.isAccessible = true
|
katField.isAccessible = true
|
||||||
val kats = katField.get(turnier) as? List<String>
|
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")
|
val typField = tClass.getDeclaredField("typ")
|
||||||
typField.isAccessible = true
|
typField.isAccessible = true
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"geraetName": "Meldestelle",
|
"geraetName": "Meldestelle",
|
||||||
"sharedKey": "Meldestelle",
|
"sharedKey": "Meldestelle",
|
||||||
"backupPath": "/home/stefan/WsMeldestelle/Meldestelle/meldestelle/docs/temp",
|
"backupPath": "/mocode/Meldestelle/docs/temp",
|
||||||
"networkRole": "MASTER",
|
"networkRole": "MASTER"
|
||||||
"syncInterval": 20
|
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-22
@@ -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.AppColors
|
||||||
import at.mocode.frontend.core.designsystem.theme.Dimens
|
import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||||
import at.mocode.frontend.core.navigation.AppScreen
|
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.BillingScreen
|
||||||
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
import at.mocode.frontend.features.billing.presentation.BillingViewModel
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
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.AdminUebersichtScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
|
||||||
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
// Primärfarbe der TopBar (kann später ins Theme ausgelagert werden)
|
||||||
private val TopBarColor = Color(0xFF1E3A8A)
|
private val TopBarColor = Color(0xFF1E3A8A)
|
||||||
@@ -91,7 +95,7 @@ fun DesktopMainLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
|
HorizontalDivider(thickness = Dimens.BorderThin, color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
DesktopFooterBar()
|
DesktopFooterBar(settings = onboardingSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +253,10 @@ private fun DesktopTopHeader(
|
|||||||
BreadcrumbContent(currentScreen, onNavigate)
|
BreadcrumbContent(currentScreen, onNavigate)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)) {
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingM)
|
||||||
|
) {
|
||||||
// Profil / Logout Bereich
|
// Profil / Logout Bereich
|
||||||
Text(
|
Text(
|
||||||
text = "Administrator",
|
text = "Administrator",
|
||||||
@@ -296,6 +303,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.VeranstalterDetail -> {
|
is AppScreen.VeranstalterDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -309,6 +317,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.VeranstaltungProfil -> {
|
is AppScreen.VeranstaltungProfil -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -330,6 +339,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.VeranstaltungDetail -> {
|
is AppScreen.VeranstaltungDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -337,6 +347,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.VeranstaltungNeu -> {
|
is AppScreen.VeranstaltungNeu -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -344,6 +355,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.TurnierDetail -> {
|
is AppScreen.TurnierDetail -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -359,6 +371,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Billing -> {
|
is AppScreen.Billing -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -382,6 +395,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.TurnierNeu -> {
|
is AppScreen.TurnierNeu -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -413,6 +427,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Meisterschaften -> {
|
is AppScreen.Meisterschaften -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -420,6 +435,7 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Cups -> {
|
is AppScreen.Cups -> {
|
||||||
BreadcrumbSeparator()
|
BreadcrumbSeparator()
|
||||||
Text(
|
Text(
|
||||||
@@ -427,21 +443,22 @@ private fun BreadcrumbContent(
|
|||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
|
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
|
||||||
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
|
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
|
||||||
"OOE" -> "OÖ"
|
"OÖ" -> "Oberösterreich"
|
||||||
"NOE" -> "NÖ"
|
"NÖ" -> "Niederösterreich"
|
||||||
"ST" -> "Stmk."
|
"ST" -> "Steiermark"
|
||||||
"W" -> "Wien"
|
"W" -> "Wien"
|
||||||
"BGLD", "B" -> "Bgld."
|
"B" -> "Burgenland"
|
||||||
"K" -> "Ktn."
|
"K" -> "Kärnten"
|
||||||
"S" -> "Sbg."
|
"S" -> "Salzburg"
|
||||||
"T" -> "Tirol"
|
"T" -> "Tirol"
|
||||||
"V" -> "Vbg."
|
"V" -> "Vorarlberg"
|
||||||
else -> code
|
else -> code
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,6 +638,7 @@ private fun DesktopContentArea(
|
|||||||
onCancel = onBack,
|
onCancel = onBack,
|
||||||
onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
onVereinCreated = { newId -> onNavigate(AppScreen.VeranstalterProfil(newId)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.VeranstalterDetail -> {
|
is AppScreen.VeranstalterDetail -> {
|
||||||
val vId = currentScreen.veranstalterId
|
val vId = currentScreen.veranstalterId
|
||||||
if (vId != 1L) { // Temporärer Check für Mock-Daten
|
if (vId != 1L) { // Temporärer Check für Mock-Daten
|
||||||
@@ -637,6 +655,7 @@ private fun DesktopContentArea(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.VeranstaltungKonfig -> {
|
is AppScreen.VeranstaltungKonfig -> {
|
||||||
val vId = currentScreen.veranstalterId
|
val vId = currentScreen.veranstalterId
|
||||||
// Falls vId == 0, kommen wir aus der Gesamtübersicht und wählen erst im Wizard
|
// 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 ->
|
val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv ->
|
||||||
at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id }
|
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 list = at.mocode.desktop.v2.TurnierStoreV2.list(currentScreen.id)
|
||||||
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
|
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
|
||||||
val draft = at.mocode.desktop.v2.TurnierV2(
|
val draft = at.mocode.desktop.v2.TurnierV2(
|
||||||
@@ -709,6 +729,7 @@ private fun DesktopContentArea(
|
|||||||
},
|
},
|
||||||
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
|
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
|
||||||
)
|
)
|
||||||
|
|
||||||
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
|
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onSave = { onBack() },
|
onSave = { onBack() },
|
||||||
@@ -743,6 +764,7 @@ private fun DesktopContentArea(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.TurnierNeu -> {
|
is AppScreen.TurnierNeu -> {
|
||||||
val evtId = currentScreen.veranstaltungId
|
val evtId = currentScreen.veranstaltungId
|
||||||
// V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert
|
// V2: Wir erlauben Turnier-Nr nur, wenn die Veranstaltung im V2-Store existiert
|
||||||
@@ -800,11 +822,11 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Meisterschaften -> {
|
is AppScreen.Meisterschaften -> {
|
||||||
SeriesScreen(title = "Meisterschaften", onBack = onBack)
|
SeriesScreen(title = "Meisterschaften", onBack = onBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Cups -> {
|
is AppScreen.Cups -> {
|
||||||
SeriesScreen(title = "Cups", onBack = onBack)
|
SeriesScreen(title = "Cups", onBack = onBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppScreen.Nennung -> {
|
is AppScreen.Nennung -> {
|
||||||
@@ -832,11 +854,22 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DesktopFooterBar() {
|
private fun DesktopFooterBar(settings: OnboardingSettings) {
|
||||||
// Echte Status-Logik vorbereitet
|
val connectivityTracker = koinInject<ConnectivityTracker>()
|
||||||
val online = remember { mutableStateOf(true) }
|
val discoveryService = koinInject<NetworkDiscoveryService>()
|
||||||
val deviceConnected = remember { mutableStateOf(true) }
|
|
||||||
val deviceName = "Richter-Turm"
|
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(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
@@ -854,18 +887,19 @@ private fun DesktopFooterBar() {
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
// Status: Cloud Sync
|
// Status: Cloud Sync
|
||||||
StatusIndicator(
|
StatusIndicator(
|
||||||
icon = if (online.value) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
icon = if (online) Icons.Filled.CloudDone else Icons.Filled.CloudOff,
|
||||||
label = if (online.value) "Cloud synchronisiert" else "Offline (Lokal)",
|
label = if (online) "Cloud synchronisiert" else "Offline (Lokal)",
|
||||||
color = if (online.value) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
color = if (online) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.width(Dimens.SpacingM))
|
Spacer(Modifier.width(Dimens.SpacingM))
|
||||||
|
|
||||||
// Status: LAN Devices (mDNS)
|
// Status: LAN Devices (mDNS)
|
||||||
|
val deviceCount = discoveredServices.value.size
|
||||||
StatusIndicator(
|
StatusIndicator(
|
||||||
icon = Icons.Filled.Lan,
|
icon = Icons.Filled.Lan,
|
||||||
label = if (deviceConnected.value) "Verbunden: $deviceName" else "Suche nach Geräten...",
|
label = if (deviceCount > 0) "Verbunden: $deviceName ($deviceCount im Netz)" else "Lokal: $deviceName",
|
||||||
color = if (deviceConnected.value) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
color = if (deviceCount > 0) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -126,7 +126,7 @@ fun PreviewTurnierOrganisationTab() {
|
|||||||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||||
}
|
}
|
||||||
val vm = NennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
OrganisationTabContent(viewModel = vm)
|
OrganisationTabContent(viewModel = vm)
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ fun PreviewTurnierNennungenTab() {
|
|||||||
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
override suspend fun listVereine(): Result<List<Verein>> = Result.success(emptyList())
|
||||||
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
override suspend fun getVereinById(id: String): Result<Verein> = Result.failure(NotImplementedError())
|
||||||
}
|
}
|
||||||
val vm = NennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
val vm = TurnierNennungViewModel(mockNennungRepo, mockMasterdataRepo, 1L)
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
NennungenTabContent(viewModel = vm)
|
NennungenTabContent(viewModel = vm)
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-7
@@ -18,8 +18,10 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.delay
|
import at.mocode.frontend.features.nennung.domain.NennungRemoteRepository
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import at.mocode.frontend.features.nennung.domain.NennungResponse
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
data class OnlineNennungMail(
|
data class OnlineNennungMail(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -38,14 +40,47 @@ data class OnlineNennungMail(
|
|||||||
var status: String = "NEU"
|
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
|
@Composable
|
||||||
fun NennungsEingangScreen(onBack: () -> Unit) {
|
fun NennungsEingangScreen(onBack: () -> Unit) {
|
||||||
|
val repository: NennungRemoteRepository = koinInject()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
DesktopThemeV2 {
|
DesktopThemeV2 {
|
||||||
var mails by remember { mutableStateOf<List<OnlineNennungMail>>(emptyList()) }
|
var mails by remember { mutableStateOf<List<OnlineNennungMail>>(emptyList()) }
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
var selectedMail by remember { mutableStateOf<OnlineNennungMail?>(null) }
|
var selectedMail by remember { mutableStateOf<OnlineNennungMail?>(null) }
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
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) {
|
val filteredMails = remember(mails, searchQuery) {
|
||||||
if (searchQuery.isBlank()) mails
|
if (searchQuery.isBlank()) mails
|
||||||
else mails.filter {
|
else mails.filter {
|
||||||
@@ -58,10 +93,7 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
|
|
||||||
// Initiales Laden
|
// Initiales Laden
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
isRefreshing = true
|
refresh()
|
||||||
delay(800.milliseconds)
|
|
||||||
mails = getMockMails()
|
|
||||||
isRefreshing = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedMail != null) {
|
if (selectedMail != null) {
|
||||||
@@ -69,9 +101,12 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
mail = selectedMail!!,
|
mail = selectedMail!!,
|
||||||
onDismiss = { selectedMail = null },
|
onDismiss = { selectedMail = null },
|
||||||
onMarkProcessed = {
|
onMarkProcessed = {
|
||||||
|
scope.launch {
|
||||||
|
repository.markiereAlsGelesen(selectedMail!!.id)
|
||||||
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
|
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
|
||||||
mails = updated
|
mails = updated
|
||||||
selectedMail = null
|
selectedMail = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -85,7 +120,7 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
if (isRefreshing) CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
||||||
Button(
|
Button(
|
||||||
onClick = { /* Refresh Logik */ },
|
onClick = { refresh() },
|
||||||
enabled = !isRefreshing
|
enabled = !isRefreshing
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Refresh, null)
|
Icon(Icons.Default.Refresh, null)
|
||||||
|
|||||||
Reference in New Issue
Block a user