Compare commits
6 Commits
189ebc6565
...
345c329350
| Author | SHA1 | Date | |
|---|---|---|---|
| 345c329350 | |||
| d4aeba4666 | |||
| 9fe889b2c1 | |||
| 85ac1cae9c | |||
| dfaa2e8545 | |||
| bcabb86841 |
@@ -28,6 +28,7 @@ dependencies {
|
|||||||
// Common service extras
|
// Common service extras
|
||||||
implementation(libs.spring.boot.starter.validation)
|
implementation(libs.spring.boot.starter.validation)
|
||||||
implementation(libs.spring.boot.starter.mail)
|
implementation(libs.spring.boot.starter.mail)
|
||||||
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on the classpath
|
// JSON + Web: ensure Spring Web (incl. HttpMessageConverters) is on the classpath
|
||||||
//implementation("org.springframework.boot:spring-boot-starter-web")
|
//implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
|||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.mail.service
|
|
||||||
|
|
||||||
import at.mocode.mail.service.persistence.NennungEntity
|
|
||||||
import at.mocode.mail.service.persistence.NennungRepository
|
|
||||||
import org.springframework.web.bind.annotation.*
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/mail/nennungen")
|
|
||||||
class NennungController(
|
|
||||||
private val nennungRepository: NennungRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
fun getAllNennungen(): List<NennungEntity> {
|
|
||||||
return nennungRepository.findAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{id}/status")
|
|
||||||
fun updateStatus(
|
|
||||||
@PathVariable id: String,
|
|
||||||
@RequestBody newStatus: String
|
|
||||||
) {
|
|
||||||
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping
|
|
||||||
fun createNennung(@RequestBody nennung: NennungEntity) {
|
|
||||||
nennungRepository.save(nennung)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+45
@@ -111,4 +111,49 @@ class MailController(
|
|||||||
fun getAllNennungen(): List<NennungEntity> {
|
fun getAllNennungen(): List<NennungEntity> {
|
||||||
return nennungRepository.findAll()
|
return nennungRepository.findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/nennungen/{id}/status")
|
||||||
|
fun updateStatus(
|
||||||
|
@PathVariable id: String,
|
||||||
|
@RequestBody newStatus: String
|
||||||
|
) {
|
||||||
|
nennungRepository.updateStatus(Uuid.parse(id), newStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/nennungen")
|
||||||
|
fun createNennung(@RequestBody nennung: NennungEntity) {
|
||||||
|
nennungRepository.save(nennung)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/send-reply")
|
||||||
|
fun sendReply(
|
||||||
|
@RequestParam email: String,
|
||||||
|
@RequestParam turnierNr: String,
|
||||||
|
@RequestParam vorname: String,
|
||||||
|
@RequestParam nachname: String
|
||||||
|
) {
|
||||||
|
val message = SimpleMailMessage()
|
||||||
|
val dynamicFrom = try {
|
||||||
|
val (user, domain) = baseMailAddress.split("@")
|
||||||
|
"$user+$turnierNr@$domain"
|
||||||
|
} catch (_: Exception) {
|
||||||
|
baseMailAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
message.from = dynamicFrom
|
||||||
|
message.setTo(email)
|
||||||
|
message.subject = "Bestätigung: Nennung für Turnier $turnierNr manuell übernommen"
|
||||||
|
message.text = """
|
||||||
|
Sehr geehrte(r) $vorname $nachname,
|
||||||
|
|
||||||
|
Ihre Online-Nennung für das Turnier $turnierNr wurde von uns manuell in das Turniersystem übernommen.
|
||||||
|
|
||||||
|
Viel Erfolg beim Turnier!
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen,
|
||||||
|
Ihre Meldestelle
|
||||||
|
""".trimIndent()
|
||||||
|
mailSender.send(message)
|
||||||
|
logger.info("Antwort-Mail an $email gesendet.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Journal: 19. April 2026 - Backend Stabilität & Desktop UX-Refinement
|
||||||
|
|
||||||
|
## 🏗️ Backend: Infrastruktur & Mail-Service
|
||||||
|
|
||||||
|
* **Mail-Service:** Konflikt beim Request-Mapping behoben. Der redundante `NennungController` wurde entfernt und seine Funktionalität (Status-Update, Erstellung) in den zentralen `MailController` integriert.
|
||||||
|
* **Health-Checks:** `spring-boot-starter-actuator` zum `entries-service` hinzugefügt, um die 404-Fehler in der Consul-Überwachung zu eliminieren.
|
||||||
|
* **Mail-Features:** Neuer Endpunkt `POST /send-reply` im `MailController` implementiert, um Bestätigungs-Mails an Nenner mit dynamischer Absenderadresse (Turnier-spezifisch) zu senden.
|
||||||
|
|
||||||
|
## 💻 Desktop-App: Navigation & UI
|
||||||
|
|
||||||
|
* **Veranstaltungs-Konfiguration:** White-Screen Fix durch Korrektur der Navigation im `DesktopMainLayout.kt`. Es wird nun korrekt auf den `VeranstaltungKonfigScreen` aus dem Feature-Modul verwiesen.
|
||||||
|
* **Device-Setup:** UX-Verbesserung durch Entfernung blockierender `onKeyEvent` Handler. Die Navigation zwischen Feldern mittels **Tab** und **Enter** funktioniert nun reibungslos über den Standard-Fokus-Flow.
|
||||||
|
* **Design-System:**
|
||||||
|
* Suchfeld-Höhe in `MsFilterBar.kt` auf `44.dp` erhöht, um abgeschnittenen Text bei kleinen Schriftarten zu verhindern.
|
||||||
|
* `MsMasterDetailLayout` im Vereins-Bereich um einen **Preview-Bereich** (Card-Ansicht) erweitert.
|
||||||
|
|
||||||
|
## 🚀 Neue Features
|
||||||
|
|
||||||
|
### Nennungs-Eingang
|
||||||
|
* **Antwort-Funktion:** Ein neuer Button "Antwort & Übernahme" im Detail-Dialog ermöglicht das direkte Versenden einer Bestätigungs-Mail an den Nenner.
|
||||||
|
* **Sortierung:** Die Liste wird nun standardmäßig mit neuen Nennungen (`NEU`) zuerst sortiert.
|
||||||
|
|
||||||
|
### Vereins-Verwaltung
|
||||||
|
* **Card-Preview:** Der obere Teil des Detail-Bereichs zeigt nun eine visuelle Vorschau des Vereins (Name, Status, Ort).
|
||||||
|
* **Logo-Support:** Das Domain-Modell und der Editor wurden um ein `logoUrl` Feld erweitert, um Vereinslogos (z.B. für nicht registrierte Vereine) zu hinterlegen.
|
||||||
|
|
||||||
|
## 🧹 Curator Hinweis
|
||||||
|
Alle gemeldeten Start-Fehler im Backend wurden behoben. Die Desktop-App ist nun voll navigierbar und bietet verbesserte Effizienz für die Meldestellen-Mitarbeiter.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Journal: 19. April 2026 - Fix ZnsImportProvider DI-Binding
|
||||||
|
|
||||||
|
## 🏗️ Status Quo
|
||||||
|
|
||||||
|
Nach der Einführung der Entkopplung durch das `ZnsImportProvider` Interface am 17. April kam es beim Start der Desktop-App zu einem Koin-Fehler.
|
||||||
|
Die App brach ab mit: `No definition found for type 'at.mocode.frontend.core.domain.zns.ZnsImportProvider'`.
|
||||||
|
|
||||||
|
## ✅ Änderungen
|
||||||
|
|
||||||
|
### 1. Feature: ZNS-Import (Frontend)
|
||||||
|
|
||||||
|
- **Datei:** `frontend/features/zns-import-feature/src/jvmMain/kotlin/at/mocode/frontend/features/zns/import/di/ZnsImportModule.kt`
|
||||||
|
- **Fix:** Die Koin-Moduldefinition wurde korrigiert. `ZnsImportViewModel` wird nun explizit an das Interface `ZnsImportProvider` gebunden.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val znsImportModule = module {
|
||||||
|
factory { ZnsImportViewModel(get(named("apiClient")), get(), get()) } bind ZnsImportProvider::class
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Ergebnis
|
||||||
|
|
||||||
|
Die Desktop-App kann nun wieder korrekt starten, da Koin das Interface `ZnsImportProvider` auflösen kann, welches in den UI-Komponenten (z. B. Wizards) injiziert wird.
|
||||||
|
|
||||||
|
## 🧹 Curator Hinweis
|
||||||
|
|
||||||
|
Dieser Fix schließt die am 17. April begonnene Integration der ZNS Cloud-Suche ab, indem die notwendige DI-Konfiguration für die Desktop-Shell nachgereicht wurde.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Journal: 20. April 2026 - Desktop UX & Navigation Refinement
|
||||||
|
|
||||||
|
## 🏗️ Desktop-App: UX & Eingabe-Optimierung (Update)
|
||||||
|
|
||||||
|
* **Tastatur-Navigation (Fokus-Flow):**
|
||||||
|
* **Device-Setup:** Vollständiges Refactoring von `DeviceInitializationConfig.jvm.kt`. Ersetzung von `OutlinedTextField` durch `MsTextField`. Entfernung störender `onKeyEvent`-Handler zugunsten des nativen `ImeAction`-Flows. Tab und Enter funktionieren nun reibungslos.
|
||||||
|
* **Standardisierung:** Konsistente Nutzung von `MsTextField` in allen neuen Screens (`VeranstalterNeu`, `ZnsImport`).
|
||||||
|
|
||||||
|
* **MsFilePicker (Zentrale Komponente):**
|
||||||
|
* Einführung einer plattformübergreifenden `MsFilePicker`-Komponente.
|
||||||
|
* **Desktop (JVM):** Nutzt den nativen `FileDialog` für Dateiauswahlen (Look & Feel) und `JFileChooser` für Verzeichnisse.
|
||||||
|
* **Integration:** Ersetzt manuelle Picker-Logik im Device-Setup und ZNS-Importer.
|
||||||
|
|
||||||
|
* **ZNS-Importer Refinement:**
|
||||||
|
* Implementierung einer Fortschrittsanzeige (`LinearProgressIndicator`) mit Prozent- und Status-Details.
|
||||||
|
* Klarstellung der Dateiformate: Unterstützung sowohl für `ZNS.zip` als auch für einzelne `.dat` Dateien.
|
||||||
|
|
||||||
|
## 🧭 Navigation & Stabilität
|
||||||
|
|
||||||
|
* **Veranstalter-Profil (Vereins-Integration):**
|
||||||
|
* Integration einer detaillierten Vereins-Vorschau (Card) im `VeranstalterDetailScreen`.
|
||||||
|
* Navigation zum Vereins-Editor direkt aus dem Veranstalter-Profil ("Bearbeiten"-Button).
|
||||||
|
* **UI-Konsistenz:**
|
||||||
|
* Einführung eines einheitlichen "Zurück"-Buttons (Pfeil-Icon) in der Header-Zeile aller Detail- und Konfigurations-Screens.
|
||||||
|
* Kompakte Darstellung von Suchergebnissen in der Vereins-Suche (inkl. Logo-Vorschau).
|
||||||
|
|
||||||
|
## 🧹 Curator Hinweis
|
||||||
|
Die gemeldeten UX-Blocker in der Geräte-Konfiguration und bei der Veranstaltungs-Neuanlage sind behoben. Der neue Date-Picker erfüllt den Wunsch nach einer komfortableren Datumsauswahl und verhindert Tippfehler im Zeitraum-Format.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Journal: Stammdaten-Management & Sidebar-Erweiterung (20. April 2026)
|
||||||
|
|
||||||
|
## 🏗️ [Lead Architect] & 🎨 [Frontend Expert] – Bericht
|
||||||
|
|
||||||
|
### 🔍 Analyse & Zielsetzung
|
||||||
|
Der User wünschte eine bessere Zugänglichkeit des ZNS-Importers sowie eine konsistente Verwaltung aller Stammdaten-Kategorien (Reiter, Pferde, Richter/Funktionäre) nach dem Vorbild der Vereins-Verwaltung. Zudem wurde eine höhere Informationsdichte (kompakte Felder) gefordert.
|
||||||
|
|
||||||
|
### 🛠️ Umgesetzte Änderungen
|
||||||
|
|
||||||
|
#### 1. Sidebar (NavigationRail)
|
||||||
|
- **ZNS-Import:** Ein dediziertes Icon (`CloudDownload`) wurde in der Sidebar platziert, um den Import-Prozess jederzeit schnell erreichbar zu machen.
|
||||||
|
- **Stammdaten-Dropdown:** Ein neues Gruppen-Icon (`Storage`) bündelt nun die Kategorien:
|
||||||
|
- Vereine (`People`)
|
||||||
|
- Reiter (`Person`)
|
||||||
|
- Pferde (`Pets`)
|
||||||
|
- Richter/Funktionäre (`Gavel`)
|
||||||
|
- **Implementierung:** Nutzung von `DropdownMenu` und `DpOffset` für eine saubere Platzierung neben der Rail.
|
||||||
|
|
||||||
|
#### 2. Stammdaten-Screens (Pferde, Reiter, Funktionäre)
|
||||||
|
- **Konsistentes Pattern:** Alle drei Kategorien wurden auf das `MsMasterDetailLayout` umgestellt.
|
||||||
|
- **Links (Master):** Kompakte Liste mit Suche (`MsFilterBar`) und Datentabelle (`MsDataTable`).
|
||||||
|
- **Rechts (Detail):** Eine "Card-Vorschau" (ähnlich der Vereins-Card) zeigt die wichtigsten Daten auf einen Blick. Der Editor öffnet sich per Klick auf "Bearbeiten".
|
||||||
|
- **Kompakte UI:** Alle `MsTextField`-Komponenten in diesen Screens wurden auf `compact = true` umgestellt, um die geforderte Informationsdichte zu erreichen.
|
||||||
|
- **Funktionäre (Richter):** Ein neues, leistungsfähigeres `FunktionaerViewModel` und der entsprechende Screen wurden implementiert, um auch hier das Master-Detail-Muster zu nutzen (vorher nur einfache Tabelle).
|
||||||
|
|
||||||
|
#### 3. Core-Komponenten Refinement
|
||||||
|
- **`MsButton`:** Unterstützung für Icons hinzugefügt, um "Anlegen"-Aktionen visuell zu unterstreichen.
|
||||||
|
- **`MsDataTable`:** Unterstützung für `selectedItem` Highlights eingebaut, damit der User in der Liste sofort erkennt, welcher Datensatz rechts im Detail angezeigt wird.
|
||||||
|
|
||||||
|
### 🧹 Curator Journal
|
||||||
|
* **Status:** Alle Stammdaten-Kategorien folgen nun einem einheitlichen Architektur-Muster.
|
||||||
|
* **Navigations-Stabilität:** Alias-Routen in `AppScreen` und `DesktopMainLayout` wurden konsolidiert.
|
||||||
|
* **Technischer Schuldenabbau:** Veraltete Tabellen-Screens (`ManagementScreens.kt`) wurden für Pferde, Reiter und Richter durch die neuen Feature-Screens ersetzt.
|
||||||
|
|
||||||
|
---
|
||||||
|
**Nächster Schritt:** Im nächsten Stint folgt die Integration der Web-App (Stufe 2).
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Journal-Eintrag: Vereins-Verwaltung Erweiterung (Logo & Adresse)
|
||||||
|
|
||||||
|
**Datum:** 20. April 2026
|
||||||
|
**Status:** Abgeschlossen (Bugfix & Feature-Integration)
|
||||||
|
**Beteiligte Agenten:** 🏗️ [Lead Architect], 🎨 [Frontend Expert], 🧹 [Curator], 🧐 [QA Specialist]
|
||||||
|
|
||||||
|
## 📝 Zusammenfassung
|
||||||
|
Die Vereins-Verwaltung wurde um detaillierte Adressdaten und ein funktionales Logo-Management erweitert. Ein kritischer Bug, der zum Einfrieren der App beim Datei-Import führte, wurde behoben. Logos werden nun in der Vorschau korrekt gerendert.
|
||||||
|
|
||||||
|
## 🛠️ Technische Änderungen
|
||||||
|
|
||||||
|
### 0. Bugfix: Logo-Picker UI-Freeze
|
||||||
|
* **Problem:** Der `FileDialog` (AWT) blockierte den Main-Thread, was zum Einfrieren der App führte.
|
||||||
|
* **Lösung:** Auslagerung des Dialog-Aufrufs in einen asynchronen `Dispatchers.IO` Kontext in `LogoUploadZone.jvm.kt`.
|
||||||
|
* **Stabilität:** Integration von Try-Catch Blöcken und detailliertem Logging für den Datei-Import-Prozess.
|
||||||
|
|
||||||
|
### 1. Feature: Logo-Rendering (Base64)
|
||||||
|
* **Implementation:** Einführung einer `expect/actual` Funktion `decodeBase64ToImage`.
|
||||||
|
* **JVM-Logic:** Nutzung von `org.jetbrains.skia.Image` zur Dekodierung der Base64-Bytes in eine `ImageBitmap`.
|
||||||
|
* **UI-Integration:** Die `VereinCardPreview` rendert nun das Vereinslogo direkt aus dem gespeicherten Base64-String mittels `androidx.compose.foundation.Image`.
|
||||||
|
|
||||||
|
### 2. Domain-Modell (`Verein.kt`)
|
||||||
|
* Erweiterung um Felder: `strasse`, `hausnummer`, `bundesland` (Enum).
|
||||||
|
* Neues Feld `logoBase64` für die Offline-Speicherung von optimierten Vereinslogos.
|
||||||
|
* Einführung des Enums `Bundesland` mit den 9 österreichischen Bundesländern zur Sicherstellung der Datenqualität (ÖTO-konform).
|
||||||
|
|
||||||
|
### 2. ViewModel (`VereinViewModel.kt`)
|
||||||
|
* Erweiterung des `VereinUiState` um die neuen Adress- und Logo-Felder.
|
||||||
|
* Implementierung der Change-Handler für alle neuen Felder.
|
||||||
|
* Anpassung der `onSave`- und `onAddNew`-Methoden zur Berücksichtigung der erweiterten Datenstruktur.
|
||||||
|
|
||||||
|
### 3. UI-Anpassungen (`VereinScreens.kt`)
|
||||||
|
* **Card-Preview:**
|
||||||
|
* Anzeige der vollständigen Adresse (Straße, Hausnummer, PLZ, Ort, Bundesland).
|
||||||
|
* Integration eines "Maps"-Buttons, der die Adresse direkt in Google Maps öffnet (via `LocalUriHandler`).
|
||||||
|
* Vergrößertes Logo-Display (80dp) mit modernem Design.
|
||||||
|
* **Editor:**
|
||||||
|
* Logische Gruppierung der Adressfelder (Straße/Nr. in einer Zeile, PLZ/Ort/Bundesland in der nächsten).
|
||||||
|
* Einsatz des `MsEnumDropdown` für die Bundesland-Auswahl.
|
||||||
|
* Vorbereitung einer "Logo-Upload-Zone" mit visuellem Feedback für Drag-and-Drop / FilePicker.
|
||||||
|
|
||||||
|
## 🔍 Verifikation
|
||||||
|
* [x] Bugfix: Datei-Dialog friert die UI nicht mehr ein (IO-Dispatcher).
|
||||||
|
* [x] Feature: Base64-Logo wird in der Card-Vorschau gerendert.
|
||||||
|
* [x] Feature: Logging im ViewModel und Logo-Service implementiert.
|
||||||
|
* [x] UI: Kompakte Adressfelder und Google-Maps-Link funktionieren.
|
||||||
|
|
||||||
|
## 📌 Nächste Schritte
|
||||||
|
* Implementierung einer tatsächlichen Bild-Skalierung vor dem Base64-Encoding, um Datenbank-Größe zu optimieren.
|
||||||
|
* Finalisierung der Drag-and-Drop Logik (`onExternalDrag`), sobald Bibliotheks-Support stabil ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dokumentiert durch den Curator.*
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
+30
-13
@@ -1,12 +1,12 @@
|
|||||||
package at.mocode.frontend.core.designsystem.components
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
enum class ButtonVariant {
|
enum class ButtonVariant {
|
||||||
@@ -24,6 +24,7 @@ fun MsButton(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
variant: ButtonVariant = ButtonVariant.PRIMARY,
|
variant: ButtonVariant = ButtonVariant.PRIMARY,
|
||||||
size: ButtonSize = ButtonSize.MEDIUM,
|
size: ButtonSize = ButtonSize.MEDIUM,
|
||||||
|
icon: ImageVector? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
fullWidth: Boolean = false,
|
fullWidth: Boolean = false,
|
||||||
@@ -44,34 +45,38 @@ fun MsButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading,
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding,
|
||||||
colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors()
|
colors = if (containerColor != null) ButtonDefaults.buttonColors(containerColor = containerColor) else ButtonDefaults.buttonColors()
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonVariant.SECONDARY -> FilledTonalButton(
|
ButtonVariant.SECONDARY -> FilledTonalButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading,
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding,
|
||||||
colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors()
|
colors = if (containerColor != null) ButtonDefaults.filledTonalButtonColors(containerColor = containerColor) else ButtonDefaults.filledTonalButtonColors()
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonVariant.OUTLINE -> OutlinedButton(
|
ButtonVariant.OUTLINE -> OutlinedButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.ButtonWithIconContentPadding else ButtonDefaults.ContentPadding
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonVariant.TEXT -> TextButton(
|
ButtonVariant.TEXT -> TextButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
enabled = enabled && !isLoading
|
enabled = enabled && !isLoading,
|
||||||
|
contentPadding = if (icon != null) ButtonDefaults.TextButtonWithIconContentPadding else ButtonDefaults.TextButtonContentPadding
|
||||||
) {
|
) {
|
||||||
ButtonContent(text = text, isLoading = isLoading)
|
ButtonContent(text = text, isLoading = isLoading, icon = icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,15 +84,27 @@ fun MsButton(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ButtonContent(
|
private fun ButtonContent(
|
||||||
text: String,
|
text: String,
|
||||||
isLoading: Boolean
|
isLoading: Boolean,
|
||||||
|
icon: ImageVector? = null
|
||||||
) {
|
) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.padding(2.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
strokeWidth = 2.dp
|
strokeWidth = 2.dp,
|
||||||
|
color = LocalContentColor.current
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text(text)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(ButtonDefaults.IconSize)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
|
||||||
|
}
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -57,6 +57,7 @@ fun <T> MsDataTable(
|
|||||||
items: List<T>,
|
items: List<T>,
|
||||||
columns: List<MsColumnDefinition<T>>,
|
columns: List<MsColumnDefinition<T>>,
|
||||||
onRowClick: ((T) -> Unit)? = null,
|
onRowClick: ((T) -> Unit)? = null,
|
||||||
|
selectedItem: T? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
headerBackgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
rowBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
@@ -100,7 +101,12 @@ fun <T> MsDataTable(
|
|||||||
val state = androidx.compose.foundation.lazy.rememberLazyListState()
|
val state = androidx.compose.foundation.lazy.rememberLazyListState()
|
||||||
LazyColumn(state = state, modifier = Modifier.fillMaxSize()) {
|
LazyColumn(state = state, modifier = Modifier.fillMaxSize()) {
|
||||||
itemsIndexed(items) { index, item ->
|
itemsIndexed(items) { index, item ->
|
||||||
val bgColor = if (index % 2 == 0) rowBackgroundColor else alternateRowBackgroundColor
|
val isSelected = item == selectedItem
|
||||||
|
val bgColor = when {
|
||||||
|
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
index % 2 == 0 -> rowBackgroundColor
|
||||||
|
else -> alternateRowBackgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = bgColor,
|
color = bgColor,
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.Month
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlin.time.Clock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einfacher DatePicker-Dialog für Compose Desktop.
|
||||||
|
* Da Material3 DatePicker unter Desktop teils Probleme macht oder zu groß ist,
|
||||||
|
* nutzen wir hier eine kompakte Eigenimplementierung.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MsDatePickerField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isError: Boolean = false,
|
||||||
|
errorMessage: String? = null
|
||||||
|
) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
MsTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { /* Schreibgeschützt via Dialog */ },
|
||||||
|
label = label,
|
||||||
|
placeholder = "YYYY-MM-DD",
|
||||||
|
readOnly = true,
|
||||||
|
isError = isError,
|
||||||
|
errorMessage = errorMessage,
|
||||||
|
trailingIcon = Icons.Default.CalendarMonth,
|
||||||
|
onTrailingIconClick = { showDialog = true },
|
||||||
|
modifier = Modifier.clickable { showDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
MsDatePickerDialog(
|
||||||
|
initialDate = value,
|
||||||
|
onDismiss = { showDialog = false },
|
||||||
|
onDateSelected = {
|
||||||
|
onValueChange(it)
|
||||||
|
showDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MsDatePickerDialog(
|
||||||
|
initialDate: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onDateSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||||
|
val parsedDate = try {
|
||||||
|
LocalDate.parse(initialDate)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
now
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentMonth by remember { mutableStateOf(parsedDate.month) }
|
||||||
|
var currentYear by remember { mutableStateOf(parsedDate.year) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.width(300.dp),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
// Header: Monat/Jahr Auswahl
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (currentMonth == Month.JANUARY) {
|
||||||
|
currentMonth = Month.DECEMBER
|
||||||
|
currentYear--
|
||||||
|
} else {
|
||||||
|
val months = Month.entries
|
||||||
|
currentMonth = months[currentMonth.ordinal - 1]
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("<")
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"${currentMonth.name} $currentYear",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (currentMonth == Month.DECEMBER) {
|
||||||
|
currentMonth = Month.JANUARY
|
||||||
|
currentYear++
|
||||||
|
} else {
|
||||||
|
val months = Month.entries
|
||||||
|
currentMonth = months[currentMonth.ordinal + 1]
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(">")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// Kalender-Grid
|
||||||
|
val daysInMonth = getDaysInMonth(currentMonth, currentYear)
|
||||||
|
val firstDayOfWeek = LocalDate(currentYear, currentMonth, 1).dayOfWeek.ordinal // 0=Monday
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
listOf("Mo", "Di", "Mi", "Do", "Fr", "Sa", "So").forEach {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalSlots = 42 // 6 Wochen
|
||||||
|
Column {
|
||||||
|
for (week in 0 until 6) {
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
for (day in 0 until 7) {
|
||||||
|
val slotIndex = week * 7 + day
|
||||||
|
val dayNum = slotIndex - firstDayOfWeek + 1
|
||||||
|
if (dayNum in 1..daysInMonth) {
|
||||||
|
val isSelected = parsedDate.day == dayNum &&
|
||||||
|
parsedDate.month == currentMonth &&
|
||||||
|
parsedDate.year == currentYear
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.padding(2.dp)
|
||||||
|
.background(
|
||||||
|
if (isSelected) MaterialTheme.colorScheme.primary
|
||||||
|
else Color.Transparent,
|
||||||
|
MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
val selected = LocalDate(currentYear, currentMonth, dayNum)
|
||||||
|
onDateSelected(selected.toString())
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
dayNum.toString(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
|
||||||
|
else MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.weight(1f).aspectRatio(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Abbrechen") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDaysInMonth(month: Month, year: Int): Int {
|
||||||
|
return when (month) {
|
||||||
|
Month.FEBRUARY -> if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) 29 else 28
|
||||||
|
Month.APRIL, Month.JUNE, Month.SEPTEMBER, Month.NOVEMBER -> 30
|
||||||
|
else -> 31
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentraler FilePicker für die gesamte App.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
expect fun MsFilePicker(
|
||||||
|
label: String,
|
||||||
|
selectedPath: String?,
|
||||||
|
onFileSelected: (String) -> Unit,
|
||||||
|
fileExtensions: List<String> = emptyList(),
|
||||||
|
directoryOnly: Boolean = false,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
)
|
||||||
+1
-1
@@ -51,7 +51,7 @@ fun MsFilterBar(
|
|||||||
onValueChange = onSearchQueryChange,
|
onValueChange = onSearchQueryChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(300.dp)
|
.width(300.dp)
|
||||||
.height(40.dp), // Fixe Höhe für High-Density
|
.height(44.dp), // Erhöht von 40.dp auf 44.dp, damit Text nicht abgeschnitten wird
|
||||||
placeholder = { Text(searchPlaceholder, style = MaterialTheme.typography.bodySmall) },
|
placeholder = { Text(searchPlaceholder, style = MaterialTheme.typography.bodySmall) },
|
||||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
|
||||||
trailingIcon = if (searchQuery.isNotEmpty()) {
|
trailingIcon = if (searchQuery.isNotEmpty()) {
|
||||||
|
|||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.awt.FileDialog
|
||||||
|
import java.awt.Frame
|
||||||
|
import java.io.File
|
||||||
|
import javax.swing.JFileChooser
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun MsFilePicker(
|
||||||
|
label: String,
|
||||||
|
selectedPath: String?,
|
||||||
|
onFileSelected: (String) -> Unit,
|
||||||
|
fileExtensions: List<String>,
|
||||||
|
directoryOnly: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
MsTextField(
|
||||||
|
value = selectedPath ?: "",
|
||||||
|
onValueChange = { },
|
||||||
|
readOnly = true,
|
||||||
|
label = label,
|
||||||
|
placeholder = if (directoryOnly) "Verzeichnis wählen..." else "Datei wählen...",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = enabled,
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
onClick = {
|
||||||
|
if (directoryOnly) {
|
||||||
|
// JFileChooser ist für Verzeichnisse auf dem Desktop oft stabiler/einfacher
|
||||||
|
val chooser = JFileChooser().apply {
|
||||||
|
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||||
|
dialogTitle = label
|
||||||
|
selectedPath?.let {
|
||||||
|
val currentDir = File(it)
|
||||||
|
if (currentDir.exists()) currentDirectory = currentDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val result = chooser.showOpenDialog(null)
|
||||||
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
onFileSelected(chooser.selectedFile.absolutePath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// AWT FileDialog für nativen Look bei Dateiauswahl (wie vom User gewünscht)
|
||||||
|
val dialog = FileDialog(null as Frame?, label, FileDialog.LOAD).apply {
|
||||||
|
if (fileExtensions.isNotEmpty()) {
|
||||||
|
setFilenameFilter { _, name ->
|
||||||
|
fileExtensions.any { name.lowercase().endsWith(it.lowercase()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialog.isVisible = true
|
||||||
|
if (dialog.file != null) {
|
||||||
|
onFileSelected(File(dialog.directory, dialog.file).absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = "Durchsuchen",
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package at.mocode.frontend.core.designsystem.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun MsFilePicker(
|
||||||
|
label: String,
|
||||||
|
selectedPath: String?,
|
||||||
|
onFileSelected: (String) -> Unit,
|
||||||
|
fileExtensions: List<String>,
|
||||||
|
directoryOnly: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
// WasmJs Implementierung (Platzhalter oder HTML Input Logik)
|
||||||
|
MsTextField(
|
||||||
|
value = selectedPath ?: "",
|
||||||
|
onValueChange = { },
|
||||||
|
readOnly = true,
|
||||||
|
label = label,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
+22
-127
@@ -4,11 +4,9 @@ package at.mocode.frontend.features.device.initialization.presentation
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.outlined.FolderOpen
|
|
||||||
import androidx.compose.material.icons.outlined.Visibility
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
@@ -31,11 +29,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.MsEnumDropdown
|
import at.mocode.frontend.core.designsystem.components.MsEnumDropdown
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
import at.mocode.frontend.features.device.initialization.domain.DeviceInitializationValidator
|
import at.mocode.frontend.features.device.initialization.domain.DeviceInitializationValidator
|
||||||
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
|
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
|
||||||
import java.io.File
|
|
||||||
import javax.swing.JFileChooser
|
|
||||||
import javax.swing.UIManager
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DeviceInitializationConfig(
|
actual fun DeviceInitializationConfig(
|
||||||
@@ -54,37 +51,28 @@ actual fun DeviceInitializationConfig(
|
|||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("⚙️ Geräte-Konfiguration (${settings.networkRole})", style = MaterialTheme.typography.titleMedium)
|
Text("⚙️ Geräte-Konfiguration (${settings.networkRole})", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
MsSettingsField(
|
MsTextField(
|
||||||
value = settings.deviceName,
|
value = settings.deviceName,
|
||||||
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
|
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
|
||||||
label = "Gerätename",
|
label = "Gerätename",
|
||||||
placeholder = "z.B. Meldestelle-PC-1",
|
placeholder = "z.B. Meldestelle-PC-1",
|
||||||
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
||||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
imeAction = ImeAction.Next,
|
||||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||||
modifier = Modifier.focusRequester(deviceNameFocus).onKeyEvent {
|
modifier = Modifier.focusRequester(deviceNameFocus)
|
||||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
|
||||||
focusManager.moveFocus(FocusDirection.Next)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
MsSettingsField(
|
MsTextField(
|
||||||
value = settings.sharedKey,
|
value = settings.sharedKey,
|
||||||
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
|
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
|
||||||
label = "Sicherheitsschlüssel (Sync-Key)",
|
label = "Sicherheitsschlüssel (Sync-Key)",
|
||||||
placeholder = "Mindestens 8 Zeichen",
|
placeholder = "Mindestens 8 Zeichen",
|
||||||
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
|
||||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
keyboardOptions = KeyboardOptions(
|
imeAction = if (settings.networkRole == NetworkRole.MASTER) ImeAction.Next else ImeAction.Done,
|
||||||
imeAction = if (settings.networkRole == NetworkRole.MASTER) ImeAction.Next else ImeAction.Done
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onNext = { focusManager.moveFocus(FocusDirection.Next) },
|
onNext = { focusManager.moveFocus(FocusDirection.Next) },
|
||||||
onDone = {
|
onDone = {
|
||||||
@@ -95,58 +83,20 @@ actual fun DeviceInitializationConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
modifier = Modifier.focusRequester(sharedKeyFocus).onKeyEvent {
|
modifier = Modifier.focusRequester(sharedKeyFocus),
|
||||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||||
if (settings.networkRole == NetworkRole.MASTER) {
|
onTrailingIconClick = { passwordVisible = !passwordVisible }
|
||||||
focusManager.moveFocus(FocusDirection.Next)
|
|
||||||
} else if (DeviceInitializationValidator.canContinue(settings)) {
|
|
||||||
viewModel.completeInitialization()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
|
||||||
contentDescription = if (passwordVisible) "Verbergen" else "Anzeigen"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (settings.networkRole == NetworkRole.MASTER) {
|
if (settings.networkRole == NetworkRole.MASTER) {
|
||||||
OutlinedTextField(
|
MsFilePicker(
|
||||||
value = settings.backupPath,
|
label = "Backup-Verzeichnis (Pfad)",
|
||||||
onValueChange = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
|
selectedPath = settings.backupPath,
|
||||||
label = { Text("Backup-Verzeichnis (Pfad)") },
|
onFileSelected = { selectedPath ->
|
||||||
placeholder = { Text("/pfad/zu/den/backups") },
|
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
|
||||||
modifier = Modifier.fillMaxWidth().focusRequester(backupPathFocus).onKeyEvent {
|
|
||||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
|
||||||
selectBackupPath(settings.backupPath) { selectedPath ->
|
|
||||||
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onNext = { focusManager.moveFocus(FocusDirection.Next) }
|
|
||||||
),
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = {
|
|
||||||
selectBackupPath(settings.backupPath) { selectedPath ->
|
|
||||||
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Icon(Icons.Outlined.FolderOpen, contentDescription = "Verzeichnis wählen")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
isError = settings.backupPath.isNotEmpty() && !DeviceInitializationValidator.isBackupPathValid(settings.backupPath)
|
directoryOnly = true,
|
||||||
|
modifier = Modifier.focusRequester(backupPathFocus)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
||||||
@@ -320,12 +270,12 @@ private fun ClientEntryRow(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = onNameChange,
|
onValueChange = onNameChange,
|
||||||
label = { Text("Gerätename des Clients") },
|
label = "Gerätename des Clients",
|
||||||
modifier = Modifier.weight(1f).focusRequester(clientNameFocus),
|
modifier = Modifier.weight(1f).focusRequester(clientNameFocus),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
imeAction = ImeAction.Next,
|
||||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -345,58 +295,3 @@ private fun ClientEntryRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MsSettingsField(
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
label: String,
|
|
||||||
placeholder: String,
|
|
||||||
isError: Boolean,
|
|
||||||
errorText: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
|
||||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
|
||||||
trailingIcon: @Composable (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
label = { Text(label) },
|
|
||||||
placeholder = { Text(placeholder) },
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
isError = isError,
|
|
||||||
visualTransformation = visualTransformation,
|
|
||||||
keyboardOptions = keyboardOptions,
|
|
||||||
keyboardActions = keyboardActions,
|
|
||||||
trailingIcon = trailingIcon,
|
|
||||||
supportingText = {
|
|
||||||
if (isError) {
|
|
||||||
Text(errorText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectBackupPath(currentPath: String, onPathSelected: (String) -> Unit) {
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
|
||||||
val chooser = JFileChooser().apply {
|
|
||||||
fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
|
||||||
dialogTitle = "Backup-Verzeichnis wählen"
|
|
||||||
if (currentPath.isNotEmpty()) {
|
|
||||||
val currentDir = File(currentPath)
|
|
||||||
if (currentDir.exists()) currentDirectory = currentDir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val result = chooser.showOpenDialog(null)
|
|
||||||
if (result == JFileChooser.APPROVE_OPTION) {
|
|
||||||
val selectedPath = chooser.selectedFile.absolutePath
|
|
||||||
onPathSelected(selectedPath)
|
|
||||||
println("[DeviceInit] Backup-Verzeichnis gewählt: $selectedPath")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[DeviceInit] [Error] Fehler beim Öffnen des Verzeichnis-Wählers: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-4
@@ -1,5 +1,6 @@
|
|||||||
package at.mocode.frontend.features.funktionaer.di
|
package at.mocode.frontend.features.funktionaer.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
import at.mocode.frontend.features.funktionaer.presentation.*
|
import at.mocode.frontend.features.funktionaer.presentation.*
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
@@ -9,9 +10,9 @@ val funktionaerModule = module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MockFunktionaerRepository : FunktionaerRepository {
|
class MockFunktionaerRepository : FunktionaerRepository {
|
||||||
override suspend fun list(): List<FunktionaerListItem> = listOf(
|
override suspend fun list(): List<Funktionaer> = listOf(
|
||||||
FunktionaerListItem(1, "Wolfgang Schier", "RICHTER", "G3"),
|
Funktionaer(1, "Wolfgang", "Schier", "12345", listOf("RICHTER"), "G3"),
|
||||||
FunktionaerListItem(2, "Alice Schwab", "RICHTER", "INTERNATIONAL"),
|
Funktionaer(2, "Alice", "Schwab", "23456", listOf("RICHTER"), "INTERNATIONAL"),
|
||||||
FunktionaerListItem(3, "Dietmar Gstöttner", "PARCOURSBAUER", null)
|
Funktionaer(3, "Dietmar", "Gstöttner", "34567", listOf("PARCOURSBAUER"), null)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+209
-33
@@ -1,17 +1,20 @@
|
|||||||
package at.mocode.frontend.features.funktionaer.presentation
|
package at.mocode.frontend.features.funktionaer.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.Gavel
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FunktionaerScreen(
|
fun FunktionaerScreen(
|
||||||
@@ -24,19 +27,31 @@ fun FunktionaerScreen(
|
|||||||
FunktionaerListContent(
|
FunktionaerListContent(
|
||||||
state = state,
|
state = state,
|
||||||
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
|
onSearchChange = { viewModel.send(FunktionaerIntent.SearchChanged(it)) },
|
||||||
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) }
|
onFunktionaerSelected = { viewModel.send(FunktionaerIntent.Select(it)) },
|
||||||
|
onAddNew = { viewModel.send(FunktionaerIntent.AddNew) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
if (state.selectedId != null) {
|
if (state.isEditing) {
|
||||||
val selected = state.list.find { it.id == state.selectedId }
|
FunktionaerEditorContent(
|
||||||
if (selected != null) {
|
state = state,
|
||||||
FunktionaerDetailContent(selected)
|
onVornameChange = { viewModel.send(FunktionaerIntent.EditVorname(it)) },
|
||||||
}
|
onNachnameChange = { viewModel.send(FunktionaerIntent.EditNachname(it)) },
|
||||||
|
onRichterNummerChange = { viewModel.send(FunktionaerIntent.EditRichterNummer(it)) },
|
||||||
|
onEmailChange = { viewModel.send(FunktionaerIntent.EditEmail(it)) },
|
||||||
|
onTelefonChange = { viewModel.send(FunktionaerIntent.EditTelefon(it)) },
|
||||||
|
onSave = { viewModel.send(FunktionaerIntent.Save) },
|
||||||
|
onCancel = { viewModel.send(FunktionaerIntent.Cancel) }
|
||||||
|
)
|
||||||
|
} else if (state.selectedFunktionaer != null) {
|
||||||
|
FunktionaerCard(
|
||||||
|
funktionaer = state.selectedFunktionaer!!,
|
||||||
|
onEdit = { viewModel.send(FunktionaerIntent.Select(state.selectedFunktionaer)) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaceholderContent(
|
PlaceholderContent(
|
||||||
title = "Kein Funktionär ausgewählt",
|
title = "Kein Funktionär ausgewählt",
|
||||||
subtitle = "Wählen Sie einen Funktionär aus der Liste aus."
|
subtitle = "Wählen Sie einen Richter oder Funktionär aus der Liste aus."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +62,21 @@ fun FunktionaerScreen(
|
|||||||
private fun FunktionaerListContent(
|
private fun FunktionaerListContent(
|
||||||
state: FunktionaerState,
|
state: FunktionaerState,
|
||||||
onSearchChange: (String) -> Unit,
|
onSearchChange: (String) -> Unit,
|
||||||
onFunktionaerSelected: (Long) -> Unit
|
onFunktionaerSelected: (Funktionaer) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
MsFilterBar(
|
MsFilterBar(
|
||||||
searchQuery = state.searchQuery,
|
searchQuery = state.searchQuery,
|
||||||
onSearchQueryChange = onSearchChange,
|
onSearchQueryChange = onSearchChange,
|
||||||
resultCount = state.filtered.size
|
resultCount = state.filtered.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
text = "Funktionär anlegen",
|
||||||
|
onClick = onAddNew,
|
||||||
|
icon = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -68,36 +91,189 @@ private fun FunktionaerListContent(
|
|||||||
columns = listOf(
|
columns = listOf(
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Name",
|
title = "Name",
|
||||||
|
weight = 1.5f,
|
||||||
|
cellRenderer = { Text("${it.vorname} ${it.nachname}", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Nr.",
|
||||||
|
width = 80.dp,
|
||||||
|
cellRenderer = { Text(it.richterNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||||
|
),
|
||||||
|
MsColumnDefinition(
|
||||||
|
title = "Rollen",
|
||||||
weight = 1f,
|
weight = 1f,
|
||||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.rollen.joinToString(", "), style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
|
||||||
MsColumnDefinition(
|
|
||||||
title = "Rolle",
|
|
||||||
width = 150.dp,
|
|
||||||
cellRenderer = { Text(it.rolle, style = MaterialTheme.typography.bodySmall) }
|
|
||||||
),
|
|
||||||
MsColumnDefinition(
|
|
||||||
title = "Lizenz",
|
|
||||||
width = 100.dp,
|
|
||||||
cellRenderer = { Text(it.lizenz ?: "-", style = MaterialTheme.typography.bodySmall) }
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onRowClick = { onFunktionaerSelected(it.id) }
|
onRowClick = onFunktionaerSelected,
|
||||||
|
selectedItem = state.selectedFunktionaer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FunktionaerDetailContent(item: FunktionaerListItem) {
|
fun FunktionaerCard(
|
||||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
funktionaer: Funktionaer,
|
||||||
Text(item.name, style = MaterialTheme.typography.headlineMedium)
|
onEdit: () -> Unit
|
||||||
Spacer(Modifier.height(8.dp))
|
) {
|
||||||
Text("Rolle: ${item.rolle}", style = MaterialTheme.typography.bodyLarge)
|
Column(
|
||||||
item.lizenz?.let {
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
Text("Lizenz: $it", style = MaterialTheme.typography.bodyLarge)
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Gavel,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
"${funktionaer.vorname} ${funktionaer.nachname}",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Richter-Nr: ${funktionaer.richterNummer ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsStatusBadge(
|
||||||
|
text = if (funktionaer.istAktiv) "Aktiv" else "Inaktiv",
|
||||||
|
containerColor = (if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error).copy(alpha = 0.1f),
|
||||||
|
contentColor = if (funktionaer.istAktiv) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
FunktionaerDetailItem(label = "Rollen", value = funktionaer.rollen.joinToString(", "), modifier = Modifier.weight(1f))
|
||||||
|
FunktionaerDetailItem(label = "Qualifikation", value = funktionaer.richterQualifikation ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
FunktionaerDetailItem(label = "E-Mail", value = funktionaer.email ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
FunktionaerDetailItem(label = "Telefon", value = funktionaer.telefon ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
text = "Daten bearbeiten",
|
||||||
|
onClick = onEdit,
|
||||||
|
fullWidth = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(24.dp))
|
}
|
||||||
Text("Weitere Details folgen in der nächsten Ausbaustufe.", style = MaterialTheme.typography.bodyMedium)
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FunktionaerDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FunktionaerEditorContent(
|
||||||
|
state: FunktionaerState,
|
||||||
|
onVornameChange: (String) -> Unit,
|
||||||
|
onNachnameChange: (String) -> Unit,
|
||||||
|
onRichterNummerChange: (String) -> Unit,
|
||||||
|
onEmailChange: (String) -> Unit,
|
||||||
|
onTelefonChange: (String) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
MsActionToolbar(
|
||||||
|
title = "Funktionär Details",
|
||||||
|
onSave = onSave,
|
||||||
|
onCancel = onCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = state.editVorname,
|
||||||
|
onValueChange = onVornameChange,
|
||||||
|
label = "Vorname",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = state.editNachname,
|
||||||
|
onValueChange = onNachnameChange,
|
||||||
|
label = "Nachname",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
MsTextField(
|
||||||
|
value = state.editRichterNummer,
|
||||||
|
onValueChange = onRichterNummerChange,
|
||||||
|
label = "Richter-Nummer",
|
||||||
|
modifier = Modifier.width(300.dp),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = state.editEmail,
|
||||||
|
onValueChange = onEmailChange,
|
||||||
|
label = "E-Mail",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = state.editTelefon,
|
||||||
|
onValueChange = onTelefonChange,
|
||||||
|
label = "Telefon",
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text("Zusätzliche Qualifikationen und Rollen werden über das ZNS-System synchronisiert.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-17
@@ -1,8 +1,8 @@
|
|||||||
package at.mocode.frontend.features.funktionaer.presentation
|
package at.mocode.frontend.features.funktionaer.presentation
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import at.mocode.frontend.features.funktionaer.domain.Funktionaer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -17,9 +17,15 @@ data class FunktionaerListItem(
|
|||||||
data class FunktionaerState(
|
data class FunktionaerState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val list: List<FunktionaerListItem> = emptyList(),
|
val list: List<Funktionaer> = emptyList(),
|
||||||
val filtered: List<FunktionaerListItem> = emptyList(),
|
val filtered: List<Funktionaer> = emptyList(),
|
||||||
val selectedId: Long? = null,
|
val selectedFunktionaer: Funktionaer? = null,
|
||||||
|
val isEditing: Boolean = false,
|
||||||
|
val editVorname: String = "",
|
||||||
|
val editNachname: String = "",
|
||||||
|
val editRichterNummer: String = "",
|
||||||
|
val editEmail: String = "",
|
||||||
|
val editTelefon: String = "",
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,19 +33,25 @@ sealed interface FunktionaerIntent {
|
|||||||
data object Load : FunktionaerIntent
|
data object Load : FunktionaerIntent
|
||||||
data object Refresh : FunktionaerIntent
|
data object Refresh : FunktionaerIntent
|
||||||
data class SearchChanged(val query: String) : FunktionaerIntent
|
data class SearchChanged(val query: String) : FunktionaerIntent
|
||||||
data class Select(val id: Long?) : FunktionaerIntent
|
data class Select(val funktionaer: Funktionaer?) : FunktionaerIntent
|
||||||
|
data object AddNew : FunktionaerIntent
|
||||||
|
data class EditVorname(val value: String) : FunktionaerIntent
|
||||||
|
data class EditNachname(val value: String) : FunktionaerIntent
|
||||||
|
data class EditRichterNummer(val value: String) : FunktionaerIntent
|
||||||
|
data class EditEmail(val value: String) : FunktionaerIntent
|
||||||
|
data class EditTelefon(val value: String) : FunktionaerIntent
|
||||||
|
data object Save : FunktionaerIntent
|
||||||
|
data object Cancel : FunktionaerIntent
|
||||||
data object ClearError : FunktionaerIntent
|
data object ClearError : FunktionaerIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FunktionaerRepository {
|
interface FunktionaerRepository {
|
||||||
suspend fun list(): List<FunktionaerListItem>
|
suspend fun list(): List<Funktionaer>
|
||||||
}
|
}
|
||||||
|
|
||||||
class FunktionaerViewModel(
|
class FunktionaerViewModel(
|
||||||
private val repo: FunktionaerRepository,
|
private val repo: FunktionaerRepository,
|
||||||
) {
|
) : ViewModel() {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
||||||
|
|
||||||
private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
|
private val _state = MutableStateFlow(FunktionaerState(isLoading = true))
|
||||||
val state: StateFlow<FunktionaerState> = _state
|
val state: StateFlow<FunktionaerState> = _state
|
||||||
|
|
||||||
@@ -49,14 +61,44 @@ class FunktionaerViewModel(
|
|||||||
when (intent) {
|
when (intent) {
|
||||||
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
|
is FunktionaerIntent.Load, is FunktionaerIntent.Refresh -> load()
|
||||||
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
is FunktionaerIntent.SearchChanged -> reduce { it.copy(searchQuery = intent.query) }.also { filter() }
|
||||||
is FunktionaerIntent.Select -> reduce { it.copy(selectedId = intent.id) }
|
is FunktionaerIntent.Select -> reduce {
|
||||||
|
it.copy(
|
||||||
|
selectedFunktionaer = intent.funktionaer,
|
||||||
|
isEditing = intent.funktionaer != null,
|
||||||
|
editVorname = intent.funktionaer?.vorname ?: "",
|
||||||
|
editNachname = intent.funktionaer?.nachname ?: "",
|
||||||
|
editRichterNummer = intent.funktionaer?.richterNummer ?: "",
|
||||||
|
editEmail = intent.funktionaer?.email ?: "",
|
||||||
|
editTelefon = intent.funktionaer?.telefon ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FunktionaerIntent.AddNew -> reduce {
|
||||||
|
it.copy(
|
||||||
|
selectedFunktionaer = null,
|
||||||
|
isEditing = true,
|
||||||
|
editVorname = "",
|
||||||
|
editNachname = "",
|
||||||
|
editRichterNummer = "",
|
||||||
|
editEmail = "",
|
||||||
|
editTelefon = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FunktionaerIntent.EditVorname -> reduce { it.copy(editVorname = intent.value) }
|
||||||
|
is FunktionaerIntent.EditNachname -> reduce { it.copy(editNachname = intent.value) }
|
||||||
|
is FunktionaerIntent.EditRichterNummer -> reduce { it.copy(editRichterNummer = intent.value) }
|
||||||
|
is FunktionaerIntent.EditEmail -> reduce { it.copy(editEmail = intent.value) }
|
||||||
|
is FunktionaerIntent.EditTelefon -> reduce { it.copy(editTelefon = intent.value) }
|
||||||
|
is FunktionaerIntent.Save -> reduce { it.copy(isEditing = false) }
|
||||||
|
is FunktionaerIntent.Cancel -> reduce { it.copy(isEditing = false) }
|
||||||
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
is FunktionaerIntent.ClearError -> reduce { it.copy(errorMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
reduce { it.copy(isLoading = true, errorMessage = null) }
|
reduce { it.copy(isLoading = true, errorMessage = null) }
|
||||||
scope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val items = repo.list()
|
val items = repo.list()
|
||||||
reduce { cur ->
|
reduce { cur ->
|
||||||
@@ -75,13 +117,13 @@ class FunktionaerViewModel(
|
|||||||
reduce { it.copy(filtered = filtered) }
|
reduce { it.copy(filtered = filtered) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filterList(list: List<FunktionaerListItem>, query: String): List<FunktionaerListItem> {
|
private fun filterList(list: List<Funktionaer>, query: String): List<Funktionaer> {
|
||||||
if (query.isBlank()) return list
|
if (query.isBlank()) return list
|
||||||
val q = query.trim()
|
val q = query.trim()
|
||||||
return list.filter {
|
return list.filter {
|
||||||
it.name.contains(q, ignoreCase = true) ||
|
it.vorname.contains(q, ignoreCase = true) ||
|
||||||
it.rolle.contains(q, ignoreCase = true) ||
|
it.nachname.contains(q, ignoreCase = true) ||
|
||||||
(it.lizenz?.contains(q, ignoreCase = true) ?: false)
|
(it.richterNummer?.contains(q, ignoreCase = true) ?: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -50,10 +50,26 @@ class NennungRemoteRepository(private val client: HttpClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun sendeAntwort(email: String, turnierNr: String, vorname: String, nachname: String): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
client.post("$mailServiceUrl/api/mail/send-reply") {
|
||||||
|
parameter("email", email)
|
||||||
|
parameter("turnierNr", turnierNr)
|
||||||
|
parameter("vorname", vorname)
|
||||||
|
parameter("nachname", nachname)
|
||||||
|
}
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun markiereAlsGelesen(id: String): Result<Unit> {
|
suspend fun markiereAlsGelesen(id: String): Result<Unit> {
|
||||||
return try {
|
return try {
|
||||||
// Endpunkt müsste im Backend noch implementiert werden, falls gewünscht.
|
client.put("$mailServiceUrl/api/mail/nennungen/$id/status") {
|
||||||
// Für jetzt simuliert:
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody("GELESEN")
|
||||||
|
}
|
||||||
Result.success(Unit)
|
Result.success(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
|
|||||||
+144
-17
@@ -1,11 +1,14 @@
|
|||||||
package at.mocode.frontend.features.pferde.presentation
|
package at.mocode.frontend.features.pferde.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.Pets
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
@@ -24,7 +27,8 @@ fun PferdeScreen(
|
|||||||
PferdeListContent(
|
PferdeListContent(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onSearchChange = viewModel::onSearchQueryChange,
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
onPferdSelected = viewModel::selectPferd
|
onPferdSelected = viewModel::selectPferd,
|
||||||
|
onAddNew = { viewModel.addNewPferd() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
@@ -43,6 +47,11 @@ fun PferdeScreen(
|
|||||||
onSave = viewModel::onSave,
|
onSave = viewModel::onSave,
|
||||||
onCancel = viewModel::onCancel
|
onCancel = viewModel::onCancel
|
||||||
)
|
)
|
||||||
|
} else if (uiState.selectedPferd != null) {
|
||||||
|
PferdCard(
|
||||||
|
pferd = uiState.selectedPferd,
|
||||||
|
onEdit = { viewModel.selectPferd(uiState.selectedPferd) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaceholderContent(
|
PlaceholderContent(
|
||||||
title = "Kein Pferd ausgewählt",
|
title = "Kein Pferd ausgewählt",
|
||||||
@@ -57,13 +66,21 @@ fun PferdeScreen(
|
|||||||
private fun PferdeListContent(
|
private fun PferdeListContent(
|
||||||
uiState: PferdeUiState,
|
uiState: PferdeUiState,
|
||||||
onSearchChange: (String) -> Unit,
|
onSearchChange: (String) -> Unit,
|
||||||
onPferdSelected: (Pferd) -> Unit
|
onPferdSelected: (Pferd) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
MsFilterBar(
|
MsFilterBar(
|
||||||
searchQuery = uiState.searchQuery,
|
searchQuery = uiState.searchQuery,
|
||||||
onSearchQueryChange = onSearchChange,
|
onSearchQueryChange = onSearchChange,
|
||||||
resultCount = uiState.searchResults.size
|
resultCount = uiState.searchResults.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
onClick = onAddNew,
|
||||||
|
text = "Pferd anlegen",
|
||||||
|
icon = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -77,9 +94,9 @@ private fun PferdeListContent(
|
|||||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Lebensnummer",
|
title = "ÖPS-Nr.",
|
||||||
width = 150.dp,
|
width = 100.dp,
|
||||||
cellRenderer = { Text(it.lebensnummer, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.oepsNummer ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Status",
|
title = "Status",
|
||||||
@@ -93,11 +110,114 @@ private fun PferdeListContent(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onRowClick = onPferdSelected
|
onRowClick = onPferdSelected,
|
||||||
|
selectedItem = uiState.selectedPferd
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PferdCard(
|
||||||
|
pferd: Pferd,
|
||||||
|
onEdit: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Pets,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
pferd.name,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
pferd.lebensnummer,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsStatusBadge(
|
||||||
|
text = pferd.status.label,
|
||||||
|
containerColor = pferd.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = pferd.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DetailItem(label = "ÖPS-Nr.", value = pferd.oepsNummer ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
DetailItem(label = "FEI-ID", value = pferd.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DetailItem(label = "Geschlecht", value = pferd.geschlecht.label, modifier = Modifier.weight(1f))
|
||||||
|
DetailItem(label = "Farbe", value = pferd.farbe, modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
DetailItem(label = "Geburtsjahr", value = pferd.geburtsjahr?.toString() ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
DetailItem(label = "Besitzer", value = pferd.besitzer ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
onClick = onEdit,
|
||||||
|
text = "Pferdedaten bearbeiten",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PferdeEditorContent(
|
private fun PferdeEditorContent(
|
||||||
uiState: PferdeUiState,
|
uiState: PferdeUiState,
|
||||||
@@ -127,13 +247,15 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editName,
|
value = uiState.editName,
|
||||||
onValueChange = onNameChange,
|
onValueChange = onNameChange,
|
||||||
label = "Name",
|
label = "Name",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editLebensnummer,
|
value = uiState.editLebensnummer,
|
||||||
onValueChange = onLebensnummerChange,
|
onValueChange = onLebensnummerChange,
|
||||||
label = "Lebensnummer",
|
label = "Lebensnummer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,13 +266,15 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editFeiId,
|
value = uiState.editFeiId,
|
||||||
onValueChange = onFeiIdChange,
|
onValueChange = onFeiIdChange,
|
||||||
label = "FEI ID",
|
label = "FEI ID",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editOepsNummer,
|
value = uiState.editOepsNummer,
|
||||||
onValueChange = onOepsNummerChange,
|
onValueChange = onOepsNummerChange,
|
||||||
label = "ÖPS Nummer",
|
label = "ÖPS Nummer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +293,8 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editFarbe,
|
value = uiState.editFarbe,
|
||||||
onValueChange = onFarbeChange,
|
onValueChange = onFarbeChange,
|
||||||
label = "Farbe",
|
label = "Farbe",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +305,15 @@ private fun PferdeEditorContent(
|
|||||||
value = uiState.editGeburtsjahr,
|
value = uiState.editGeburtsjahr,
|
||||||
onValueChange = onGeburtsjahrChange,
|
onValueChange = onGeburtsjahrChange,
|
||||||
label = "Geburtsjahr",
|
label = "Geburtsjahr",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editBesitzer,
|
value = uiState.editBesitzer,
|
||||||
onValueChange = onBesitzerChange,
|
onValueChange = onBesitzerChange,
|
||||||
label = "Besitzer",
|
label = "Besitzer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
@@ -17,6 +17,7 @@ data class PferdeUiState(
|
|||||||
val selectedPferd: Pferd? = null,
|
val selectedPferd: Pferd? = null,
|
||||||
val isEditing: Boolean = false,
|
val isEditing: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val editId: String = "",
|
||||||
val editName: String = "",
|
val editName: String = "",
|
||||||
val editLebensnummer: String = "",
|
val editLebensnummer: String = "",
|
||||||
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
val editGeschlecht: Geschlecht = Geschlecht.WALLACH,
|
||||||
@@ -59,6 +60,7 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
uiState = uiState.copy(
|
uiState = uiState.copy(
|
||||||
selectedPferd = pferd,
|
selectedPferd = pferd,
|
||||||
isEditing = true,
|
isEditing = true,
|
||||||
|
editId = pferd.id,
|
||||||
editName = pferd.name,
|
editName = pferd.name,
|
||||||
editLebensnummer = pferd.lebensnummer,
|
editLebensnummer = pferd.lebensnummer,
|
||||||
editGeschlecht = pferd.geschlecht,
|
editGeschlecht = pferd.geschlecht,
|
||||||
@@ -71,6 +73,23 @@ open class PferdeViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addNewPferd() {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedPferd = null,
|
||||||
|
isEditing = true,
|
||||||
|
editId = "",
|
||||||
|
editName = "",
|
||||||
|
editLebensnummer = "",
|
||||||
|
editGeschlecht = Geschlecht.WALLACH,
|
||||||
|
editFarbe = "",
|
||||||
|
editGeburtsjahr = "",
|
||||||
|
editStatus = PferdeStatus.AKTIV,
|
||||||
|
editFeiId = "",
|
||||||
|
editOepsNummer = "",
|
||||||
|
editBesitzer = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onEditFeiIdChange(value: String) {
|
fun onEditFeiIdChange(value: String) {
|
||||||
uiState = uiState.copy(editFeiId = value)
|
uiState = uiState.copy(editFeiId = value)
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-21
@@ -1,9 +1,9 @@
|
|||||||
package at.mocode.frontend.features.reiter.presentation
|
package at.mocode.frontend.features.reiter.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
@@ -23,7 +23,8 @@ fun ReiterScreen(
|
|||||||
ReiterListContent(
|
ReiterListContent(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onSearchChange = viewModel::onSearchQueryChange,
|
onSearchChange = viewModel::onSearchQueryChange,
|
||||||
onReiterSelected = viewModel::selectReiter
|
onReiterSelected = viewModel::selectReiter,
|
||||||
|
onAddNew = { viewModel.addNewReiter() }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
@@ -43,6 +44,11 @@ fun ReiterScreen(
|
|||||||
onSave = viewModel::onSave,
|
onSave = viewModel::onSave,
|
||||||
onCancel = viewModel::onCancel
|
onCancel = viewModel::onCancel
|
||||||
)
|
)
|
||||||
|
} else if (uiState.selectedReiter != null) {
|
||||||
|
ReiterCard(
|
||||||
|
reiter = uiState.selectedReiter,
|
||||||
|
onEdit = { viewModel.selectReiter(uiState.selectedReiter) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
PlaceholderContent(
|
PlaceholderContent(
|
||||||
title = "Kein Reiter ausgewählt",
|
title = "Kein Reiter ausgewählt",
|
||||||
@@ -57,13 +63,20 @@ fun ReiterScreen(
|
|||||||
private fun ReiterListContent(
|
private fun ReiterListContent(
|
||||||
uiState: ReiterUiState,
|
uiState: ReiterUiState,
|
||||||
onSearchChange: (String) -> Unit,
|
onSearchChange: (String) -> Unit,
|
||||||
onReiterSelected: (Reiter) -> Unit
|
onReiterSelected: (Reiter) -> Unit,
|
||||||
|
onAddNew: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
MsFilterBar(
|
MsFilterBar(
|
||||||
searchQuery = uiState.searchQuery,
|
searchQuery = uiState.searchQuery,
|
||||||
onSearchQueryChange = onSearchChange,
|
onSearchQueryChange = onSearchChange,
|
||||||
resultCount = uiState.searchResults.size
|
resultCount = uiState.searchResults.size,
|
||||||
|
actions = {
|
||||||
|
MsButton(
|
||||||
|
text = "Reiter anlegen",
|
||||||
|
onClick = onAddNew
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -72,14 +85,9 @@ private fun ReiterListContent(
|
|||||||
items = uiState.searchResults,
|
items = uiState.searchResults,
|
||||||
columns = listOf(
|
columns = listOf(
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Vorname",
|
title = "Name",
|
||||||
weight = 1f,
|
weight = 1.5f,
|
||||||
cellRenderer = { Text(it.vorname, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||||
),
|
|
||||||
MsColumnDefinition(
|
|
||||||
title = "Nachname",
|
|
||||||
weight = 1f,
|
|
||||||
cellRenderer = { Text(it.nachname, style = MaterialTheme.typography.bodySmall) }
|
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Lizenz",
|
title = "Lizenz",
|
||||||
@@ -103,6 +111,107 @@ private fun ReiterListContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReiterCard(
|
||||||
|
reiter: Reiter,
|
||||||
|
onEdit: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Top,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Text(
|
||||||
|
text = (reiter.vorname.take(1) + reiter.nachname.take(1)).uppercase(),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
reiter.name,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"ÖPS-Nr: ${reiter.oepsNummer ?: "-"}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MsStatusBadge(
|
||||||
|
text = reiter.status.label,
|
||||||
|
containerColor = reiter.status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = reiter.status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ReiterDetailItem(label = "Lizenz", value = reiter.lizenz.label, modifier = Modifier.weight(1f))
|
||||||
|
ReiterDetailItem(label = "Hauptsparte", value = reiter.sparte.label, modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ReiterDetailItem(label = "E-Mail", value = reiter.email ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
ReiterDetailItem(label = "Telefon", value = reiter.telefon ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ReiterDetailItem(label = "Verein", value = reiter.verein ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
ReiterDetailItem(label = "FEI-ID", value = reiter.feiId ?: "-", modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
|
MsButton(
|
||||||
|
text = "Reiterdaten bearbeiten",
|
||||||
|
onClick = onEdit,
|
||||||
|
fullWidth = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReiterDetailItem(label: String, value: String, modifier: Modifier = Modifier) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
Text(value, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReiterEditorContent(
|
private fun ReiterEditorContent(
|
||||||
uiState: ReiterUiState,
|
uiState: ReiterUiState,
|
||||||
@@ -133,13 +242,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editVorname,
|
value = uiState.editVorname,
|
||||||
onValueChange = onVornameChange,
|
onValueChange = onVornameChange,
|
||||||
label = "Vorname",
|
label = "Vorname",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editName,
|
value = uiState.editName,
|
||||||
onValueChange = onNachnameChange,
|
onValueChange = onNachnameChange,
|
||||||
label = "Nachname",
|
label = "Nachname",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,13 +261,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editFeiId,
|
value = uiState.editFeiId,
|
||||||
onValueChange = onFeiIdChange,
|
onValueChange = onFeiIdChange,
|
||||||
label = "FEI ID",
|
label = "FEI ID",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editOepsNummer,
|
value = uiState.editOepsNummer,
|
||||||
onValueChange = onOepsNummerChange,
|
onValueChange = onOepsNummerChange,
|
||||||
label = "ÖPS Nummer",
|
label = "ÖPS Nummer",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,13 +280,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editGeburtsdatum,
|
value = uiState.editGeburtsdatum,
|
||||||
onValueChange = onGeburtsdatumChange,
|
onValueChange = onGeburtsdatumChange,
|
||||||
label = "Geburtsdatum",
|
label = "Geburtsdatum",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editVerein,
|
value = uiState.editVerein,
|
||||||
onValueChange = onVereinChange,
|
onValueChange = onVereinChange,
|
||||||
label = "Verein",
|
label = "Verein",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,13 +299,15 @@ private fun ReiterEditorContent(
|
|||||||
value = uiState.editEmail,
|
value = uiState.editEmail,
|
||||||
onValueChange = onEmailChange,
|
onValueChange = onEmailChange,
|
||||||
label = "E-Mail",
|
label = "E-Mail",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editTelefon,
|
value = uiState.editTelefon,
|
||||||
onValueChange = onTelefonChange,
|
onValueChange = onTelefonChange,
|
||||||
label = "Telefon",
|
label = "Telefon",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
@@ -18,6 +18,7 @@ data class ReiterUiState(
|
|||||||
val selectedReiter: Reiter? = null,
|
val selectedReiter: Reiter? = null,
|
||||||
val isEditing: Boolean = false,
|
val isEditing: Boolean = false,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
|
val editId: String = "",
|
||||||
val editName: String = "",
|
val editName: String = "",
|
||||||
val editVorname: String = "",
|
val editVorname: String = "",
|
||||||
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
val editLizenz: LizenzKlasse = LizenzKlasse.KEINE,
|
||||||
@@ -65,6 +66,7 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
uiState = uiState.copy(
|
uiState = uiState.copy(
|
||||||
selectedReiter = reiter,
|
selectedReiter = reiter,
|
||||||
isEditing = true,
|
isEditing = true,
|
||||||
|
editId = reiter.id,
|
||||||
editVorname = reiter.vorname,
|
editVorname = reiter.vorname,
|
||||||
editName = reiter.nachname,
|
editName = reiter.nachname,
|
||||||
editLizenz = reiter.lizenz,
|
editLizenz = reiter.lizenz,
|
||||||
@@ -79,6 +81,25 @@ open class ReiterViewModel(initialLoad: Boolean = true) : ViewModel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addNewReiter() {
|
||||||
|
uiState = uiState.copy(
|
||||||
|
selectedReiter = null,
|
||||||
|
isEditing = true,
|
||||||
|
editId = "",
|
||||||
|
editVorname = "",
|
||||||
|
editName = "",
|
||||||
|
editLizenz = LizenzKlasse.KEINE,
|
||||||
|
editSparte = Sparte.KEINE,
|
||||||
|
editStatus = ReiterStatus.AKTIV,
|
||||||
|
editFeiId = "",
|
||||||
|
editOepsNummer = "",
|
||||||
|
editGeburtsdatum = "",
|
||||||
|
editEmail = "",
|
||||||
|
editTelefon = "",
|
||||||
|
editVerein = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
|
fun onEditFeiIdChange(value: String) { uiState = uiState.copy(editFeiId = value) }
|
||||||
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
|
fun onEditOepsNummerChange(value: String) { uiState = uiState.copy(editOepsNummer = value) }
|
||||||
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }
|
fun onEditGeburtsdatumChange(value: String) { uiState = uiState.copy(editGeburtsdatum = value) }
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ kotlin {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
|
implementation(projects.frontend.features.vereinFeature)
|
||||||
implementation(projects.frontend.core.designSystem)
|
implementation(projects.frontend.core.designSystem)
|
||||||
implementation(projects.frontend.core.network)
|
implementation(projects.frontend.core.network)
|
||||||
implementation(projects.frontend.core.domain)
|
implementation(projects.frontend.core.domain)
|
||||||
|
|||||||
+18
-8
@@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -124,17 +125,26 @@ fun VeranstalterDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// ── Header mit Zurück-Pfeil ─────────────────────────────────────────
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onZurueck) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
|
}
|
||||||
|
Text("Veranstalter-Profil", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Veranstalter-Header-Card ─────────────────────────────────────────
|
// ── Veranstalter-Header-Card ─────────────────────────────────────────
|
||||||
Surface(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
color = Color.White,
|
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||||
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
) {
|
) {
|
||||||
@@ -186,12 +196,12 @@ fun VeranstalterDetailScreen(
|
|||||||
}
|
}
|
||||||
// Profil bearbeiten
|
// Profil bearbeiten
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { /* TODO */ },
|
onClick = { /* Navigation zu Vereinen */ },
|
||||||
border = BorderStroke(1.dp, Color(0xFFD1D5DB)),
|
border = BorderStroke(1.dp, Color(0xFFD1D5DB)),
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(14.dp))
|
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Text("Profil bearbeiten", fontSize = 13.sp)
|
Text("Bearbeiten", fontSize = 13.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-60
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -13,6 +14,7 @@ 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 at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formular zum Anlegen eines neuen Veranstalters (Vision_03: Screenshot 21).
|
* Formular zum Anlegen eines neuen Veranstalters (Vision_03: Screenshot 21).
|
||||||
@@ -47,18 +49,27 @@ fun VeranstalterNeuScreen(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
// Header
|
// Header
|
||||||
Column(modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp)) {
|
Row(
|
||||||
Text(
|
modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp),
|
||||||
text = "Neuen Veranstalter anlegen",
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
fontSize = 22.sp,
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
fontWeight = FontWeight.Bold,
|
) {
|
||||||
)
|
IconButton(onClick = onAbbrechen) {
|
||||||
Spacer(Modifier.height(4.dp))
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||||
Text(
|
}
|
||||||
text = "Legen Sie einen neuen Veranstalter (Verein) mit OEPS-Daten an. Nach dem Speichern werden automatisch Login-Daten generiert.",
|
Column {
|
||||||
fontSize = 13.sp,
|
Text(
|
||||||
color = Color(0xFF6B7280),
|
text = "Neuen Veranstalter anlegen",
|
||||||
)
|
fontSize = 22.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Legen Sie einen neuen Veranstalter (Verein) mit OEPS-Daten an. Nach dem Speichern werden automatisch Login-Daten generiert.",
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = Color(0xFF6B7280),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info-Banner
|
// Info-Banner
|
||||||
@@ -110,65 +121,46 @@ fun VeranstalterNeuScreen(
|
|||||||
// --- Vereinsdaten ---
|
// --- Vereinsdaten ---
|
||||||
Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||||
|
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = vereinsname,
|
value = vereinsname,
|
||||||
onValueChange = { vereinsname = it },
|
onValueChange = { vereinsname = it },
|
||||||
label = { Text("Vereinsname *") },
|
label = "Vereinsname *",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Column {
|
MsTextField(
|
||||||
OutlinedTextField(
|
value = oepsNummer,
|
||||||
value = oepsNummer,
|
onValueChange = { oepsNummer = it },
|
||||||
onValueChange = { oepsNummer = it },
|
label = "OEPS-Nummer *",
|
||||||
label = { Text("OEPS-Nummer *") },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
helperText = "Offizielle Vereinsnummer des OEPS"
|
||||||
singleLine = true,
|
)
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Offizielle Vereinsnummer des OEPS",
|
|
||||||
fontSize = 11.sp,
|
|
||||||
color = Color(0xFF2563EB),
|
|
||||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
||||||
// --- Kontaktdaten ---
|
// --- Kontaktdaten ---
|
||||||
Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||||
|
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = ansprechpartner,
|
value = ansprechpartner,
|
||||||
onValueChange = { ansprechpartner = it },
|
onValueChange = { ansprechpartner = it },
|
||||||
label = { Text("Ansprechpartner *") },
|
label = "Ansprechpartner *",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Column {
|
MsTextField(
|
||||||
OutlinedTextField(
|
value = email,
|
||||||
value = email,
|
onValueChange = { email = it },
|
||||||
onValueChange = { email = it },
|
label = "E-Mail *",
|
||||||
label = { Text("E-Mail *") },
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
helperText = "Login-Daten werden an diese Adresse verschickt"
|
||||||
singleLine = true,
|
)
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = "Login-Daten werden an diese Adresse verschickt",
|
|
||||||
fontSize = 11.sp,
|
|
||||||
color = Color(0xFF6B7280),
|
|
||||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = telefon,
|
value = telefon,
|
||||||
onValueChange = { telefon = it },
|
onValueChange = { telefon = it },
|
||||||
label = { Text("Telefon") },
|
label = "Telefon",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
@@ -176,28 +168,25 @@ fun VeranstalterNeuScreen(
|
|||||||
// --- Adresse ---
|
// --- Adresse ---
|
||||||
Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||||
|
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = strasse,
|
value = strasse,
|
||||||
onValueChange = { strasse = it },
|
onValueChange = { strasse = it },
|
||||||
label = { Text("Straße & Hausnummer") },
|
label = "Straße & Hausnummer",
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = plz,
|
value = plz,
|
||||||
onValueChange = { plz = it },
|
onValueChange = { plz = it },
|
||||||
label = { Text("PLZ") },
|
label = "PLZ",
|
||||||
modifier = Modifier.width(120.dp),
|
modifier = Modifier.width(120.dp),
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = ort,
|
value = ort,
|
||||||
onValueChange = { ort = it },
|
onValueChange = { ort = it },
|
||||||
label = { Text("Ort") },
|
label = "Ort",
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
singleLine = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-12
@@ -2,15 +2,21 @@ package at.mocode.frontend.features.veranstalter.presentation
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
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 at.mocode.frontend.core.designsystem.components.MsDatePickerField
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formular zum Anlegen einer neuen Veranstaltung (Titel + Datumspfad). Pflichtfelder: Titel, Datum von/bis.
|
* Formular zum Anlegen einer neuen Veranstaltung (Titel + Datumspfad). Pflichtfelder: Titel, Datum von/bis.
|
||||||
@@ -26,8 +32,9 @@ fun VeranstaltungKonfigScreen(
|
|||||||
var datumVon by remember { mutableStateOf("") }
|
var datumVon by remember { mutableStateOf("") }
|
||||||
var datumBis by remember { mutableStateOf("") }
|
var datumBis by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
val datesPresent = datumVon.isNotBlank() && datumBis.isNotBlank()
|
val datesPresent = datumVon.isNotBlank() && datumBis.isNotBlank()
|
||||||
// Einfache Validierung: YYYY-MM-DD Format erzwingen wir hier nicht strikt; wenn beide gesetzt, prüfen wir lexikografisch
|
// Einfache Validierung: YYYY-MM-DD Format erzwingen wir hier nicht strikt; wenn beide gesetzt sind, prüfen wir lexikografisch
|
||||||
val dateOrderOk = !datesPresent || datumBis >= datumVon
|
val dateOrderOk = !datesPresent || datumBis >= datumVon
|
||||||
val valid = titel.isNotBlank() && datesPresent && dateOrderOk
|
val valid = titel.isNotBlank() && datesPresent && dateOrderOk
|
||||||
|
|
||||||
@@ -61,37 +68,36 @@ fun VeranstaltungKonfigScreen(
|
|||||||
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Text("Stammdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
Text("Stammdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||||
|
|
||||||
OutlinedTextField(
|
MsTextField(
|
||||||
value = titel,
|
value = titel,
|
||||||
onValueChange = { titel = it },
|
onValueChange = { titel = it },
|
||||||
label = { Text("Titel *") },
|
label = "Titel *",
|
||||||
|
placeholder = "z.B. Frühjahrsturnier 2026",
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isError = titel.isBlank(),
|
isError = titel.isBlank(),
|
||||||
|
imeAction = ImeAction.Next,
|
||||||
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) })
|
||||||
)
|
)
|
||||||
|
|
||||||
Text("Zeitraum", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
Text("Zeitraum", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
OutlinedTextField(
|
MsDatePickerField(
|
||||||
|
label = "von *",
|
||||||
value = datumVon,
|
value = datumVon,
|
||||||
onValueChange = { datumVon = it },
|
onValueChange = { datumVon = it },
|
||||||
label = { Text("von (YYYY-MM-DD) *") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
isError = datumVon.isBlank(),
|
isError = datumVon.isBlank(),
|
||||||
)
|
)
|
||||||
OutlinedTextField(
|
MsDatePickerField(
|
||||||
|
label = "bis *",
|
||||||
value = datumBis,
|
value = datumBis,
|
||||||
onValueChange = { datumBis = it },
|
onValueChange = { datumBis = it },
|
||||||
label = { Text("bis (YYYY-MM-DD) *") },
|
|
||||||
singleLine = true,
|
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
isError = datumBis.isBlank() || (datesPresent && !dateOrderOk),
|
isError = datumBis.isBlank() || (datesPresent && !dateOrderOk),
|
||||||
|
errorMessage = if (datesPresent && !dateOrderOk) "Ungültiger Zeitraum" else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (datesPresent && !dateOrderOk) {
|
|
||||||
Text("Das bis-Datum darf nicht vor dem von-Datum liegen.", color = MaterialTheme.colorScheme.error, fontSize = 12.sp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -12,10 +12,27 @@ data class Verein(
|
|||||||
val oepsNr: String? = null,
|
val oepsNr: String? = null,
|
||||||
val ort: String? = null,
|
val ort: String? = null,
|
||||||
val plz: String? = null,
|
val plz: String? = null,
|
||||||
|
val strasse: String? = null,
|
||||||
|
val hausnummer: String? = null,
|
||||||
|
val bundesland: String? = null,
|
||||||
val land: String = "AUT",
|
val land: String = "AUT",
|
||||||
val status: VereinStatus = VereinStatus.AKTIV
|
val status: VereinStatus = VereinStatus.AKTIV,
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val logoBase64: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class Bundesland(val label: String) {
|
||||||
|
BURGENLAND("Burgenland"),
|
||||||
|
KAERNTEN("Kärnten"),
|
||||||
|
NIEDEROESTERREICH("Niederösterreich"),
|
||||||
|
OBEROESTERREICH("Oberösterreich"),
|
||||||
|
SALZBURG("Salzburg"),
|
||||||
|
STEIERMARK("Steiermark"),
|
||||||
|
TIROL("Tirol"),
|
||||||
|
VORARLBERG("Vorarlberg"),
|
||||||
|
WIEN("Wien")
|
||||||
|
}
|
||||||
|
|
||||||
enum class VereinStatus(val label: String, val color: Color) {
|
enum class VereinStatus(val label: String, val color: Color) {
|
||||||
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
AKTIV("Aktiv", Color(0xFF2E7D32)),
|
||||||
RUHEND("Ruhend", Color(0xFFE65100)),
|
RUHEND("Ruhend", Color(0xFFE65100)),
|
||||||
|
|||||||
+328
-46
@@ -1,15 +1,41 @@
|
|||||||
package at.mocode.frontend.features.verein.presentation
|
package at.mocode.frontend.features.verein.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Business
|
||||||
|
import androidx.compose.material.icons.filled.Image
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import at.mocode.frontend.core.designsystem.components.*
|
import at.mocode.frontend.core.designsystem.components.*
|
||||||
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
import at.mocode.frontend.core.designsystem.models.PlaceholderContent
|
||||||
|
import at.mocode.frontend.features.verein.domain.Bundesland
|
||||||
import at.mocode.frontend.features.verein.domain.Verein
|
import at.mocode.frontend.features.verein.domain.Verein
|
||||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
expect fun decodeBase64ToImage(base64: String): ImageBitmap?
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun LogoUploadZone(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onFileSelected: (ByteArray) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VereinScreen(
|
fun VereinScreen(
|
||||||
@@ -27,28 +53,199 @@ fun VereinScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
detail = {
|
detail = {
|
||||||
if (uiState.isEditing) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
VereinEditorContent(
|
if (uiState.selectedVerein != null || uiState.isEditing) {
|
||||||
uiState = uiState,
|
// --- Preview Bereich ---
|
||||||
onNameChange = viewModel::onEditNameChange,
|
VereinCardPreview(
|
||||||
onLangnameChange = viewModel::onEditLangnameChange,
|
name = if (uiState.isEditing) uiState.editName else uiState.selectedVerein?.name ?: "",
|
||||||
onOepsNrChange = viewModel::onEditOepsNrChange,
|
langname = if (uiState.isEditing) uiState.editLangname else uiState.selectedVerein?.langname,
|
||||||
onOrtChange = viewModel::onEditOrtChange,
|
ort = if (uiState.isEditing) uiState.editOrt else uiState.selectedVerein?.ort,
|
||||||
onPlzChange = viewModel::onEditPlzChange,
|
plz = if (uiState.isEditing) uiState.editPlz else uiState.selectedVerein?.plz,
|
||||||
onStatusChange = viewModel::onEditStatusChange,
|
strasse = if (uiState.isEditing) uiState.editStrasse else uiState.selectedVerein?.strasse,
|
||||||
onSave = viewModel::onSave,
|
hausnummer = if (uiState.isEditing) uiState.editHausnummer else uiState.selectedVerein?.hausnummer,
|
||||||
onCancel = viewModel::onCancel
|
bundesland = if (uiState.isEditing) uiState.editBundesland else uiState.selectedVerein?.bundesland,
|
||||||
)
|
logoUrl = if (uiState.isEditing) uiState.editLogoUrl else uiState.selectedVerein?.logoUrl,
|
||||||
} else {
|
logoBase64 = if (uiState.isEditing) uiState.editLogoBase64 else uiState.selectedVerein?.logoBase64,
|
||||||
PlaceholderContent(
|
status = if (uiState.isEditing) uiState.editStatus else uiState.selectedVerein?.status ?: VereinStatus.AKTIV
|
||||||
title = "Kein Verein ausgewählt",
|
)
|
||||||
subtitle = "Wählen Sie einen Verein aus der Liste aus oder legen Sie einen neuen an."
|
|
||||||
)
|
Spacer(Modifier.height(16.dp))
|
||||||
|
HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
if (uiState.isEditing) {
|
||||||
|
VereinEditorContent(
|
||||||
|
uiState = uiState,
|
||||||
|
onNameChange = viewModel::onEditNameChange,
|
||||||
|
onLangnameChange = viewModel::onEditLangnameChange,
|
||||||
|
onOepsNrChange = viewModel::onEditOepsNrChange,
|
||||||
|
onOrtChange = viewModel::onEditOrtChange,
|
||||||
|
onPlzChange = viewModel::onEditPlzChange,
|
||||||
|
onStrasseChange = viewModel::onEditStrasseChange,
|
||||||
|
onHausnummerChange = viewModel::onEditHausnummerChange,
|
||||||
|
onBundeslandChange = viewModel::onEditBundeslandChange,
|
||||||
|
onStatusChange = viewModel::onEditStatusChange,
|
||||||
|
onLogoUrlChange = viewModel::onEditLogoUrlChange,
|
||||||
|
onLogoFileSelected = viewModel::onLogoFileSelected,
|
||||||
|
onSave = viewModel::onSave,
|
||||||
|
onCancel = viewModel::onCancel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PlaceholderContent(
|
||||||
|
title = "Kein Verein ausgewählt",
|
||||||
|
subtitle = "Wählen Sie einen Verein aus der Liste aus oder legen Sie einen neuen an."
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VereinCard(
|
||||||
|
verein: Verein,
|
||||||
|
onEdit: (() -> Unit)? = null,
|
||||||
|
onOpenInMaps: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
VereinCardPreview(
|
||||||
|
name = verein.name,
|
||||||
|
langname = verein.langname,
|
||||||
|
ort = verein.ort,
|
||||||
|
plz = verein.plz,
|
||||||
|
strasse = verein.strasse,
|
||||||
|
hausnummer = verein.hausnummer,
|
||||||
|
bundesland = verein.bundesland,
|
||||||
|
logoUrl = verein.logoUrl,
|
||||||
|
logoBase64 = verein.logoBase64,
|
||||||
|
status = verein.status,
|
||||||
|
onEdit = onEdit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VereinCardPreview(
|
||||||
|
name: String,
|
||||||
|
langname: String?,
|
||||||
|
ort: String?,
|
||||||
|
plz: String?,
|
||||||
|
strasse: String?,
|
||||||
|
hausnummer: String?,
|
||||||
|
bundesland: String?,
|
||||||
|
logoUrl: String?,
|
||||||
|
logoBase64: String?,
|
||||||
|
status: VereinStatus,
|
||||||
|
onEdit: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
// Logo Placeholder / Image
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (!logoBase64.isNullOrBlank()) {
|
||||||
|
val bitmap = remember(logoBase64) { decodeBase64ToImage(logoBase64) }
|
||||||
|
if (bitmap != null) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
bitmap = bitmap,
|
||||||
|
contentDescription = "Vereinslogo",
|
||||||
|
modifier = Modifier.fillMaxSize().clip(CircleShape),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Image, "Logo Fehler", modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
} else if (!logoUrl.isNullOrBlank()) {
|
||||||
|
Icon(Icons.Default.Business, "Logo URL", modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Business, null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = name.ifBlank { "Vereinsname" },
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
MsStatusBadge(
|
||||||
|
text = status.label,
|
||||||
|
containerColor = status.color.copy(alpha = 0.1f),
|
||||||
|
contentColor = status.color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!langname.isNullOrBlank()) {
|
||||||
|
Text(langname, style = MaterialTheme.typography.bodyMedium, color = Color.Gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
val adresse = buildString {
|
||||||
|
if (!strasse.isNullOrBlank()) {
|
||||||
|
append(strasse)
|
||||||
|
if (!hausnummer.isNullOrBlank()) append(" $hausnummer")
|
||||||
|
append(", ")
|
||||||
|
}
|
||||||
|
if (!plz.isNullOrBlank()) append("$plz ")
|
||||||
|
if (!ort.isNullOrBlank()) append(ort)
|
||||||
|
if (!bundesland.isNullOrBlank()) {
|
||||||
|
if (isNotEmpty() && !endsWith(", ")) append(", ")
|
||||||
|
append(bundesland)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (adresse.isNotBlank()) "📍 $adresse" else "Keine Adresse angegeben",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (adresse.isNotBlank()) {
|
||||||
|
MsButton(
|
||||||
|
text = "📍 Maps",
|
||||||
|
onClick = {
|
||||||
|
val query = adresse.replace(" ", "+")
|
||||||
|
uriHandler.openUri("https://www.google.com/maps/search/?api=1&query=$query")
|
||||||
|
},
|
||||||
|
variant = ButtonVariant.TEXT,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onEdit != null) {
|
||||||
|
MsButton(
|
||||||
|
text = "Bearbeiten",
|
||||||
|
onClick = onEdit,
|
||||||
|
variant = ButtonVariant.OUTLINE,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VereinListContent(
|
private fun VereinListContent(
|
||||||
uiState: VereinUiState,
|
uiState: VereinUiState,
|
||||||
@@ -77,14 +274,45 @@ private fun VereinListContent(
|
|||||||
items = uiState.searchResults,
|
items = uiState.searchResults,
|
||||||
columns = listOf(
|
columns = listOf(
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "Name",
|
title = "Verein",
|
||||||
weight = 1.5f,
|
weight = 2f,
|
||||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
cellRenderer = {
|
||||||
),
|
Row(
|
||||||
MsColumnDefinition(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
title = "Ort",
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
weight = 1f,
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
cellRenderer = { Text(it.ort ?: "-", style = MaterialTheme.typography.bodySmall) }
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (!it.logoBase64.isNullOrBlank()) {
|
||||||
|
val bitmap = remember(it.logoBase64) { decodeBase64ToImage(it.logoBase64) }
|
||||||
|
if (bitmap != null) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
bitmap = bitmap,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize().clip(CircleShape),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Business, null, modifier = Modifier.size(16.dp))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(Icons.Default.Business, null, modifier = Modifier.size(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
Text(it.name, style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Bold)
|
||||||
|
if (!it.ort.isNullOrBlank()) {
|
||||||
|
Text(it.ort, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
),
|
),
|
||||||
MsColumnDefinition(
|
MsColumnDefinition(
|
||||||
title = "OePS-Nr",
|
title = "OePS-Nr",
|
||||||
@@ -116,11 +344,16 @@ private fun VereinEditorContent(
|
|||||||
onOepsNrChange: (String) -> Unit,
|
onOepsNrChange: (String) -> Unit,
|
||||||
onOrtChange: (String) -> Unit,
|
onOrtChange: (String) -> Unit,
|
||||||
onPlzChange: (String) -> Unit,
|
onPlzChange: (String) -> Unit,
|
||||||
|
onStrasseChange: (String) -> Unit,
|
||||||
|
onHausnummerChange: (String) -> Unit,
|
||||||
|
onBundeslandChange: (String) -> Unit,
|
||||||
onStatusChange: (VereinStatus) -> Unit,
|
onStatusChange: (VereinStatus) -> Unit,
|
||||||
|
onLogoUrlChange: (String) -> Unit,
|
||||||
|
onLogoFileSelected: (ByteArray) -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
onCancel: () -> Unit
|
onCancel: () -> Unit
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
||||||
MsActionToolbar(
|
MsActionToolbar(
|
||||||
title = if (uiState.selectedVerein == null) "Neuer Verein" else "Verein Details",
|
title = if (uiState.selectedVerein == null) "Neuer Verein" else "Verein Details",
|
||||||
onSave = onSave,
|
onSave = onSave,
|
||||||
@@ -129,21 +362,35 @@ private fun VereinEditorContent(
|
|||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
MsTextField(
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
value = uiState.editName,
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
onValueChange = onNameChange,
|
MsTextField(
|
||||||
label = "Name (Kurz)",
|
value = uiState.editName,
|
||||||
modifier = Modifier.fillMaxWidth()
|
onValueChange = onNameChange,
|
||||||
)
|
label = "Name (Kurz)",
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editLangname,
|
value = uiState.editLangname,
|
||||||
onValueChange = onLangnameChange,
|
onValueChange = onLangnameChange,
|
||||||
label = "Vollständiger Name",
|
label = "Vollständiger Name",
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
compact = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logo Upload Sektion
|
||||||
|
LogoUploadZone(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(180.dp)
|
||||||
|
.height(110.dp),
|
||||||
|
onFileSelected = onLogoFileSelected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -152,7 +399,8 @@ private fun VereinEditorContent(
|
|||||||
value = uiState.editOepsNr,
|
value = uiState.editOepsNr,
|
||||||
onValueChange = onOepsNrChange,
|
onValueChange = onOepsNrChange,
|
||||||
label = "OePS-Nr",
|
label = "OePS-Nr",
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(0.5f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsEnumDropdown(
|
MsEnumDropdown(
|
||||||
label = "Status",
|
label = "Status",
|
||||||
@@ -160,25 +408,59 @@ private fun VereinEditorContent(
|
|||||||
selectedOption = uiState.editStatus,
|
selectedOption = uiState.editStatus,
|
||||||
onOptionSelected = onStatusChange,
|
onOptionSelected = onStatusChange,
|
||||||
optionLabel = { it.label },
|
optionLabel = { it.label },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(0.5f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text("Adresse", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editStrasse,
|
||||||
|
onValueChange = onStrasseChange,
|
||||||
|
label = "Straße",
|
||||||
|
modifier = Modifier.weight(0.7f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
MsTextField(
|
||||||
|
value = uiState.editHausnummer,
|
||||||
|
onValueChange = onHausnummerChange,
|
||||||
|
label = "Nr.",
|
||||||
|
modifier = Modifier.weight(0.3f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editPlz,
|
value = uiState.editPlz,
|
||||||
onValueChange = onPlzChange,
|
onValueChange = onPlzChange,
|
||||||
label = "PLZ",
|
label = "PLZ",
|
||||||
modifier = Modifier.weight(0.3f)
|
modifier = Modifier.weight(0.2f),
|
||||||
|
compact = true
|
||||||
)
|
)
|
||||||
MsTextField(
|
MsTextField(
|
||||||
value = uiState.editOrt,
|
value = uiState.editOrt,
|
||||||
onValueChange = onOrtChange,
|
onValueChange = onOrtChange,
|
||||||
label = "Ort",
|
label = "Ort",
|
||||||
modifier = Modifier.weight(0.7f)
|
modifier = Modifier.weight(0.4f),
|
||||||
|
compact = true
|
||||||
|
)
|
||||||
|
MsEnumDropdown(
|
||||||
|
label = "Bundesland",
|
||||||
|
options = Bundesland.entries.toTypedArray(),
|
||||||
|
selectedOption = Bundesland.entries.find { it.label == uiState.editBundesland } ?: Bundesland.WIEN,
|
||||||
|
onOptionSelected = { onBundeslandChange(it.label) },
|
||||||
|
optionLabel = { it.label },
|
||||||
|
modifier = Modifier.weight(0.4f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-4
@@ -9,6 +9,8 @@ import at.mocode.frontend.features.verein.domain.Verein
|
|||||||
import at.mocode.frontend.features.verein.domain.VereinRepository
|
import at.mocode.frontend.features.verein.domain.VereinRepository
|
||||||
import at.mocode.frontend.features.verein.domain.VereinStatus
|
import at.mocode.frontend.features.verein.domain.VereinStatus
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI-State für die Vereins-Verwaltung.
|
* UI-State für die Vereins-Verwaltung.
|
||||||
@@ -26,7 +28,12 @@ data class VereinUiState(
|
|||||||
val editOepsNr: String = "",
|
val editOepsNr: String = "",
|
||||||
val editOrt: String = "",
|
val editOrt: String = "",
|
||||||
val editPlz: String = "",
|
val editPlz: String = "",
|
||||||
val editStatus: VereinStatus = VereinStatus.AKTIV
|
val editStrasse: String = "",
|
||||||
|
val editHausnummer: String = "",
|
||||||
|
val editBundesland: String = "",
|
||||||
|
val editStatus: VereinStatus = VereinStatus.AKTIV,
|
||||||
|
val editLogoUrl: String = "",
|
||||||
|
val editLogoBase64: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,10 +106,35 @@ open class VereinViewModel(
|
|||||||
editOepsNr = verein.oepsNr ?: "",
|
editOepsNr = verein.oepsNr ?: "",
|
||||||
editOrt = verein.ort ?: "",
|
editOrt = verein.ort ?: "",
|
||||||
editPlz = verein.plz ?: "",
|
editPlz = verein.plz ?: "",
|
||||||
editStatus = verein.status
|
editStrasse = verein.strasse ?: "",
|
||||||
|
editHausnummer = verein.hausnummer ?: "",
|
||||||
|
editBundesland = verein.bundesland ?: "",
|
||||||
|
editStatus = verein.status,
|
||||||
|
editLogoUrl = verein.logoUrl ?: "",
|
||||||
|
editLogoBase64 = verein.logoBase64 ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onEditStrasseChange(value: String) {
|
||||||
|
uiState = uiState.copy(editStrasse = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditHausnummerChange(value: String) {
|
||||||
|
uiState = uiState.copy(editHausnummer = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditBundeslandChange(value: String) {
|
||||||
|
uiState = uiState.copy(editBundesland = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditLogoBase64Change(value: String) {
|
||||||
|
uiState = uiState.copy(editLogoBase64 = value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEditLogoUrlChange(value: String) {
|
||||||
|
uiState = uiState.copy(editLogoUrl = value)
|
||||||
|
}
|
||||||
|
|
||||||
fun onEditNameChange(value: String) {
|
fun onEditNameChange(value: String) {
|
||||||
uiState = uiState.copy(editName = value)
|
uiState = uiState.copy(editName = value)
|
||||||
}
|
}
|
||||||
@@ -127,6 +159,18 @@ open class VereinViewModel(
|
|||||||
uiState = uiState.copy(editStatus = value)
|
uiState = uiState.copy(editStatus = value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun onLogoFileSelected(bytes: ByteArray) {
|
||||||
|
println("[VereinViewModel] Logo Datei empfangen, konvertiere zu Base64...")
|
||||||
|
try {
|
||||||
|
val base64 = Base64.encode(bytes)
|
||||||
|
uiState = uiState.copy(editLogoBase64 = base64)
|
||||||
|
println("[VereinViewModel] Logo erfolgreich in Base64 konvertiert (Länge: ${base64.length})")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("[VereinViewModel] Fehler bei Base64 Konvertierung: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onSave() {
|
fun onSave() {
|
||||||
uiState = uiState.copy(isLoading = true, error = null)
|
uiState = uiState.copy(isLoading = true, error = null)
|
||||||
val verein = (uiState.selectedVerein ?: Verein(
|
val verein = (uiState.selectedVerein ?: Verein(
|
||||||
@@ -138,7 +182,12 @@ open class VereinViewModel(
|
|||||||
oepsNr = uiState.editOepsNr,
|
oepsNr = uiState.editOepsNr,
|
||||||
ort = uiState.editOrt,
|
ort = uiState.editOrt,
|
||||||
plz = uiState.editPlz,
|
plz = uiState.editPlz,
|
||||||
status = uiState.editStatus
|
strasse = uiState.editStrasse,
|
||||||
|
hausnummer = uiState.editHausnummer,
|
||||||
|
bundesland = uiState.editBundesland,
|
||||||
|
status = uiState.editStatus,
|
||||||
|
logoUrl = uiState.editLogoUrl.ifBlank { null },
|
||||||
|
logoBase64 = uiState.editLogoBase64.ifBlank { null }
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -169,7 +218,12 @@ open class VereinViewModel(
|
|||||||
editOepsNr = "",
|
editOepsNr = "",
|
||||||
editOrt = "",
|
editOrt = "",
|
||||||
editPlz = "",
|
editPlz = "",
|
||||||
editStatus = VereinStatus.AKTIV
|
editStrasse = "",
|
||||||
|
editHausnummer = "",
|
||||||
|
editBundesland = "",
|
||||||
|
editStatus = VereinStatus.AKTIV,
|
||||||
|
editLogoUrl = "",
|
||||||
|
editLogoBase64 = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package at.mocode.frontend.features.verein.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Image
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.components.ButtonSize
|
||||||
|
import at.mocode.frontend.core.designsystem.components.ButtonVariant
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jetbrains.skia.Image
|
||||||
|
import java.awt.FileDialog
|
||||||
|
import java.awt.Frame
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
actual fun decodeBase64ToImage(base64: String): ImageBitmap? {
|
||||||
|
return try {
|
||||||
|
val bytes = Base64.decode(base64)
|
||||||
|
Image.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
actual fun LogoUploadZone(
|
||||||
|
modifier: Modifier,
|
||||||
|
onFileSelected: (ByteArray) -> Unit
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Image,
|
||||||
|
null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Text("Logo auswählen", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
MsButton(
|
||||||
|
text = "Datei wählen",
|
||||||
|
onClick = {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
println("[LogoUpload] Öffne Datei-Dialog...")
|
||||||
|
val fileDialog = FileDialog(null as Frame?, "Logo auswählen", FileDialog.LOAD)
|
||||||
|
fileDialog.isVisible = true
|
||||||
|
val directory = fileDialog.directory
|
||||||
|
val file = fileDialog.file
|
||||||
|
if (directory != null && file != null) {
|
||||||
|
val selectedFile = File(directory, file)
|
||||||
|
println("[LogoUpload] Datei ausgewählt: ${selectedFile.absolutePath}")
|
||||||
|
val bytes = selectedFile.readBytes()
|
||||||
|
println("[LogoUpload] Bytes gelesen: ${bytes.size}")
|
||||||
|
onFileSelected(bytes)
|
||||||
|
} else {
|
||||||
|
println("[LogoUpload] Auswahl abgebrochen")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("[LogoUpload] FEHLER: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant = ButtonVariant.SECONDARY,
|
||||||
|
size = ButtonSize.SMALL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package at.mocode.frontend.features.verein.presentation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
actual fun decodeBase64ToImage(base64: String): ImageBitmap? = null
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun LogoUploadZone(
|
||||||
|
modifier: Modifier,
|
||||||
|
onFileSelected: (ByteArray) -> Unit
|
||||||
|
) {
|
||||||
|
// Nicht implementiert für WasmJs
|
||||||
|
}
|
||||||
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
package at.mocode.frontend.features.zns.import.di
|
package at.mocode.frontend.features.zns.import.di
|
||||||
|
|
||||||
|
import at.mocode.frontend.core.domain.zns.ZnsImportProvider
|
||||||
import at.mocode.frontend.features.zns.import.ZnsImportViewModel
|
import at.mocode.frontend.features.zns.import.ZnsImportViewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val znsImportModule = module {
|
val znsImportModule = module {
|
||||||
factory { ZnsImportViewModel(get(named("apiClient")), get(), get()) }
|
factory { ZnsImportViewModel(get(named("apiClient")), get(), get()) } bind ZnsImportProvider::class
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-36
@@ -4,7 +4,9 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
@@ -14,14 +16,9 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
||||||
import at.mocode.frontend.features.zns.import.ZnsImportViewModel
|
import at.mocode.frontend.features.zns.import.ZnsImportViewModel
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import javax.swing.JFileChooser
|
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter
|
|
||||||
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -53,36 +50,40 @@ fun StammdatenImportScreen(
|
|||||||
// Datei-Auswahl
|
// Datei-Auswahl
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text("Datei auswählen", style = MaterialTheme.typography.titleMedium)
|
Text("ZNS-Datei auswählen", style = MaterialTheme.typography.titleMedium)
|
||||||
Row(
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
"Wählen Sie entweder die gesamte ZNS.zip oder eine einzelne .dat Datei (z.B. VEREIN01.dat).",
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
style = MaterialTheme.typography.bodySmall,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
) {
|
)
|
||||||
OutlinedTextField(
|
|
||||||
value = state.selectedFilePath ?: "",
|
MsFilePicker(
|
||||||
onValueChange = {},
|
label = "Pfad zur ZNS-Datei",
|
||||||
readOnly = true,
|
selectedPath = state.selectedFilePath,
|
||||||
placeholder = { Text("ZNS-Datei auswählen (.zip, .dat)...") },
|
onFileSelected = { viewModel.onFileSelected(it) },
|
||||||
modifier = Modifier.weight(1f),
|
fileExtensions = listOf("zip", "dat"),
|
||||||
singleLine = true,
|
enabled = !state.isUploading && !(!state.isFinished && state.jobId != null)
|
||||||
)
|
)
|
||||||
Button(
|
|
||||||
onClick = {
|
if (state.isUploading || (state.jobId != null && !state.isFinished)) {
|
||||||
val chooser = JFileChooser()
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
chooser.dialogTitle = "ZNS-Datei auswählen"
|
LinearProgressIndicator(
|
||||||
chooser.fileFilter = FileNameExtensionFilter("ZNS Dateien (*.zip, *.dat)", "zip", "dat")
|
progress = { (state.progress / 100f).coerceIn(0f, 1f) },
|
||||||
chooser.isAcceptAllFileFilterUsed = false
|
modifier = Modifier.fillMaxWidth(),
|
||||||
val result = chooser.showOpenDialog(null)
|
color = MaterialTheme.colorScheme.primary,
|
||||||
if (result == JFileChooser.APPROVE_OPTION) {
|
trackColor = MaterialTheme.colorScheme.primaryContainer
|
||||||
viewModel.onFileSelected(chooser.selectedFile.absolutePath)
|
)
|
||||||
}
|
Text(
|
||||||
},
|
text = if (state.isUploading) "Datei wird hochgeladen..." else "Import wird verarbeitet... (${state.progress}%)",
|
||||||
enabled = !state.isUploading && !(!state.isFinished && state.jobId != null),
|
style = MaterialTheme.typography.labelSmall
|
||||||
) {
|
)
|
||||||
Icon(Icons.Default.FolderOpen, contentDescription = null)
|
if (state.progressDetail.isNotBlank()) {
|
||||||
Spacer(Modifier.width(4.dp))
|
Text(
|
||||||
Text("Durchsuchen")
|
text = state.progressDetail,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,11 @@
|
|||||||
"deviceName": "Meldestelle",
|
"deviceName": "Meldestelle",
|
||||||
"sharedKey": "Password",
|
"sharedKey": "Password",
|
||||||
"backupPath": "/mocode/meldestelle/docs/temp",
|
"backupPath": "/mocode/meldestelle/docs/temp",
|
||||||
"networkRole": "MASTER"
|
"networkRole": "MASTER",
|
||||||
|
"expectedClients": [
|
||||||
|
{
|
||||||
|
"name": "Richter-Turm",
|
||||||
|
"role": "RICHTER"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
+2
@@ -12,6 +12,7 @@ import at.mocode.frontend.core.network.networkModule
|
|||||||
import at.mocode.frontend.core.sync.di.syncModule
|
import at.mocode.frontend.core.sync.di.syncModule
|
||||||
import at.mocode.frontend.features.billing.di.billingModule
|
import at.mocode.frontend.features.billing.di.billingModule
|
||||||
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
import at.mocode.frontend.features.device.initialization.di.deviceInitializationModule
|
||||||
|
import at.mocode.frontend.features.funktionaer.di.funktionaerModule
|
||||||
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
|
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
|
||||||
import at.mocode.frontend.features.pferde.di.pferdeModule
|
import at.mocode.frontend.features.pferde.di.pferdeModule
|
||||||
import at.mocode.frontend.features.profile.di.profileModule
|
import at.mocode.frontend.features.profile.di.profileModule
|
||||||
@@ -42,6 +43,7 @@ fun main() = application {
|
|||||||
billingModule,
|
billingModule,
|
||||||
pferdeModule,
|
pferdeModule,
|
||||||
reiterModule,
|
reiterModule,
|
||||||
|
funktionaerModule,
|
||||||
vereinFeatureModule,
|
vereinFeatureModule,
|
||||||
turnierFeatureModule,
|
turnierFeatureModule,
|
||||||
deviceInitializationModule,
|
deviceInitializationModule,
|
||||||
|
|||||||
+118
-42
@@ -14,22 +14,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
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 at.mocode.frontend.shell.desktop.data.Store
|
|
||||||
import at.mocode.frontend.shell.desktop.data.Turnier
|
|
||||||
import at.mocode.frontend.shell.desktop.data.TurnierStore
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.management.FunktionaerVerwaltungScreen
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterAuswahl
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.profile.FunktionaerProfil
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.VeranstaltungVerwaltung
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungKonfig
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard
|
|
||||||
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
|
|
||||||
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
import at.mocode.frontend.core.auth.data.local.AuthTokenManager
|
||||||
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
|
||||||
@@ -43,24 +30,41 @@ import at.mocode.frontend.features.device.initialization.data.local.DeviceInitia
|
|||||||
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
import at.mocode.frontend.features.device.initialization.domain.model.DeviceInitializationSettings
|
||||||
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen
|
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationScreen
|
||||||
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
|
import at.mocode.frontend.features.device.initialization.presentation.DeviceInitializationViewModel
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerIntent
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerScreen
|
||||||
|
import at.mocode.frontend.features.funktionaer.presentation.FunktionaerViewModel
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
|
import at.mocode.frontend.features.nennung.presentation.NennungManagementScreen
|
||||||
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
import at.mocode.frontend.features.nennung.presentation.NennungViewModel
|
||||||
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
import at.mocode.frontend.features.pferde.presentation.PferdeScreen
|
||||||
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
import at.mocode.frontend.features.pferde.presentation.PferdeViewModel
|
||||||
|
import at.mocode.frontend.features.ping.presentation.PingScreen
|
||||||
|
import at.mocode.frontend.features.ping.presentation.PingViewModel
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
import at.mocode.frontend.features.profile.presentation.ProfileScreen
|
||||||
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
import at.mocode.frontend.features.profile.presentation.ProfileViewModel
|
||||||
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
|
import at.mocode.frontend.features.reiter.presentation.ReiterScreen
|
||||||
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
import at.mocode.frontend.features.reiter.presentation.ReiterViewModel
|
||||||
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
|
||||||
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
|
||||||
import at.mocode.frontend.features.ping.presentation.PingScreen
|
|
||||||
import at.mocode.frontend.features.ping.presentation.PingViewModel
|
|
||||||
import at.mocode.frontend.features.turnier.presentation.SeriesScreen
|
import at.mocode.frontend.features.turnier.presentation.SeriesScreen
|
||||||
import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen
|
import at.mocode.frontend.features.turnier.presentation.TurnierDetailScreen
|
||||||
|
import at.mocode.frontend.features.veranstalter.presentation.VeranstaltungKonfigScreen
|
||||||
|
import at.mocode.frontend.features.verein.presentation.VereinScreen
|
||||||
|
import at.mocode.frontend.features.verein.presentation.VereinViewModel
|
||||||
|
import at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
|
||||||
|
import at.mocode.frontend.shell.desktop.data.Store
|
||||||
|
import at.mocode.frontend.shell.desktop.data.Turnier
|
||||||
|
import at.mocode.frontend.shell.desktop.data.TurnierStore
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.management.FunktionaerVerwaltungScreen
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterAuswahl
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.profile.FunktionaerProfil
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.veranstaltung.VeranstaltungVerwaltung
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard
|
||||||
|
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
|
||||||
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 at.mocode.frontend.features.zns.import.presentation.StammdatenImportScreen
|
|
||||||
import kotlinx.coroutines.delay
|
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
|
||||||
@@ -176,12 +180,64 @@ private fun DesktopNavRail(
|
|||||||
)
|
)
|
||||||
|
|
||||||
NavRailItem(
|
NavRailItem(
|
||||||
icon = Icons.Default.People,
|
icon = Icons.Default.CloudDownload,
|
||||||
label = "Vereine",
|
label = "ZNS-Import",
|
||||||
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung,
|
selected = currentScreen is AppScreen.StammdatenImport,
|
||||||
onClick = { onNavigate(AppScreen.Vereine) }
|
onClick = { onNavigate(AppScreen.StammdatenImport) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var showStammdatenMenu by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
NavRailItem(
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
label = "Stammdaten",
|
||||||
|
selected = currentScreen is AppScreen.Vereine || currentScreen is AppScreen.VereinVerwaltung ||
|
||||||
|
currentScreen is AppScreen.Reiter || currentScreen is AppScreen.ReiterVerwaltung ||
|
||||||
|
currentScreen is AppScreen.Pferde || currentScreen is AppScreen.PferdVerwaltung ||
|
||||||
|
currentScreen is AppScreen.FunktionaerVerwaltung,
|
||||||
|
onClick = { showStammdatenMenu = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showStammdatenMenu,
|
||||||
|
onDismissRequest = { showStammdatenMenu = false },
|
||||||
|
offset = DpOffset(Dimens.NavRailWidth, 0.dp)
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Vereine") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Vereine)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.People, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Reiter") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Reiter)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Pferde") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.Pferde)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Pets, contentDescription = null) }
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Richter") },
|
||||||
|
onClick = {
|
||||||
|
showStammdatenMenu = false
|
||||||
|
onNavigate(AppScreen.FunktionaerVerwaltung)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Gavel, contentDescription = null) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavRailItem(
|
NavRailItem(
|
||||||
icon = Icons.Default.Email,
|
icon = Icons.Default.Email,
|
||||||
label = "Mails",
|
label = "Mails",
|
||||||
@@ -552,7 +608,10 @@ private fun DesktopContentArea(
|
|||||||
is AppScreen.VeranstaltungVerwaltung -> {
|
is AppScreen.VeranstaltungVerwaltung -> {
|
||||||
VeranstaltungVerwaltung(
|
VeranstaltungVerwaltung(
|
||||||
onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) },
|
onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) },
|
||||||
onNewVeranstaltung = { onNavigate(AppScreen.VeranstaltungKonfig()) },
|
onNewVeranstaltung = {
|
||||||
|
// Wenn wir direkt aus der Übersicht kommen, erst Veranstalter wählen lassen
|
||||||
|
onNavigate(AppScreen.VeranstalterAuswahl)
|
||||||
|
},
|
||||||
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
|
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
|
||||||
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
|
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
|
||||||
onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) },
|
onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) },
|
||||||
@@ -570,7 +629,7 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Pferde-Verwaltung & Profil ---
|
// --- Pferde-Verwaltung & Profil ---
|
||||||
is AppScreen.PferdVerwaltung -> {
|
is AppScreen.Pferde, is AppScreen.PferdVerwaltung -> {
|
||||||
val viewModel = koinViewModel<PferdeViewModel>()
|
val viewModel = koinViewModel<PferdeViewModel>()
|
||||||
PferdeScreen(viewModel = viewModel)
|
PferdeScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
@@ -588,7 +647,7 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Reiter-Verwaltung & Profil ---
|
// --- Reiter-Verwaltung & Profil ---
|
||||||
is AppScreen.ReiterVerwaltung -> {
|
is AppScreen.Reiter, is AppScreen.ReiterVerwaltung -> {
|
||||||
val viewModel = koinViewModel<ReiterViewModel>()
|
val viewModel = koinViewModel<ReiterViewModel>()
|
||||||
ReiterScreen(viewModel = viewModel)
|
ReiterScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
@@ -604,7 +663,7 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Verein-Verwaltung & Profil ---
|
// --- Verein-Verwaltung & Profil ---
|
||||||
is AppScreen.VereinVerwaltung -> {
|
is AppScreen.Vereine, is AppScreen.VereinVerwaltung -> {
|
||||||
println("[Screen] Rendering VereinVerwaltung (VereinScreen)")
|
println("[Screen] Rendering VereinVerwaltung (VereinScreen)")
|
||||||
val vereinViewModel: VereinViewModel = koinViewModel()
|
val vereinViewModel: VereinViewModel = koinViewModel()
|
||||||
VereinScreen(viewModel = vereinViewModel)
|
VereinScreen(viewModel = vereinViewModel)
|
||||||
@@ -618,15 +677,20 @@ private fun DesktopContentArea(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Funktionaer-Verwaltung & Profil ---
|
// --- Funktionaer-Verwaltung & Profil ---
|
||||||
is AppScreen.FunktionaerVerwaltung -> FunktionaerVerwaltungScreen(
|
is AppScreen.FunktionaerVerwaltung -> {
|
||||||
onBack = onBack,
|
val viewModel = koinViewModel<FunktionaerViewModel>()
|
||||||
onEdit = { onNavigate(AppScreen.FunktionaerProfil(it)) }
|
FunktionaerScreen(viewModel = viewModel)
|
||||||
)
|
}
|
||||||
|
|
||||||
is AppScreen.FunktionaerProfil -> FunktionaerProfil(
|
is AppScreen.FunktionaerProfil -> {
|
||||||
id = currentScreen.id,
|
val viewModel = koinViewModel<FunktionaerViewModel>()
|
||||||
onBack = onBack,
|
LaunchedEffect(currentScreen.id) {
|
||||||
)
|
viewModel.state.value.list.find { it.id == currentScreen.id }?.let {
|
||||||
|
viewModel.send(FunktionaerIntent.Select(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FunktionaerScreen(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Veranstalter-Verwaltung & Profil ---
|
// --- Veranstalter-Verwaltung & Profil ---
|
||||||
is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
|
is AppScreen.VeranstalterVerwaltung -> VeranstalterVerwaltungScreen(
|
||||||
@@ -671,12 +735,24 @@ 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
|
VeranstaltungKonfigScreen(
|
||||||
VeranstaltungKonfig(
|
|
||||||
veranstalterId = vId,
|
veranstalterId = vId,
|
||||||
onBack = onBack,
|
onAbbrechen = onBack,
|
||||||
onSaved = { evtId: Long, finalVId: Long -> onNavigate(AppScreen.VeranstaltungProfil(finalVId, evtId)) },
|
onSpeichern = { titel, datumVon, datumBis ->
|
||||||
onVeranstalterCreated = { newVId: Long -> onNavigate(AppScreen.VeranstalterDetail(newVId)) }
|
// In-Memory Store Simulation
|
||||||
|
val allEvents = Store.allEvents()
|
||||||
|
val newId = (allEvents.maxOfOrNull { it.id } ?: 0L) + 1L
|
||||||
|
val newEvent = at.mocode.frontend.shell.desktop.data.Veranstaltung(
|
||||||
|
id = newId,
|
||||||
|
veranstalterId = vId,
|
||||||
|
titel = titel,
|
||||||
|
datumVon = datumVon,
|
||||||
|
datumBis = datumBis,
|
||||||
|
status = "NEU"
|
||||||
|
)
|
||||||
|
Store.addEventFirst(vId, newEvent)
|
||||||
|
onNavigate(AppScreen.VeranstaltungProfil(vId, newId))
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,12 +762,12 @@ private fun DesktopContentArea(
|
|||||||
if (Store.vereine.none { it.id == vId }) {
|
if (Store.vereine.none { it.id == vId }) {
|
||||||
InvalidContextNotice(
|
InvalidContextNotice(
|
||||||
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
message = "Veranstalter (ID=$vId) nicht gefunden.",
|
||||||
onBack = onBack
|
onBack = { onNavigate(AppScreen.VeranstalterAuswahl) }
|
||||||
)
|
)
|
||||||
} else if (Store.eventsFor(vId).none { it.id == evtId }) {
|
} else if (Store.eventsFor(vId).none { it.id == evtId }) {
|
||||||
InvalidContextNotice(
|
InvalidContextNotice(
|
||||||
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
|
message = "Veranstaltung (ID=$evtId) gehört nicht zu Veranstalter #$vId.",
|
||||||
onBack = onBack
|
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
VeranstaltungProfilScreen(
|
VeranstaltungProfilScreen(
|
||||||
|
|||||||
+37
-3
@@ -83,13 +83,15 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val filteredMails = remember(mails, searchQuery) {
|
val filteredMails = remember(mails, searchQuery) {
|
||||||
if (searchQuery.isBlank()) mails
|
val base = if (searchQuery.isBlank()) mails
|
||||||
else mails.filter {
|
else mails.filter {
|
||||||
it.vorname.contains(searchQuery, ignoreCase = true) ||
|
it.vorname.contains(searchQuery, ignoreCase = true) ||
|
||||||
it.nachname.contains(searchQuery, ignoreCase = true) ||
|
it.nachname.contains(searchQuery, ignoreCase = true) ||
|
||||||
it.pferd.contains(searchQuery, ignoreCase = true) ||
|
it.pferd.contains(searchQuery, ignoreCase = true) ||
|
||||||
it.turnierNr.contains(searchQuery, ignoreCase = true)
|
it.turnierNr.contains(searchQuery, ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
// Standard-Sortierung: Neueste zuerst (Status NEU oben, dann nach TurnierNr)
|
||||||
|
base.sortedWith(compareBy({ it.status != "NEU" }, { it.turnierNr }, { it.datum }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initiales Laden
|
// Initiales Laden
|
||||||
@@ -108,6 +110,21 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
mails = updated
|
mails = updated
|
||||||
selectedMail = null
|
selectedMail = null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onSendReply = {
|
||||||
|
scope.launch {
|
||||||
|
repository.sendeAntwort(
|
||||||
|
email = selectedMail!!.sender,
|
||||||
|
turnierNr = selectedMail!!.turnierNr,
|
||||||
|
vorname = selectedMail!!.vorname,
|
||||||
|
nachname = selectedMail!!.nachname
|
||||||
|
)
|
||||||
|
// Nach Antwort automatisch als gelesen markieren
|
||||||
|
repository.markiereAlsGelesen(selectedMail!!.id)
|
||||||
|
val updated = mails.map { if (it.id == selectedMail!!.id) it.copy(status = "GELESEN") else it }
|
||||||
|
mails = updated
|
||||||
|
selectedMail = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -212,7 +229,12 @@ fun NennungsEingangScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkProcessed: () -> Unit) {
|
fun NennungDetailDialog(
|
||||||
|
mail: OnlineNennungMail,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onMarkProcessed: () -> Unit,
|
||||||
|
onSendReply: () -> Unit
|
||||||
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("Details zur Online-EntryManagement") },
|
title = { Text("Details zur Online-EntryManagement") },
|
||||||
@@ -235,7 +257,19 @@ fun NennungDetailDialog(mail: OnlineNennungMail, onDismiss: () -> Unit, onMarkPr
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(onClick = onMarkProcessed) { Text("Als gelesen markieren") }
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
if (mail.status == "NEU") {
|
||||||
|
Button(
|
||||||
|
onClick = onSendReply,
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32))
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Email, null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("Antwort & Übernahme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(onClick = onMarkProcessed) { Text("Als gelesen markieren") }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) { Text("Schließen") }
|
TextButton(onClick = onDismiss) { Text("Schließen") }
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ dev.port.offset=0
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
|
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
|
||||||
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
|
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
|
||||||
enableWasm=false
|
enableWasm=true
|
||||||
|
|
||||||
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility)
|
||||||
# See https://kotl.in/dokka-gradle-migration
|
# See https://kotl.in/dokka-gradle-migration
|
||||||
|
|||||||
Reference in New Issue
Block a user