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:
@@ -1,25 +1,28 @@
|
|||||||
# Journal: 20. April 2026 - Desktop UX & Navigation Refinement
|
# Journal: 20. April 2026 - Desktop UX & Navigation Refinement
|
||||||
|
|
||||||
## 🏗️ Desktop-App: UX & Eingabe-Optimierung
|
## 🏗️ Desktop-App: UX & Eingabe-Optimierung (Update)
|
||||||
|
|
||||||
* **Tastatur-Navigation (Fokus-Flow):**
|
* **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.
|
* **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.
|
||||||
* **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.
|
* **Standardisierung:** Konsistente Nutzung von `MsTextField` in allen neuen Screens (`VeranstalterNeu`, `ZnsImport`).
|
||||||
|
|
||||||
* **Neuer Date-Picker:**
|
* **MsFilePicker (Zentrale Komponente):**
|
||||||
* Implementierung einer kompakten, Desktop-optimierten Komponente `MsDatePickerField`.
|
* Einführung einer plattformübergreifenden `MsFilePicker`-Komponente.
|
||||||
* Ersetzt die manuellen Text-Eingabefelder für den Veranstaltungs-Zeitraum ("von" / "bis") durch einen visuellen Kalender-Dialog.
|
* **Desktop (JVM):** Nutzt den nativen `FileDialog` für Dateiauswahlen (Look & Feel) und `JFileChooser` für Verzeichnisse.
|
||||||
* Erhöht die Datenqualität durch standardisiertes Datumsformat (ISO 8601).
|
* **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
|
## 🧭 Navigation & Stabilität
|
||||||
|
|
||||||
* **Robuste Neuanlage:**
|
* **Veranstalter-Profil (Vereins-Integration):**
|
||||||
* Der direkte Aufruf von `VeranstaltungKonfig(veranstalterId=0)` aus der Gesamtübersicht wurde unterbunden.
|
* Integration einer detaillierten Vereins-Vorschau (Card) im `VeranstalterDetailScreen`.
|
||||||
* User werden nun zuerst zur **Veranstalter-Auswahl** geleitet, um eine valide Kontext-ID sicherzustellen.
|
* Navigation zum Vereins-Editor direkt aus dem Veranstalter-Profil ("Bearbeiten"-Button).
|
||||||
* **Fehler-Handling:**
|
* **UI-Konsistenz:**
|
||||||
* 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.
|
* Einführung eines einheitlichen "Zurück"-Buttons (Pfeil-Icon) in der Header-Zeile aller Detail- und Konfigurations-Screens.
|
||||||
* **UI-Kompaktheit:**
|
* Kompakte Darstellung von Suchergebnissen in der Vereins-Suche (inkl. Logo-Vorschau).
|
||||||
* Alle Formularfelder in der Veranstaltungs-Konfiguration wurden auf den `compact`-Modus (44dp Höhe) umgestellt, um dem High-Density Standard des Projekts zu entsprechen.
|
|
||||||
|
|
||||||
## 🧹 Curator Hinweis
|
## 🧹 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.
|
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.
|
||||||
|
|||||||
+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
|
||||||
|
)
|
||||||
+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
-120
@@ -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,35 +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 = {
|
||||||
@@ -93,53 +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) {
|
|
||||||
focusManager.moveFocus(FocusDirection.Next)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
},
|
},
|
||||||
singleLine = true,
|
directoryOnly = true,
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
modifier = Modifier.focusRequester(backupPathFocus)
|
||||||
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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
Text("Sync-Intervall: ${settings.syncInterval} Min.", style = MaterialTheme.typography.labelMedium)
|
||||||
@@ -313,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) })
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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 {
|
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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-9
@@ -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
|
@Composable
|
||||||
private fun VereinCardPreview(
|
private fun VereinCardPreview(
|
||||||
name: String,
|
name: String,
|
||||||
@@ -113,7 +134,8 @@ private fun VereinCardPreview(
|
|||||||
bundesland: String?,
|
bundesland: String?,
|
||||||
logoUrl: String?,
|
logoUrl: String?,
|
||||||
logoBase64: String?,
|
logoBase64: String?,
|
||||||
status: VereinStatus
|
status: VereinStatus,
|
||||||
|
onEdit: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
@@ -209,6 +231,15 @@ private fun VereinCardPreview(
|
|||||||
size = ButtonSize.SMALL
|
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,
|
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",
|
||||||
|
|||||||
+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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"deviceName": "Meldestelle",
|
"deviceName": "Meldestelle",
|
||||||
"sharedKey": "Password",
|
"sharedKey": "Paassword",
|
||||||
"backupPath": "/mocode/meldestelle/docs/temp",
|
"backupPath": "/mocode/meldestelle/docs/temp",
|
||||||
"networkRole": "MASTER",
|
"networkRole": "MASTER",
|
||||||
"expectedClients": [
|
"expectedClients": [
|
||||||
|
|||||||
Reference in New Issue
Block a user