- Frontend-Aufbau für Meldestelle KMP

- Network Layer
- Shared Foundation
- Service Layer and API Integration
- Test-Fix und Development Screen
- WASM-Js Test-Implementation
- Build-Konfiguration reparieren
This commit is contained in:
2025-10-06 22:54:32 +02:00
parent 389e612e88
commit d462f98e05
42 changed files with 3064 additions and 741 deletions
@@ -0,0 +1 @@
expect fun isDevelopmentMode(): Boolean
@@ -0,0 +1,95 @@
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun MainApp() {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
DevelopmentScreen()
}
}
}
@Composable
fun DevelopmentScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"🚀 Meldestelle Development Mode",
style = MaterialTheme.typography.headlineMedium
)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"🌐 Backend Connectivity",
style = MaterialTheme.typography.titleMedium
)
var testStatus by remember { mutableStateOf("Not tested") }
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { testStatus = "Testing Gateway..." }) {
Text("Test Gateway")
}
Button(onClick = { testStatus = "Testing Ping Service..." }) {
Text("Test Ping Service")
}
}
Text("Status: $testStatus")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"🏓 Ping Service Tests",
style = MaterialTheme.typography.titleMedium
)
var isDarkMode by remember { mutableStateOf(false) }
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { /* TODO: Health Check */ }) {
Text("Health Check")
}
Button(onClick = { /* TODO: Ping Normal */ }) {
Text("Ping Normal")
}
Button(onClick = { isDarkMode = !isDarkMode }) {
Text("Toggle Dark Mode")
}
}
Text("Dark Mode: ${if(isDarkMode) "🌙 Enabled" else "☀️ Disabled"}")
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"✅ System Status",
style = MaterialTheme.typography.titleMedium
)
Text("Frontend: 🟢 Running")
Text("Backend: ⚠️ Testing needed")
Text("Build: ✅ Successful")
}
}
}
}
@@ -1,65 +0,0 @@
package at.mocode.clients.app
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import at.mocode.clients.shared.commonui.components.AppHeader
import at.mocode.clients.shared.commonui.components.AppScaffold
import at.mocode.clients.shared.commonui.theme.AppTheme
import at.mocode.clients.shared.navigation.AppScreen
import at.mocode.clients.pingfeature.PingScreen
import at.mocode.clients.pingfeature.PingViewModel
import at.mocode.clients.authfeature.LoginScreen
import at.mocode.clients.authfeature.AuthTokenManager
import androidx.compose.runtime.collectAsState
@Composable
fun App() {
var currentScreen: AppScreen by remember { mutableStateOf(AppScreen.Home) }
// Create a single PingViewModel instance for the lifetime of the App composition.
val pingViewModel: PingViewModel = remember { PingViewModel() }
// Create a single AuthTokenManager instance for the lifetime of the App composition.
val authTokenManager: AuthTokenManager = remember { AuthTokenManager() }
// Observe authentication state
val authState by authTokenManager.authState.collectAsState()
AppTheme {
AppScaffold(
header = {
AppHeader(
title = "Meldestelle",
onNavigateToPing = { currentScreen = AppScreen.Ping },
onNavigateToLogin = { currentScreen = AppScreen.Login },
onLogout = {
authTokenManager.clearToken()
currentScreen = AppScreen.Home
},
isAuthenticated = authState.isAuthenticated,
username = authState.username,
userPermissions = authState.permissions.map { it.name }
)
},
{ paddingValues ->
Box(modifier = Modifier.padding(paddingValues)) {
when (currentScreen) {
is AppScreen.Home -> {
LandingScreen(authTokenManager = authTokenManager)
}
is AppScreen.Login -> {
LoginScreen(
authTokenManager = authTokenManager,
onLoginSuccess = { currentScreen = AppScreen.Home }
)
}
is AppScreen.Ping -> {
PingScreen(viewModel = pingViewModel)
}
}
}
}
)
}
}
@@ -1,232 +0,0 @@
package at.mocode.clients.app
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import at.mocode.clients.authfeature.AuthTokenManager
import at.mocode.clients.authfeature.Permission
@Composable
fun LandingScreen(
authTokenManager: AuthTokenManager? = null
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Text(
text = "Willkommen bei Meldestelle",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Eine moderne, skalierbare Frontend-Architektur",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Diese Anwendung demonstriert eine \"Shell + Feature-Module\"-Architektur " +
"basierend auf Kotlin Multiplatform. Sie spiegelt die DDD-Struktur des Backends " +
"wider und ist als native Desktop-Anwendung (JVM) und Web-Anwendung (JS/Wasm) lauffähig.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
lineHeight = MaterialTheme.typography.bodyLarge.lineHeight * 1.2
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "🚀 Technologien:",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
TechItem("Kotlin Multiplatform")
TechItem("Jetpack Compose Multiplatform")
TechItem("Material Design 3")
TechItem("Ktor Client")
TechItem("Domain-Driven Design")
}
}
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Verwenden Sie das Ping Service Menü oben, um die API-Funktionalität zu testen.",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// Permission-based UI demonstration
authTokenManager?.let { tokenManager ->
val authState by tokenManager.authState.collectAsState()
if (authState.isAuthenticated && authState.permissions.isNotEmpty()) {
Spacer(modifier = Modifier.height(32.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "🔐 Verfügbare Funktionen",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
Spacer(modifier = Modifier.height(16.dp))
// Admin features (visible only to users with delete permissions)
if (tokenManager.isAdmin()) {
PermissionCard(
title = "👑 Administrator-Bereich",
description = "Vollzugriff auf alle System-Funktionen",
permissions = listOf("Alle Berechtigungen", "System-Verwaltung", "Benutzer-Management"),
backgroundColor = MaterialTheme.colorScheme.errorContainer,
textColor = MaterialTheme.colorScheme.onErrorContainer
)
}
// Management features (visible to users with create/update permissions)
if (tokenManager.canCreate() || tokenManager.canUpdate()) {
PermissionCard(
title = "✏️ Verwaltung",
description = "Erstellen und bearbeiten von Daten",
permissions = buildList {
if (tokenManager.hasPermission(Permission.PERSON_CREATE)) add("Personen erstellen")
if (tokenManager.hasPermission(Permission.PERSON_UPDATE)) add("Personen bearbeiten")
if (tokenManager.hasPermission(Permission.VEREIN_CREATE)) add("Vereine erstellen")
if (tokenManager.hasPermission(Permission.VEREIN_UPDATE)) add("Vereine bearbeiten")
if (tokenManager.hasPermission(Permission.PFERD_CREATE)) add("Pferde erstellen")
if (tokenManager.hasPermission(Permission.PFERD_UPDATE)) add("Pferde bearbeiten")
if (tokenManager.hasPermission(Permission.VERANSTALTUNG_CREATE)) add("Veranstaltungen erstellen")
if (tokenManager.hasPermission(Permission.VERANSTALTUNG_UPDATE)) add("Veranstaltungen bearbeiten")
},
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
textColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
// Read-only features (visible to all authenticated users)
if (tokenManager.canRead()) {
PermissionCard(
title = "👁️ Ansicht",
description = "Nur-Lese-Zugriff auf Daten",
permissions = buildList {
if (tokenManager.hasPermission(Permission.PERSON_READ)) add("Personen anzeigen")
if (tokenManager.hasPermission(Permission.VEREIN_READ)) add("Vereine anzeigen")
if (tokenManager.hasPermission(Permission.PFERD_READ)) add("Pferde anzeigen")
if (tokenManager.hasPermission(Permission.VERANSTALTUNG_READ)) add("Veranstaltungen anzeigen")
},
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
textColor = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
@Composable
private fun TechItem(text: String) {
Text(
text = "$text",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(vertical = 2.dp)
)
}
@Composable
private fun PermissionCard(
title: String,
description: String,
permissions: List<String>,
backgroundColor: androidx.compose.ui.graphics.Color,
textColor: androidx.compose.ui.graphics.Color
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = backgroundColor
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = textColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = textColor
)
if (permissions.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
permissions.forEach { permission ->
Text(
text = "$permission",
style = MaterialTheme.typography.bodySmall,
color = textColor,
modifier = Modifier.padding(vertical = 2.dp)
)
}
}
}
}
}
@@ -0,0 +1,2 @@
actual fun isDevelopmentMode(): Boolean =
kotlinx.browser.window.location.hostname == "localhost"
+12 -4
View File
@@ -1,13 +1,21 @@
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.ComposeViewport
import at.mocode.clients.app.App
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.HTMLElement
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val root = document.getElementById("ComposeTarget") as HTMLElement
ComposeViewport(root) {
App()
window.onload = {
try {
val root = document.getElementById("ComposeTarget") as HTMLElement
ComposeViewport(root) {
MainApp()
}
} catch (e: Exception) {
console.error("Failed to start Compose Web app", e)
document.getElementById("root")?.innerHTML =
"<div style='padding: 50px; text-align: center;'>❌ Failed to load app: ${e.message}</div>"
}
}
}
+31 -22
View File
@@ -1,30 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Meldestelle - Web Development</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meldestelle</title>
<meta name="theme-color" content="#0f172a">
<link type="text/css" rel="stylesheet" href="styles.css">
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" href="icons/icon-192.png" type="image/png">
<style>
body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, sans-serif;
background: #fafafa;
}
#ComposeTarget {
width: 100vw;
height: 100vh;
}
#root {
width: 100vw;
height: 100vh;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 18px;
color: #666;
}
</style>
</head>
<body>
<div id="ComposeTarget"></div>
<script src="web-app.js"></script>
<script>
if ('serviceWorker' in navigator) {
const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(location.hostname);
if (isLocalhost) {
navigator.serviceWorker.getRegistrations().then(regs => {
for (const reg of regs) reg.unregister();
}).catch(console.error);
} else {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js').catch(console.error);
});
}
}
</script>
<div id="root">
<canvas id="ComposeTarget"></canvas>
<div class="loading">🚀 Loading Meldestelle...</div>
</div>
<script src="web-app.js"></script>
</body>
</html>
@@ -0,0 +1,2 @@
actual fun isDevelopmentMode(): Boolean =
System.getProperty("development.mode", "false").toBoolean()
+5 -3
View File
@@ -1,12 +1,14 @@
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import at.mocode.clients.app.App
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.unit.dp
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Meldestelle - Desktop Application"
title = "Meldestelle - Desktop Development",
state = WindowState(width = 1200.dp, height = 800.dp)
) {
App()
MainApp()
}
}