chore: implementiere MsFilePicker-Komponente, ersetze veraltete Input-Felder in Geräteneukonfiguration und ZNS-Importer, verbessere Vereinskarten-Darstellung und Detail-UX, behebe Tippfehler in settings.json
This commit is contained in:
parent
9fe889b2c1
commit
d4aeba4666
|
|
@ -1,25 +1,28 @@
|
|||
# Journal: 20. April 2026 - Desktop UX & Navigation Refinement
|
||||
|
||||
## 🏗️ Desktop-App: UX & Eingabe-Optimierung
|
||||
## 🏗️ Desktop-App: UX & Eingabe-Optimierung (Update)
|
||||
|
||||
* **Tastatur-Navigation (Fokus-Flow):**
|
||||
* **Device-Setup:** In `DeviceInitializationConfig.jvm.kt` wurde das Verhalten der **Enter-Taste** korrigiert. Sie führt nun konsistent zum nächsten Eingabefeld (Gerätename -> Schlüssel -> Pfad) oder schließt den Prozess ab, anstatt Zeilenumbrüche in einzeiligen Feldern zu erzeugen.
|
||||
* **Veranstaltungs-Konfig:** Das Formular nutzt nun `MsTextField` mit dedizierten `KeyboardActions`. Der Fokus springt beim Drücken von **Enter** oder **Tab** logisch zum nächsten Feld.
|
||||
* **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`).
|
||||
|
||||
* **Neuer Date-Picker:**
|
||||
* Implementierung einer kompakten, Desktop-optimierten Komponente `MsDatePickerField`.
|
||||
* Ersetzt die manuellen Text-Eingabefelder für den Veranstaltungs-Zeitraum ("von" / "bis") durch einen visuellen Kalender-Dialog.
|
||||
* Erhöht die Datenqualität durch standardisiertes Datumsformat (ISO 8601).
|
||||
* **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
|
||||
|
||||
* **Robuste Neuanlage:**
|
||||
* Der direkte Aufruf von `VeranstaltungKonfig(veranstalterId=0)` aus der Gesamtübersicht wurde unterbunden.
|
||||
* User werden nun zuerst zur **Veranstalter-Auswahl** geleitet, um eine valide Kontext-ID sicherzustellen.
|
||||
* **Fehler-Handling:**
|
||||
* Die `InvalidContextNotice` (Fehlermeldung bei ungültigen IDs) wurde verbessert. Der Button "Zur Auswahl" führt nun kontextsensitiv entweder zurück zur Veranstalter-Auswahl oder zum Veranstalter-Profil, anstatt den User im "Nichts" stehen zu lassen.
|
||||
* **UI-Kompaktheit:**
|
||||
* Alle Formularfelder in der Veranstaltungs-Konfiguration wurden auf den `compact`-Modus (44dp Höhe) umgestellt, um dem High-Density Standard des Projekts zu entsprechen.
|
||||
* **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,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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -4,11 +4,9 @@ package at.mocode.frontend.features.device.initialization.presentation
|
|||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
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.VisibilityOff
|
||||
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.unit.dp
|
||||
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.model.NetworkRole
|
||||
import java.io.File
|
||||
import javax.swing.JFileChooser
|
||||
import javax.swing.UIManager
|
||||
|
||||
@Composable
|
||||
actual fun DeviceInitializationConfig(
|
||||
|
|
@ -54,35 +51,28 @@ actual fun DeviceInitializationConfig(
|
|||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("⚙️ Geräte-Konfiguration (${settings.networkRole})", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
MsSettingsField(
|
||||
MsTextField(
|
||||
value = settings.deviceName,
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
|
||||
label = "Gerätename",
|
||||
placeholder = "z.B. Meldestelle-PC-1",
|
||||
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
|
||||
errorText = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) }),
|
||||
modifier = Modifier.focusRequester(deviceNameFocus).onKeyEvent {
|
||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
modifier = Modifier.focusRequester(deviceNameFocus)
|
||||
)
|
||||
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
MsSettingsField(
|
||||
MsTextField(
|
||||
value = settings.sharedKey,
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
|
||||
label = "Sicherheitsschlüssel (Sync-Key)",
|
||||
placeholder = "Mindestens 8 Zeichen",
|
||||
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(),
|
||||
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(
|
||||
onNext = { focusManager.moveFocus(FocusDirection.Next) },
|
||||
onDone = {
|
||||
|
|
@ -93,53 +83,20 @@ actual fun DeviceInitializationConfig(
|
|||
}
|
||||
}
|
||||
),
|
||||
modifier = Modifier.focusRequester(sharedKeyFocus).onKeyEvent {
|
||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
modifier = Modifier.focusRequester(sharedKeyFocus),
|
||||
trailingIcon = if (passwordVisible) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
onTrailingIconClick = { passwordVisible = !passwordVisible }
|
||||
)
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
OutlinedTextField(
|
||||
value = settings.backupPath,
|
||||
onValueChange = { viewModel.updateSettings { s -> s.copy(backupPath = it) } },
|
||||
label = { Text("Backup-Verzeichnis (Pfad)") },
|
||||
placeholder = { Text("/pfad/zu/den/backups") },
|
||||
modifier = Modifier.fillMaxWidth().focusRequester(backupPathFocus).onKeyEvent {
|
||||
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
true
|
||||
} else false
|
||||
MsFilePicker(
|
||||
label = "Backup-Verzeichnis (Pfad)",
|
||||
selectedPath = settings.backupPath,
|
||||
onFileSelected = { selectedPath ->
|
||||
viewModel.updateSettings { s -> s.copy(backupPath = selectedPath) }
|
||||
},
|
||||
singleLine = true,
|
||||
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)
|
||||
|
|
@ -313,12 +270,12 @@ private fun ClientEntryRow(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = name,
|
||||
onValueChange = onNameChange,
|
||||
label = { Text("Gerätename des Clients") },
|
||||
label = "Gerätename des Clients",
|
||||
modifier = Modifier.weight(1f).focusRequester(clientNameFocus),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
|
||||
|
|
@ -338,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}")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ kotlin {
|
|||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.frontend.features.vereinFeature)
|
||||
implementation(projects.frontend.core.designSystem)
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.domain)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.Add
|
|||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -124,17 +125,26 @@ fun VeranstalterDetailScreen(
|
|||
}
|
||||
|
||||
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 ─────────────────────────────────────────
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White),
|
||||
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
|
|
@ -186,12 +196,12 @@ fun VeranstalterDetailScreen(
|
|||
}
|
||||
// Profil bearbeiten
|
||||
OutlinedButton(
|
||||
onClick = { /* TODO */ },
|
||||
onClick = { /* Navigation zu Vereinen */ },
|
||||
border = BorderStroke(1.dp, Color(0xFFD1D5DB)),
|
||||
) {
|
||||
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(14.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Profil bearbeiten", fontSize = 13.sp)
|
||||
Text("Bearbeiten", fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material3.*
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
|
||||
/**
|
||||
* Formular zum Anlegen eines neuen Veranstalters (Vision_03: Screenshot 21).
|
||||
|
|
@ -47,18 +49,27 @@ fun VeranstalterNeuScreen(
|
|||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
// Header
|
||||
Column(modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp)) {
|
||||
Text(
|
||||
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),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
IconButton(onClick = onAbbrechen) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Zurück")
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
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
|
||||
|
|
@ -110,65 +121,46 @@ fun VeranstalterNeuScreen(
|
|||
// --- Vereinsdaten ---
|
||||
Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = vereinsname,
|
||||
onValueChange = { vereinsname = it },
|
||||
label = { Text("Vereinsname *") },
|
||||
label = "Vereinsname *",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = oepsNummer,
|
||||
onValueChange = { oepsNummer = it },
|
||||
label = { Text("OEPS-Nummer *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
Text(
|
||||
text = "Offizielle Vereinsnummer des OEPS",
|
||||
fontSize = 11.sp,
|
||||
color = Color(0xFF2563EB),
|
||||
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
|
||||
)
|
||||
}
|
||||
MsTextField(
|
||||
value = oepsNummer,
|
||||
onValueChange = { oepsNummer = it },
|
||||
label = "OEPS-Nummer *",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
helperText = "Offizielle Vereinsnummer des OEPS"
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// --- Kontaktdaten ---
|
||||
Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = ansprechpartner,
|
||||
onValueChange = { ansprechpartner = it },
|
||||
label = { Text("Ansprechpartner *") },
|
||||
label = "Ansprechpartner *",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Column {
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = { Text("E-Mail *") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
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),
|
||||
)
|
||||
}
|
||||
MsTextField(
|
||||
value = email,
|
||||
onValueChange = { email = it },
|
||||
label = "E-Mail *",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
helperText = "Login-Daten werden an diese Adresse verschickt"
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = telefon,
|
||||
onValueChange = { telefon = it },
|
||||
label = { Text("Telefon") },
|
||||
label = "Telefon",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
|
@ -176,28 +168,25 @@ fun VeranstalterNeuScreen(
|
|||
// --- Adresse ---
|
||||
Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
|
||||
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = strasse,
|
||||
onValueChange = { strasse = it },
|
||||
label = { Text("Straße & Hausnummer") },
|
||||
label = "Straße & Hausnummer",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = plz,
|
||||
onValueChange = { plz = it },
|
||||
label = { Text("PLZ") },
|
||||
label = "PLZ",
|
||||
modifier = Modifier.width(120.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
MsTextField(
|
||||
value = ort,
|
||||
onValueChange = { ort = it },
|
||||
label = { Text("Ort") },
|
||||
label = "Ort",
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,27 @@ fun VereinScreen(
|
|||
)
|
||||
}
|
||||
|
||||
@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,
|
||||
|
|
@ -113,7 +134,8 @@ private fun VereinCardPreview(
|
|||
bundesland: String?,
|
||||
logoUrl: String?,
|
||||
logoBase64: String?,
|
||||
status: VereinStatus
|
||||
status: VereinStatus,
|
||||
onEdit: (() -> Unit)? = null
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
|
|
@ -209,6 +231,15 @@ private fun VereinCardPreview(
|
|||
size = ButtonSize.SMALL
|
||||
)
|
||||
}
|
||||
|
||||
if (onEdit != null) {
|
||||
MsButton(
|
||||
text = "Bearbeiten",
|
||||
onClick = onEdit,
|
||||
variant = ButtonVariant.OUTLINE,
|
||||
size = ButtonSize.SMALL
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -243,14 +274,45 @@ private fun VereinListContent(
|
|||
items = uiState.searchResults,
|
||||
columns = listOf(
|
||||
MsColumnDefinition(
|
||||
title = "Name",
|
||||
weight = 1.5f,
|
||||
cellRenderer = { Text(it.name, style = MaterialTheme.typography.bodySmall) }
|
||||
),
|
||||
MsColumnDefinition(
|
||||
title = "Ort",
|
||||
weight = 1f,
|
||||
cellRenderer = { Text(it.ort ?: "-", style = MaterialTheme.typography.bodySmall) }
|
||||
title = "Verein",
|
||||
weight = 2f,
|
||||
cellRenderer = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
) {
|
||||
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(
|
||||
title = "OePS-Nr",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
|
|
@ -14,14 +16,9 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.frontend.core.designsystem.components.MsFilePicker
|
||||
import at.mocode.frontend.features.zns.import.ZnsImportViewModel
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
|
@ -53,36 +50,40 @@ fun StammdatenImportScreen(
|
|||
// Datei-Auswahl
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("Datei auswählen", style = MaterialTheme.typography.titleMedium)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = state.selectedFilePath ?: "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
placeholder = { Text("ZNS-Datei auswählen (.zip, .dat)...") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
val chooser = JFileChooser()
|
||||
chooser.dialogTitle = "ZNS-Datei auswählen"
|
||||
chooser.fileFilter = FileNameExtensionFilter("ZNS Dateien (*.zip, *.dat)", "zip", "dat")
|
||||
chooser.isAcceptAllFileFilterUsed = false
|
||||
val result = chooser.showOpenDialog(null)
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
viewModel.onFileSelected(chooser.selectedFile.absolutePath)
|
||||
}
|
||||
},
|
||||
enabled = !state.isUploading && !(!state.isFinished && state.jobId != null),
|
||||
) {
|
||||
Icon(Icons.Default.FolderOpen, contentDescription = null)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Durchsuchen")
|
||||
Text("ZNS-Datei auswählen", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Wählen Sie entweder die gesamte ZNS.zip oder eine einzelne .dat Datei (z.B. VEREIN01.dat).",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
MsFilePicker(
|
||||
label = "Pfad zur ZNS-Datei",
|
||||
selectedPath = state.selectedFilePath,
|
||||
onFileSelected = { viewModel.onFileSelected(it) },
|
||||
fileExtensions = listOf("zip", "dat"),
|
||||
enabled = !state.isUploading && !(!state.isFinished && state.jobId != null)
|
||||
)
|
||||
|
||||
if (state.isUploading || (state.jobId != null && !state.isFinished)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
LinearProgressIndicator(
|
||||
progress = { (state.progress / 100f).coerceIn(0f, 1f) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
Text(
|
||||
text = if (state.isUploading) "Datei wird hochgeladen..." else "Import wird verarbeitet... (${state.progress}%)",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
if (state.progressDetail.isNotBlank()) {
|
||||
Text(
|
||||
text = state.progressDetail,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"deviceName": "Meldestelle",
|
||||
"sharedKey": "Password",
|
||||
"sharedKey": "Paassword",
|
||||
"backupPath": "/mocode/meldestelle/docs/temp",
|
||||
"networkRole": "MASTER",
|
||||
"expectedClients": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user