- 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:
@@ -0,0 +1,65 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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,202 @@
|
||||
package screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import at.mocode.clients.shared.presentation.store.AppStore
|
||||
import at.mocode.clients.shared.presentation.state.AppState
|
||||
import at.mocode.clients.pingfeature.PingViewModel
|
||||
import at.mocode.ping.api.HealthResponse
|
||||
import at.mocode.ping.api.PingResponse
|
||||
import at.mocode.ping.api.EnhancedPingResponse
|
||||
|
||||
@Composable
|
||||
fun DevelopmentScreen(appStore: AppStore) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
"🚀 Meldestelle Development Mode",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
|
||||
// Backend Connectivity Tests
|
||||
BackendTestSection()
|
||||
|
||||
// Ping Service Test
|
||||
PingTestSection()
|
||||
|
||||
// State Debugging
|
||||
StateDebugSection(appStore)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackendTestSection() {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("🌐 Backend Connectivity", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
var testStatus by remember { mutableStateOf("Not tested") }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
// TODO: Test Gateway Connection
|
||||
isLoading = true
|
||||
testStatus = "Testing..."
|
||||
},
|
||||
enabled = !isLoading
|
||||
) {
|
||||
Text("Test Gateway")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
// TODO: Test Ping Service Direct
|
||||
isLoading = true
|
||||
testStatus = "Testing direct connection..."
|
||||
},
|
||||
enabled = !isLoading
|
||||
) {
|
||||
Text("Test Ping Service")
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(8.dp))
|
||||
}
|
||||
|
||||
Text("Status: $testStatus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PingTestSection() {
|
||||
val pingViewModel = remember { PingViewModel() }
|
||||
val uiState = pingViewModel.uiState
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("🏓 Ping Service Integration", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = { pingViewModel.performHealthCheck() },
|
||||
enabled = !uiState.isLoading
|
||||
) {
|
||||
Text("Health Check")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { pingViewModel.performSimplePing() },
|
||||
enabled = !uiState.isLoading
|
||||
) {
|
||||
Text("Simple Ping")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { pingViewModel.performEnhancedPing(true) },
|
||||
enabled = !uiState.isLoading
|
||||
) {
|
||||
Text("Test Circuit Breaker")
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.isLoading) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp))
|
||||
}
|
||||
|
||||
// Results Display
|
||||
uiState.healthResponse?.let { health ->
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Text("✅ Health Check Result:")
|
||||
Text("Status: ${health.status}")
|
||||
Text("Service: ${health.service}")
|
||||
Text("Healthy: ${health.healthy}")
|
||||
Text("Timestamp: ${health.timestamp}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.simplePingResponse?.let { ping ->
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Text("🏓 Simple Ping Result:")
|
||||
Text("Status: ${ping.status}")
|
||||
Text("Service: ${ping.service}")
|
||||
Text("Timestamp: ${ping.timestamp}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.enhancedPingResponse?.let { ping ->
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
Text("⚡ Enhanced Ping Result:")
|
||||
Text("Status: ${ping.status}")
|
||||
Text("Circuit Breaker: ${ping.circuitBreakerState}")
|
||||
Text("Response Time: ${ping.responseTime}ms")
|
||||
Text("Service: ${ping.service}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.errorMessage?.let { error ->
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
"❌ Error: $error",
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StateDebugSection(appStore: AppStore) {
|
||||
val appState by appStore.state.collectAsState()
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("🔍 App State Debug", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Text("Auth State: ${if(appState.auth.isAuthenticated) "✅ Authenticated" else "❌ Not Authenticated"}")
|
||||
Text("Current Route: ${appState.navigation.currentRoute}")
|
||||
Text("Dark Mode: ${if(appState.ui.isDarkMode) "🌙 Enabled" else "☀️ Disabled"}")
|
||||
Text("Online: ${if(appState.network.isOnline) "🟢 Online" else "🔴 Offline"}")
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
appStore.dispatch(at.mocode.clients.shared.presentation.actions.AppAction.UI.ToggleDarkMode)
|
||||
}
|
||||
) {
|
||||
Text("Toggle Dark Mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,19 @@
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const path = require('path');
|
||||
// HTML template will be handled by Kotlin/JS build system
|
||||
// No need for custom HtmlWebpackPlugin configuration
|
||||
|
||||
// Template-Pfad für deine index.html
|
||||
const templatePath = path.resolve(__dirname, '../../../../clients/app/src/jsMain/resources/index.html');
|
||||
|
||||
// Erweitere die bestehende Kotlin/JS Webpack-Konfiguration
|
||||
config.plugins.push(new HtmlWebpackPlugin({
|
||||
template: templatePath,
|
||||
filename: 'index.html',
|
||||
inject: 'body',
|
||||
scriptLoading: 'blocking',
|
||||
// Optimierung hinzufügen
|
||||
minify: false
|
||||
/*{
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeEmptyAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
}*/
|
||||
}));
|
||||
|
||||
// Bundle-Analyse für Development
|
||||
// Bundle-Analyse für Development (optional, only if package is available)
|
||||
if (process.env.ANALYZE_BUNDLE === 'true') {
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
config.plugins.push(new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
openAnalyzer: false,
|
||||
reportFilename: 'bundle-report.html'
|
||||
}));
|
||||
try {
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
config.plugins.push(new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
openAnalyzer: false,
|
||||
reportFilename: 'bundle-report.html'
|
||||
}));
|
||||
console.log('Bundle analyzer enabled');
|
||||
} catch (e) {
|
||||
console.log('Bundle analyzer not available (webpack-bundle-analyzer not installed)');
|
||||
}
|
||||
}
|
||||
|
||||
// Weitere Optimierungen hinzufügen (erweitert bestehende config)
|
||||
|
||||
Reference in New Issue
Block a user