feat(onboarding): Netzwerkrollen und automatisches Discovery im Onboarding hinzugefügt
- Unterstützung für Master- und Client-Rollen mit angepasster Konfiguration. - Automatische Dienstsuche (Discovery) für Clients implementiert. - Erweiterte UI für Drucker-, Backup- und Rollenspezifische Einstellungen. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+203
-81
@@ -10,6 +10,7 @@ import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -29,15 +30,27 @@ import at.mocode.desktop.screens.onboarding.NetworkRole
|
||||
import at.mocode.desktop.screens.onboarding.OnboardingSettings
|
||||
import at.mocode.desktop.screens.onboarding.OnboardingValidator
|
||||
import at.mocode.frontend.core.designsystem.components.MsTextField
|
||||
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
|
||||
import org.koin.compose.koinInject
|
||||
import javax.print.PrintServiceLookup
|
||||
import javax.swing.JFileChooser
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
settings: OnboardingSettings,
|
||||
onSettingsChange: (OnboardingSettings) -> Unit,
|
||||
onContinue: (OnboardingSettings) -> Unit,
|
||||
) {
|
||||
var currentStep by remember { mutableStateOf(0) }
|
||||
val discoveryService: NetworkDiscoveryService = koinInject()
|
||||
val discoveredServices by remember { mutableStateOf(discoveryService.getDiscoveredServices()) }
|
||||
|
||||
// Automatische Discovery starten, wenn wir auf Schritt 0 sind
|
||||
LaunchedEffect(currentStep) {
|
||||
if (currentStep == 0) discoveryService.startDiscovery()
|
||||
}
|
||||
|
||||
DesktopThemeV2 {
|
||||
Surface(color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
@@ -50,27 +63,136 @@ fun OnboardingScreen(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
"Bitte konfiguriere deine lokale Instanz (Geburtsurkunde).",
|
||||
if (currentStep == 0) "Schritt 1: Netzwerk-Rolle festlegen" else "Schritt 2: Rollenspezifische Konfiguration",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
if (currentStep == 0) {
|
||||
// PHASE 1: NETZWERK-ROLLE
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text("🌐 Netzwerk-Rolle wählen", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Wähle aus, ob dieses Gerät als Master (zentrale Datenbank) oder als Client fungiert.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Surface(
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = if (settings.networkRole == NetworkRole.MASTER) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.MASTER,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) }
|
||||
)
|
||||
Column {
|
||||
Text("Master (Host)", style = MaterialTheme.typography.labelLarge)
|
||||
Text(
|
||||
"Verwaltet die zentrale Datenbank und koordiniert den Sync.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = if (settings.networkRole == NetworkRole.CLIENT) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.CLIENT,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) }
|
||||
)
|
||||
Column {
|
||||
Text("Client", style = MaterialTheme.typography.labelLarge)
|
||||
Text(
|
||||
"Verbindet sich mit einem Master und synchronisiert Daten.",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { currentStep = 1 },
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
) {
|
||||
Text("Weiter")
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowForward, null, Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// PHASE 2: ROLLENSPEZIFISCH
|
||||
var showPw by remember { mutableStateOf(false) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
// 2.1 / 2.2 IDENTITÄT & SICHERHEIT
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🛡️ Identität & Sicherheit", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
MsTextField(
|
||||
value = settings.geraetName,
|
||||
onValueChange = { onSettingsChange(settings.copy(geraetName = it)) },
|
||||
label = "Gerätename (Pflicht)",
|
||||
placeholder = "z. B. Meldestelle-PC-1",
|
||||
placeholder = "z. B. Haupt-PC",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
} else {
|
||||
// Client: Auswahlbox
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val availableSlots =
|
||||
discoveredServices.flatMap { it.metadata["availableSlots"]?.split(",") ?: emptyList() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }
|
||||
) {
|
||||
MsTextField(
|
||||
value = settings.geraetName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = "Gerätename (Vom Master freigegeben)",
|
||||
trailingIcon = Icons.Default.ArrowDropDown,
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
if (availableSlots.isEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Suche nach verfügbaren Slots...") },
|
||||
onClick = { expanded = false }
|
||||
)
|
||||
} else {
|
||||
availableSlots.forEach { slot ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(slot) },
|
||||
onClick = {
|
||||
onSettingsChange(settings.copy(geraetName = slot))
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MsTextField(
|
||||
value = settings.sharedKey,
|
||||
@@ -87,65 +209,11 @@ fun OnboardingScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// 3.1 ERWARTETE GERÄTE (NUR MASTER)
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("⚙️ Lokale Einstellungen", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
MsTextField(
|
||||
value = settings.backupPath,
|
||||
onValueChange = { onSettingsChange(settings.copy(backupPath = it)) },
|
||||
label = "💾 Datenbank-Sicherungspfad (Backup)",
|
||||
placeholder = "Pfad zum Backup-Verzeichnis",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.FolderOpen,
|
||||
onTrailingIconClick = {
|
||||
val chooser = JFileChooser()
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
chooser.dialogTitle = "Backup-Verzeichnis auswählen"
|
||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
onSettingsChange(settings.copy(backupPath = chooser.selectedFile.absolutePath))
|
||||
}
|
||||
},
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
|
||||
Text("🌐 Netzwerk-Rolle", style = MaterialTheme.typography.labelLarge)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.MASTER,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) }
|
||||
)
|
||||
Text(
|
||||
"Master (Hostet lokale DB)",
|
||||
modifier = Modifier.clickable { onSettingsChange(settings.copy(networkRole = NetworkRole.MASTER)) })
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = settings.networkRole == NetworkRole.CLIENT,
|
||||
onClick = { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) }
|
||||
)
|
||||
Text(
|
||||
"Client",
|
||||
modifier = Modifier.clickable { onSettingsChange(settings.copy(networkRole = NetworkRole.CLIENT)) })
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Text("📡 Sync-Intervall: ${settings.syncInterval} Minuten", style = MaterialTheme.typography.labelLarge)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { onSettingsChange(settings.copy(syncInterval = it.toInt())) },
|
||||
valueRange = 1f..60f,
|
||||
steps = 59,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("📋 Erwartete Geräte (Clients)", style = MaterialTheme.typography.titleSmall)
|
||||
Text("📋 Erwartete Geräte (Clients)", style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
"Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
@@ -195,7 +263,11 @@ fun OnboardingScreen(
|
||||
newList.removeAt(index)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
}) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error)
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Entfernen",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,6 +285,59 @@ fun OnboardingScreen(
|
||||
Text("Gerät hinzufügen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4.1 / 3.2 DATENBANK-SICHERHEITSPFAD
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
if (settings.networkRole == NetworkRole.MASTER) "💾 Datenbank-Sicherheitspfad" else "💾 Lokaler Cache-Sicherungspfad",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
MsTextField(
|
||||
value = settings.backupPath,
|
||||
onValueChange = { onSettingsChange(settings.copy(backupPath = it)) },
|
||||
label = "Pfad auswählen",
|
||||
placeholder = "Verzeichnis für Backups/Cache",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.FolderOpen,
|
||||
onTrailingIconClick = {
|
||||
val chooser = JFileChooser()
|
||||
chooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
|
||||
chooser.dialogTitle = "Verzeichnis auswählen"
|
||||
if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
|
||||
onSettingsChange(settings.copy(backupPath = chooser.selectedFile.absolutePath))
|
||||
}
|
||||
},
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 5.1 SYNC-INTERVALL (NUR MASTER)
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🔄 Sync-Intervall", style = MaterialTheme.typography.titleMedium)
|
||||
Text("📡 Intervall: ${settings.syncInterval} Minuten", style = MaterialTheme.typography.labelLarge)
|
||||
Slider(
|
||||
value = settings.syncInterval.toFloat(),
|
||||
onValueChange = { onSettingsChange(settings.copy(syncInterval = it.toInt())) },
|
||||
valueRange = 1f..60f,
|
||||
steps = 59,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6.1 / 4.2 DRUCKER
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text("🖨️ Drucker", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
var showPrinterDialog by remember { mutableStateOf(false) }
|
||||
val availablePrinters = remember {
|
||||
@@ -222,7 +347,7 @@ fun OnboardingScreen(
|
||||
MsTextField(
|
||||
value = settings.defaultPrinter,
|
||||
onValueChange = { onSettingsChange(settings.copy(defaultPrinter = it)) },
|
||||
label = "🖨️ Standard-Drucker",
|
||||
label = "Standard-Drucker",
|
||||
placeholder = "Name des Standard-Druckers",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
trailingIcon = Icons.Default.Print,
|
||||
@@ -253,10 +378,7 @@ fun OnboardingScreen(
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = settings.defaultPrinter == printer,
|
||||
onClick = null
|
||||
)
|
||||
RadioButton(selected = settings.defaultPrinter == printer, onClick = null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(printer)
|
||||
}
|
||||
@@ -264,31 +386,31 @@ fun OnboardingScreen(
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { showPrinterDialog = false }) {
|
||||
Text("Schließen")
|
||||
}
|
||||
}
|
||||
confirmButton = { TextButton(onClick = { showPrinterDialog = false }) { Text("Schließen") } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val canContinue = OnboardingValidator.canContinue(settings)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(onClick = { currentStep = 0 }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Zurück zur Rollenauswahl")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { onContinue(settings) },
|
||||
enabled = canContinue,
|
||||
modifier = Modifier.align(Alignment.End)
|
||||
enabled = OnboardingValidator.canContinue(settings)
|
||||
) {
|
||||
Text("Konfiguration speichern & starten")
|
||||
Text("Konfiguration abschließen")
|
||||
Icon(Icons.Default.Check, null, Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (!canContinue) {
|
||||
Text(
|
||||
"Bitte alle Pflichtfelder korrekt ausfüllen (Name min. 3, Key min. 8, Backup-Pfad gesetzt).",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user