chore: entferne settings.json und Veranstaltungskomponenten, refaktoriere Veranstaltungsverwaltung und implementiere StoreVeranstaltungRepository

Signed-off-by: StefanMoCoAt <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-20 18:33:38 +02:00
parent edfe05cbe3
commit db58c24613
17 changed files with 512 additions and 383 deletions
@@ -45,6 +45,7 @@ kotlin {
jvmMain.dependencies {
// Core-Module
implementation(projects.frontend.core.domain)
implementation(projects.core.coreDomain)
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.navigation)
implementation(projects.frontend.core.network)
@@ -1,12 +0,0 @@
{
"deviceName": "Meldestelle",
"sharedKey": "Password",
"backupPath": "/mocode/meldestelle/docs/temp",
"networkRole": "MASTER",
"expectedClients": [
{
"name": "Richter-Turm",
"role": "RICHTER"
}
]
}
@@ -0,0 +1,59 @@
package at.mocode.frontend.shell.desktop.data.repository
import at.mocode.core.domain.model.VeranstaltungsStatusE
import at.mocode.frontend.shell.desktop.data.Store
import at.mocode.veranstaltung.feature.domain.model.VeranstaltungModel
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.datetime.LocalDate
/**
* Brücken-Implementierung, die den bestehenden Store nutzt.
* Liegt in der Shell, da sie Zugriff auf den Shell-spezifischen [Store] benötigt.
*/
class StoreVeranstaltungRepository : VeranstaltungRepository {
override fun getAllVeranstaltungen(): Flow<List<VeranstaltungModel>> = flow {
val allEvents = Store.allEvents().map { event ->
val verein = Store.vereine.find { it.id == event.veranstalterId }
VeranstaltungModel(
id = event.id,
veranstalterId = event.veranstalterId,
titel = event.titel,
datumVon = try {
LocalDate.parse(event.datumVon)
} catch (_: Exception) {
LocalDate(2026, 4, 20)
},
datumBis = try {
event.datumBis?.let { LocalDate.parse(it) }
} catch (_: Exception) {
null
},
status = mapStatus(event.status),
ort = event.ort,
vereinName = verein?.name ?: "Unbekannter Verein",
logoUrl = event.logoUrl
)
}
emit(allEvents)
}
override fun getVeranstaltungenByStatus(status: String): Flow<List<VeranstaltungModel>> = flow {
// Aktuell filtern wir noch nicht tief im Store, das Dashboard übernimmt das Filtern des Flows
emit(emptyList())
}
override suspend fun deleteVeranstaltung(veranstalterId: Long, veranstaltungId: Long) {
Store.removeEvent(veranstalterId, veranstaltungId)
}
private fun mapStatus(status: String): VeranstaltungsStatusE {
return when (status) {
"Abgeschlossen" -> VeranstaltungsStatusE.ABGESCHLOSSEN
"In Vorbereitung" -> VeranstaltungsStatusE.IN_PLANUNG
else -> VeranstaltungsStatusE.AKTIV
}
}
}
@@ -15,13 +15,17 @@ import at.mocode.frontend.features.device.initialization.di.deviceInitialization
import at.mocode.frontend.features.funktionaer.di.funktionaerModule
import at.mocode.frontend.features.nennung.di.nennungFeatureModule
import at.mocode.frontend.features.pferde.di.pferdeModule
import at.mocode.frontend.features.ping.di.pingFeatureModule
import at.mocode.frontend.features.profile.di.profileModule
import at.mocode.frontend.features.reiter.di.reiterModule
import at.mocode.frontend.features.turnier.di.turnierFeatureModule
import at.mocode.frontend.features.veranstalter.di.veranstalterModule
import at.mocode.frontend.features.verein.di.vereinFeatureModule
import at.mocode.frontend.features.zns.import.di.znsImportModule
import at.mocode.frontend.shell.desktop.data.repository.StoreVeranstaltungRepository
import at.mocode.frontend.shell.desktop.di.desktopModule
import at.mocode.frontend.features.ping.di.pingFeatureModule
import at.mocode.frontend.features.turnier.di.turnierFeatureModule
import at.mocode.veranstaltung.feature.di.veranstaltungModule
import at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository
import kotlinx.coroutines.runBlocking
import org.koin.core.context.GlobalContext
import org.koin.core.context.loadKoinModules
@@ -45,7 +49,12 @@ fun main() = application {
reiterModule,
funktionaerModule,
vereinFeatureModule,
veranstalterModule,
turnierFeatureModule,
veranstaltungModule,
module {
single<VeranstaltungRepository> { StoreVeranstaltungRepository() }
},
deviceInitializationModule,
desktopModule,
)
@@ -70,6 +70,7 @@ fun DesktopMainLayout(
onBack = onBack,
onLogout = onLogout,
isAuthenticated = isAuthenticated,
isConfigured = onboardingSettings.isConfigured,
connectedPeersCount = connectedPeers.size
)
@@ -40,13 +40,14 @@ import at.mocode.frontend.shell.desktop.screens.management.VeranstalterAuswahl
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterDetail
import at.mocode.frontend.shell.desktop.screens.management.VeranstalterVerwaltungScreen
import at.mocode.frontend.shell.desktop.screens.nennung.NennungsEingangScreen
import at.mocode.frontend.shell.desktop.screens.veranstaltung.VeranstaltungVerwaltung
import at.mocode.frontend.shell.desktop.screens.veranstaltung.details.VeranstaltungProfilScreen
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.TurnierWizard
import at.mocode.frontend.shell.desktop.screens.veranstaltung.wizards.VeranstalterAnlegenWizard
import at.mocode.veranstaltung.feature.presentation.AdminUebersichtScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungDetailScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungNeuScreen
import at.mocode.veranstaltung.feature.presentation.VeranstaltungenScreen
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -76,18 +77,9 @@ fun DesktopContentArea(
// Haupt-Zentrale: Veranstaltung-Verwaltung
is AppScreen.VeranstaltungVerwaltung -> {
VeranstaltungVerwaltung(
onVeranstaltungOpen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) },
onNewVeranstaltung = {
// Wenn wir direkt aus der Übersicht kommen, erst Veranstalter wählen lassen
onNavigate(AppScreen.VeranstalterAuswahl)
},
onNavigateToPferde = { onNavigate(AppScreen.PferdVerwaltung) },
onNavigateToReiter = { onNavigate(AppScreen.ReiterVerwaltung) },
onNavigateToVereine = { onNavigate(AppScreen.VereinVerwaltung) },
onNavigateToFunktionaere = { onNavigate(AppScreen.FunktionaerVerwaltung) },
onNavigateToVeranstalter = { onNavigate(AppScreen.VeranstalterVerwaltung) },
onNavigateToZnsImport = { onNavigate(AppScreen.StammdatenImport) }
VeranstaltungenScreen(
onVeranstaltungNeu = { onNavigate(AppScreen.VeranstalterAuswahl) },
onVeranstaltungOeffnen = { vId: Long, eId: Long -> onNavigate(AppScreen.VeranstaltungProfil(vId, eId)) }
)
}
@@ -231,11 +223,14 @@ fun DesktopContentArea(
}
is AppScreen.VeranstaltungDetail -> {
val repository: at.mocode.veranstaltung.feature.domain.repository.VeranstaltungRepository = koinInject()
VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id,
repository = repository,
onBack = onBack,
onTurnierOeffnen = { tId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tId)) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) }
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tId)) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) },
onNavigateToVeranstalterProfil = { verId -> onNavigate(AppScreen.VeranstalterDetail(verId)) }
)
}
@@ -261,11 +256,11 @@ fun DesktopContentArea(
at.mocode.frontend.shell.desktop.data.Store.eventsFor(parent.id).firstOrNull { it.id == evtId }
// bewerbViewModel: BewerbViewModel, nennungViewModel: TurnierNennungViewModel, stammdatenViewModel: TurnierStammdatenViewModel
val bewerbViewModel: at.mocode.frontend.features.turnier.presentation.BewerbViewModel =
org.koin.compose.koinInject { parametersOf(currentScreen.turnierId) }
koinInject { parametersOf(currentScreen.turnierId) }
val nennungViewModel: at.mocode.frontend.features.turnier.presentation.TurnierNennungViewModel =
org.koin.compose.koinInject { parametersOf(currentScreen.turnierId) }
koinInject { parametersOf(currentScreen.turnierId) }
val stammdatenViewModel: at.mocode.frontend.features.turnier.presentation.TurnierStammdatenViewModel =
org.koin.compose.koinInject()
koinInject()
TurnierDetailScreen(
veranstaltungId = evtId,
@@ -36,7 +36,7 @@ fun DesktopNavRail(
label = "Logo",
selected = false,
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
enabled = true
enabled = isConfigured
)
Spacer(Modifier.height(Dimens.SpacingL))
@@ -28,6 +28,7 @@ fun DesktopTopHeader(
onBack: () -> Unit,
onLogout: () -> Unit,
isAuthenticated: Boolean,
isConfigured: Boolean = true,
connectedPeersCount: Int = 0
) {
Surface(
@@ -59,13 +60,16 @@ fun DesktopTopHeader(
// Home Icon als Anker
IconButton(
onClick = { onNavigate(AppScreen.VeranstaltungVerwaltung) },
modifier = Modifier.size(Dimens.IconSizeM)
modifier = Modifier.size(Dimens.IconSizeM),
enabled = isConfigured
) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = "Home",
modifier = Modifier.size(Dimens.IconSizeM),
tint = MaterialTheme.colorScheme.primary
tint = if (isConfigured) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.38f
)
)
}
@@ -1,195 +0,0 @@
package at.mocode.frontend.shell.desktop.screens.veranstaltung
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import at.mocode.frontend.shell.desktop.data.Store
import at.mocode.frontend.shell.desktop.theme.DesktopTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VeranstaltungVerwaltung(
onVeranstaltungOpen: (Long, Long) -> Unit, // veranstalterId, veranstaltungId
onNewVeranstaltung: () -> Unit,
onNavigateToPferde: () -> Unit,
onNavigateToReiter: () -> Unit,
onNavigateToVereine: () -> Unit,
onNavigateToFunktionaere: () -> Unit,
onNavigateToVeranstalter: () -> Unit,
onNavigateToZnsImport: () -> Unit
) {
LaunchedEffect(Unit) { println("[Screen] VeranstaltungVerwaltung geladen") }
DesktopTheme {
val allVeranstaltungen = remember { Store.allEvents() }
val vereine = Store.vereine
var searchQuery by remember { mutableStateOf("") }
var selectedStatus by remember { mutableStateOf<String?>(null) }
val availableStatuses = remember(allVeranstaltungen) { allVeranstaltungen.map { it.status }.distinct().sorted() }
val filteredVeranstaltungen = remember(allVeranstaltungen, searchQuery, selectedStatus) {
allVeranstaltungen.filter { veranstaltung ->
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
val matchesSearch = veranstaltung.titel.contains(searchQuery, ignoreCase = true) ||
(verein?.name?.contains(searchQuery, ignoreCase = true) ?: false)
val matchesStatus = selectedStatus == null || veranstaltung.status == selectedStatus
matchesSearch && matchesStatus
}.sortedByDescending { it.datumVon }
}
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
// Header
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Veranstaltungen - verwalten", style = MaterialTheme.typography.headlineMedium)
Button(onClick = onNewVeranstaltung) {
Icon(Icons.Default.Add, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Neue Veranstaltung")
}
}
// Filter & Suche
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text("Suche nach Titel oder Verein...") },
modifier = Modifier.weight(1f),
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { searchQuery = "" }) {
Icon(Icons.Default.Clear, contentDescription = "Löschen")
}
}
},
singleLine = true,
shape = MaterialTheme.shapes.medium
)
// Status Filter Chips
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.FilterList, contentDescription = null, tint = Color.Gray)
FilterChip(
selected = selectedStatus == null,
onClick = { selectedStatus = null },
label = { Text("Alle") }
)
availableStatuses.forEach { status ->
FilterChip(
selected = selectedStatus == status,
onClick = { selectedStatus = status },
label = { Text(status) }
)
}
}
}
}
}
// Liste
if (filteredVeranstaltungen.isEmpty()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Default.EventBusy, contentDescription = null, modifier = Modifier.size(64.dp), tint = Color.LightGray)
Spacer(Modifier.height(16.dp))
Text("Keine Veranstaltungen gefunden", style = MaterialTheme.typography.bodyLarge, color = Color.Gray)
}
}
} else {
LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxSize()) {
items(filteredVeranstaltungen) { veranstaltung ->
val verein = vereine.find { it.id == veranstaltung.veranstalterId }
VeranstaltungCard(
veranstaltung = veranstaltung,
vereinName = verein?.name ?: "Unbekannter Verein",
onClick = { onVeranstaltungOpen(veranstaltung.veranstalterId, veranstaltung.id) }
)
}
}
}
}
}
}
@Composable
fun VeranstaltungCard(
veranstaltung: at.mocode.frontend.shell.desktop.data.Veranstaltung,
vereinName: String,
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Row(
Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
shape = MaterialTheme.shapes.medium
) {
Icon(
Icons.Default.CalendarToday,
contentDescription = null,
modifier = Modifier.padding(12.dp).size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
}
Column(Modifier.weight(1f)) {
Text(veranstaltung.titel, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Text(vereinName, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(Icons.Default.Place, contentDescription = null, modifier = Modifier.size(14.dp), tint = Color.Gray)
Text(veranstaltung.ort, style = MaterialTheme.typography.labelSmall, color = Color.Gray)
Text("", color = Color.Gray)
Text("${veranstaltung.datumVon} - ${veranstaltung.datumBis ?: ""}", style = MaterialTheme.typography.labelSmall, color = Color.Gray)
}
}
Surface(
color = when (veranstaltung.status) {
"Abgeschlossen" -> Color(0xFFE8F5E9)
"In Vorbereitung" -> Color(0xFFE3F2FD)
else -> MaterialTheme.colorScheme.secondaryContainer
},
shape = MaterialTheme.shapes.small
) {
Text(
veranstaltung.status,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
color = when (veranstaltung.status) {
"Abgeschlossen" -> Color(0xFF2E7D32)
"In Vorbereitung" -> Color(0xFF1976D2)
else -> MaterialTheme.colorScheme.onSecondaryContainer
}
)
}
Icon(Icons.Default.ChevronRight, contentDescription = null, tint = Color.LightGray)
}
}
}