feat(onboarding): Explicit Device Enrollment für Master-Geräte hinzugefügt
- Master-Geräte können erwartete Clients inkl. Name & Rolle definieren. - Neue Rollen (`RICHTER`, `ZEITNEHMER` etc.) integriert. - Backend- und Frontend-Validierung erweitert, UI-Komponente für Client-Verwaltung. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"geraetName": "Meldestelle",
|
||||
"sharedKey": "Meldestelle",
|
||||
"backupPath": "/mocode/Meldestelle/docs/temp",
|
||||
"networkRole": "MASTER"
|
||||
"backupPath": "/home/stefan/WsMeldestelle/Meldestelle/meldestelle/docs/temp",
|
||||
"networkRole": "MASTER",
|
||||
"expectedClients": [
|
||||
{
|
||||
"name": "Richter-Turm",
|
||||
"role": "RICHTER"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+15
-1
@@ -5,15 +5,29 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
enum class NetworkRole {
|
||||
MASTER,
|
||||
CLIENT
|
||||
CLIENT,
|
||||
RICHTER,
|
||||
ZEITNEHMER,
|
||||
STALLMEISTER,
|
||||
ANZEIGE,
|
||||
PARCOURS_CHEF
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ExpectedClient(
|
||||
val name: String,
|
||||
val role: NetworkRole,
|
||||
val isOnline: Boolean = false,
|
||||
val isSynchronized: Boolean = true
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OnboardingSettings(
|
||||
val geraetName: String = "",
|
||||
val sharedKey: String = "",
|
||||
val backupPath: String = "",
|
||||
val networkRole: NetworkRole = NetworkRole.CLIENT,
|
||||
val expectedClients: List<ExpectedClient> = emptyList(),
|
||||
val syncInterval: Int = 30, // in Minuten
|
||||
val defaultPrinter: String = ""
|
||||
)
|
||||
|
||||
+12
-2
@@ -37,9 +37,19 @@ object OnboardingValidator {
|
||||
* Gibt `true` zurück, wenn alle Pflichtfelder gültig sind und
|
||||
* der „Weiter"-Button aktiviert werden darf.
|
||||
*/
|
||||
fun canContinue(settings: OnboardingSettings): Boolean =
|
||||
isNameValid(settings.geraetName) &&
|
||||
fun canContinue(settings: OnboardingSettings): Boolean {
|
||||
val basicValid = isNameValid(settings.geraetName) &&
|
||||
isKeyValid(settings.sharedKey) &&
|
||||
isBackupPathValid(settings.backupPath) &&
|
||||
isSyncIntervalValid(settings.syncInterval)
|
||||
|
||||
if (!basicValid) return false
|
||||
|
||||
// Falls Master, müssen alle erwarteten Clients einen Namen haben
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
return settings.expectedClients.all { it.name.trim().isNotEmpty() }
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+72
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.desktop.screens.onboarding.ExpectedClient
|
||||
import at.mocode.desktop.screens.onboarding.NetworkRole
|
||||
import at.mocode.desktop.screens.onboarding.OnboardingSettings
|
||||
import at.mocode.desktop.screens.onboarding.OnboardingValidator
|
||||
@@ -142,6 +143,77 @@ fun OnboardingScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (settings.networkRole == NetworkRole.MASTER) {
|
||||
HorizontalDivider(Modifier.padding(vertical = 8.dp))
|
||||
Text("📋 Erwartete Geräte (Clients)", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"Definiere hier, welche Geräte sich in diesem Netzwerk anmelden dürfen.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
settings.expectedClients.forEachIndexed { index, client ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MsTextField(
|
||||
value = client.name,
|
||||
onValueChange = { newName ->
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList[index] = client.copy(name = newName)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
},
|
||||
label = "Name",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }) {
|
||||
Text(client.role.name)
|
||||
Icon(Icons.Default.ArrowDropDown, null)
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
NetworkRole.entries.filter { it != NetworkRole.MASTER }.forEach { role ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(role.name) },
|
||||
onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList[index] = client.copy(role = role)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList.removeAt(index)
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
}) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Entfernen", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
val newList = settings.expectedClients.toMutableList()
|
||||
newList.add(ExpectedClient("Neues Gerät", NetworkRole.CLIENT))
|
||||
onSettingsChange(settings.copy(expectedClients = newList))
|
||||
},
|
||||
modifier = Modifier.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Add, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Gerät hinzufügen")
|
||||
}
|
||||
}
|
||||
|
||||
var showPrinterDialog by remember { mutableStateOf(false) }
|
||||
val availablePrinters = remember {
|
||||
PrintServiceLookup.lookupPrintServices(null, null).map { it.name }
|
||||
|
||||
+33
-1
@@ -136,7 +136,7 @@ class OnboardingValidatorTest {
|
||||
|
||||
@Test
|
||||
fun `B2 canContinue bleibt stabil bei wiederholtem Aufruf mit gleichen Werten`() {
|
||||
// Simuliert schnelles Doppelklick: canContinue darf sich nicht ändern
|
||||
// Simuliert schneller Doppelklick: canContinue darf sich nicht ändern
|
||||
val settings = OnboardingSettings(geraetName = "Meldestelle", sharedKey = "Neumarkt2026", backupPath = "/tmp")
|
||||
val first = OnboardingValidator.canContinue(settings)
|
||||
val second = OnboardingValidator.canContinue(settings)
|
||||
@@ -186,4 +186,36 @@ class OnboardingValidatorTest {
|
||||
"Nach Abbrechen darf der Weiter-Button nicht aktiviert sein"
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Explicit Device Enrollment (Master) ───────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `Master canContinue false wenn ein erwarteter Client keinen Namen hat`() {
|
||||
val masterSettings = OnboardingSettings(
|
||||
geraetName = "Master-PC",
|
||||
sharedKey = "Neumarkt2026",
|
||||
backupPath = "/tmp",
|
||||
networkRole = NetworkRole.MASTER,
|
||||
expectedClients = listOf(
|
||||
ExpectedClient("Richter 1", NetworkRole.RICHTER),
|
||||
ExpectedClient("", NetworkRole.ZEITNEHMER) // Ungültig: Name leer
|
||||
)
|
||||
)
|
||||
assertFalse(OnboardingValidator.canContinue(masterSettings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Master canContinue true wenn alle erwarteten Clients Namen haben`() {
|
||||
val masterSettings = OnboardingSettings(
|
||||
geraetName = "Master-PC",
|
||||
sharedKey = "Neumarkt2026",
|
||||
backupPath = "/tmp",
|
||||
networkRole = NetworkRole.MASTER,
|
||||
expectedClients = listOf(
|
||||
ExpectedClient("Richter 1", NetworkRole.RICHTER),
|
||||
ExpectedClient("Zeitnehmer 1", NetworkRole.ZEITNEHMER)
|
||||
)
|
||||
)
|
||||
assertTrue(OnboardingValidator.canContinue(masterSettings))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user