chore: remove obsolete screens from meldestelle-desktop module
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Failing after 2m56s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Failing after 3m3s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Failing after 2m49s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 2m13s

- Deleted unused screens including `AdminUebersichtScreen`, `AktorScreens`, `StammdatenImportScreen`, `TurnierDetailScreen`, and supporting components such as `PlaceholderContent`.
- Cleaned up references and placeholders to streamline module structure.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-26 15:08:38 +01:00
parent 1d393fdefe
commit c2b3b5889f
50 changed files with 5067 additions and 1016 deletions
@@ -0,0 +1,31 @@
/**
* Feature-Modul: Veranstalter-Verwaltung (Desktop-only)
* Kapselt alle Screens und Logik für Veranstalter-Auswahl, -Detail und -Neuanlage.
*/
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
group = "at.mocode.clients"
version = "1.0.0"
kotlin {
jvm()
sourceSets {
jvmMain.dependencies {
implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.domain)
implementation(projects.frontend.core.navigation)
implementation(compose.desktop.currentOs)
implementation(compose.foundation)
implementation(compose.runtime)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.materialIconsExtended)
implementation(libs.bundles.kmp.common)
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
}
}
}
@@ -0,0 +1,259 @@
package at.mocode.veranstalter.feature.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Search
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 androidx.compose.ui.unit.sp
import at.mocode.frontend.core.designsystem.models.LoginStatus
import at.mocode.frontend.core.designsystem.models.LoginStatusBadge
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
/**
* Screen: "Admin - Verwaltung / Veranstalter auswählen"
*
* Gemäß Figma Vision_03 (figma-entwurf_20 / figma-entwurf_22):
* - Titel + Untertitel
* - Suchfeld + "+ Neuer Veranstalter"-Button
* - Tabelle: Vereinsname, OEPS-Nummer, Ort, Ansprechpartner, E-Mail, Login-Status
* - Hinweis-Box
* - Abbrechen / "Weiter zum Veranstalter"-Buttons (unten)
*
* TODO: Echte Daten aus customer-context laden (Phase 4/5).
*/
@Composable
fun VeranstalterAuswahlScreen(
onZurueck: () -> Unit,
onWeiter: (Long) -> Unit,
onNeuerVeranstalter: () -> Unit = {},
) {
var selectedId by remember { mutableStateOf<Long?>(null) }
var suchtext by remember { mutableStateOf("") }
// Placeholder-Daten gemäß Figma
val veranstalter = remember {
listOf(
VeranstalterUiModel(
id = 1L,
name = "Reit- und Fahrverein Wels",
oepsNummer = "V-OOE-1234",
ort = "4600 Wels",
ansprechpartner = "Maria Huber",
email = "office@rfv-wels.at",
loginStatus = LoginStatus.AKTIV,
),
VeranstalterUiModel(
id = 2L,
name = "Pferdesportverein Linz",
oepsNummer = "V-OOE-5678",
ort = "4020 Linz",
ansprechpartner = "Thomas Maier",
email = "kontakt@psv-linz.at",
loginStatus = LoginStatus.AKTIV,
),
VeranstalterUiModel(
id = 3L,
name = "Reitclub Eferding",
oepsNummer = "V-OOE-9012",
ort = "4070 Eferding",
ansprechpartner = "Anna Schmid",
email = "info@rc-eferding.at",
loginStatus = LoginStatus.AUSSTEHEND,
),
)
}
val gefiltert = veranstalter.filter {
suchtext.isBlank() ||
it.name.contains(suchtext, ignoreCase = true) ||
it.oepsNummer.contains(suchtext, ignoreCase = true) ||
it.ort.contains(suchtext, ignoreCase = true)
}
Column(modifier = Modifier.fillMaxSize()) {
// ── Titel ────────────────────────────────────────────────────────────
Column(modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)) {
Text(
text = "Veranstalter für neue Veranstaltung auswählen",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
)
Text(
text = "Wählen Sie einen bestehenden Veranstalter aus oder legen Sie einen neuen Veranstalter an.",
fontSize = 13.sp,
color = Color(0xFF6B7280),
)
}
// ── Suchfeld + Neuer Veranstalter ────────────────────────────────────
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlinedTextField(
value = suchtext,
onValueChange = { suchtext = it },
placeholder = { Text("Veranstalter suchen (Name, OEPS-Nummer, Ort)...", fontSize = 13.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = Modifier.weight(1f).height(48.dp),
singleLine = true,
)
Button(
onClick = onNeuerVeranstalter,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neuer Veranstalter")
}
}
// ── Tabellen-Header ──────────────────────────────────────────────────
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color(0xFFF3F4F6))
.padding(horizontal = 24.dp, vertical = 8.dp),
) {
Spacer(Modifier.width(28.dp)) // Checkmark-Spalte
Text("Vereinsname", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(2.5f))
Text("OEPS-Nummer", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
Text("Ort", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
Text("Ansprechpartner", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1.5f))
Text("E-Mail", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(2f))
Text("Login", fontWeight = FontWeight.SemiBold, fontSize = 12.sp, modifier = Modifier.weight(1f))
}
HorizontalDivider()
// ── Tabellen-Inhalt ──────────────────────────────────────────────────
LazyColumn(modifier = Modifier.weight(1f)) {
items(gefiltert) { v ->
val isSelected = v.id == selectedId
Row(
modifier = Modifier
.fillMaxWidth()
.background(
if (isSelected) AccentBlue.copy(alpha = 0.08f)
else Color.Transparent,
)
.clickable { selectedId = v.id }
.padding(horizontal = 24.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Auswahl-Checkmark
Box(modifier = Modifier.width(28.dp)) {
if (isSelected) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = AccentBlue,
modifier = Modifier.size(18.dp),
)
}
}
Text(
text = v.name,
fontSize = 13.sp,
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
color = AccentBlue,
modifier = Modifier.weight(2.5f),
)
Text(v.oepsNummer, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
Text(v.ort, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
Text(v.ansprechpartner, fontSize = 13.sp, modifier = Modifier.weight(1.5f))
Text(v.email, fontSize = 13.sp, modifier = Modifier.weight(2f))
// Login-Status-Badge
Box(modifier = Modifier.weight(1f)) {
LoginStatusBadge(v.loginStatus)
}
}
HorizontalDivider(color = Color(0xFFE5E7EB))
}
}
// ── Hinweis-Box ──────────────────────────────────────────────────────
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp),
color = Color(0xFFEFF6FF),
shape = MaterialTheme.shapes.small,
border = androidx.compose.foundation.BorderStroke(1.dp, Color(0xFFBFDBFE)),
) {
Row(
modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
Icons.Default.Info,
contentDescription = null,
tint = AccentBlue,
modifier = Modifier.size(16.dp).padding(top = 2.dp),
)
Text(
text = "Veranstalter sind Vereine, die beim österreichischen Pferdesportverband (OEPS) registriert sind. " +
"Beim Anlegen eines neuen Veranstalters werden automatisch Login-Daten generiert und per E-Mail verschickt. " +
"Der Veranstalter kann dann sein Profil (Logo, Kontaktdaten, etc.) selbst verwalten.",
fontSize = 12.sp,
color = Color(0xFF1E40AF),
)
}
}
HorizontalDivider()
// ── Aktions-Buttons ──────────────────────────────────────────────────
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(onClick = onZurueck) {
Text("Abbrechen")
}
Spacer(Modifier.width(12.dp))
Button(
onClick = { selectedId?.let { onWeiter(it) } },
enabled = selectedId != null,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Text("Weiter zum Veranstalter")
}
}
}
}
// --- UI-Modelle ---
data class VeranstalterUiModel(
val id: Long,
val name: String,
val oepsNummer: String,
val ort: String,
val ansprechpartner: String,
val email: String,
val loginStatus: LoginStatus,
)
@@ -0,0 +1,379 @@
package at.mocode.veranstalter.feature.presentation
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import at.mocode.frontend.core.designsystem.models.LoginStatus
import at.mocode.frontend.core.designsystem.models.LoginStatusBadge
import at.mocode.frontend.core.designsystem.models.VeranstaltungStatus
private val PrimaryBlue = Color(0xFF1E3A8A)
private val AccentBlue = Color(0xFF3B82F6)
private val StatusVorbereitungColor = Color(0xFFEA580C)
private val StatusLiveColor = Color(0xFF16A34A)
private val StatusAbgeschlossenColor = Color(0xFF6B7280)
/**
* Screen: "Admin - Verwaltung / Veranstalter auswählen / <Vereinsname>"
*
* Gemäß Figma Vision_03 (figma-entwurf_18 / figma-entwurf_19):
* - Veranstalter-Header: Avatar-Circle, Name, OEPS-Nr., Kontaktdetails-Grid, Login-Status, Mitglied-seit
* - Aktionsleiste: "+ Neue Veranstaltung", Suchfeld, Status-Filter-Chips
* - Veranstaltungs-Liste: Status-Badge, Datum, Ort, Turniere, Nennungen, Bewerbe, Letzte Aktivität, Bearbeiten-Icon
*
* TODO: Echte Daten aus customer-context / event-management-context laden (Phase 4/5).
*/
@Composable
fun VeranstalterDetailScreen(
veranstalterId: Long,
onZurueck: () -> Unit,
onVeranstaltungOeffnen: (Long) -> Unit,
onVeranstaltungNeu: () -> Unit,
) {
var suchtext by remember { mutableStateOf("") }
var statusFilter by remember { mutableStateOf(VeranstaltungStatusFilter.ALLE) }
// Placeholder-Daten gemäß Figma
val veranstalter = remember(veranstalterId) {
VeranstalterDetailUiModel(
id = veranstalterId,
name = "Reit- und Fahrverein Wels",
oepsNummer = "V-OOE-1234",
ansprechpartner = "Maria Huber",
email = "office@rfv-wels.at",
telefon = "+43 7242 12345",
adresse = "Reitweg 15\n4600 Wels",
loginStatus = LoginStatus.AKTIV,
mitgliedSeit = "15.1.2023",
)
}
val veranstaltungen = remember(veranstalterId) {
listOf(
VeranstaltungListUiModel(
id = 1L,
name = "Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026",
datum = "25.-26. April 2026",
ort = "Reitanlage Stroblmair, Neumarkt/M., OO",
turnierAnzahl = 2,
nennungen = 87,
bewerbe = 26,
letzteAktivitaet = "22.03.2026 14:30",
status = VeranstaltungStatus.VORBEREITUNG,
),
VeranstaltungListUiModel(
id = 2L,
name = "AWÖ-Cup Stadl-Paura 2025",
datum = "15.-17. Mai 2025",
ort = "Bundesgestüt Piber, Stadl-Paura",
turnierAnzahl = 2,
nennungen = 142,
bewerbe = 33,
letzteAktivitaet = "17.05.2025 18:45",
status = VeranstaltungStatus.ABGESCHLOSSEN,
),
VeranstaltungListUiModel(
id = 3L,
name = "Linzer Pferdetage 2026",
datum = "12.-14. Juni 2026",
ort = "Reitsportzentrum Linz-Ebelsberg",
turnierAnzahl = 2,
nennungen = 23,
bewerbe = 30,
letzteAktivitaet = "20.03.2026 09:15",
status = VeranstaltungStatus.VORBEREITUNG,
),
)
}
val gefiltert = veranstaltungen.filter { v ->
val matchesStatus = when (statusFilter) {
VeranstaltungStatusFilter.ALLE -> true
VeranstaltungStatusFilter.VORBEREITUNG -> v.status == VeranstaltungStatus.VORBEREITUNG
VeranstaltungStatusFilter.LIVE -> v.status == VeranstaltungStatus.LIVE
VeranstaltungStatusFilter.ABGESCHLOSSEN -> v.status == VeranstaltungStatus.ABGESCHLOSSEN
}
val matchesSuche = suchtext.isBlank() ||
v.name.contains(suchtext, ignoreCase = true) ||
v.ort.contains(suchtext, ignoreCase = true)
matchesStatus && matchesSuche
}
Column(modifier = Modifier.fillMaxSize()) {
// ── Veranstalter-Header-Card ─────────────────────────────────────────
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.White,
border = BorderStroke(1.dp, Color(0xFFE2E8F0)),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.Top,
) {
// Avatar-Circle
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center,
) {
Text(
text = veranstalter.name.first().uppercase(),
color = Color.White,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
)
}
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = veranstalter.name,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
)
Text(
text = "OEPS-Nummer: ${veranstalter.oepsNummer}",
fontSize = 12.sp,
color = Color(0xFF6B7280),
)
Spacer(Modifier.height(6.dp))
// Kontaktdetails-Grid
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
KontaktSpalte("Ansprechpartner", veranstalter.ansprechpartner)
KontaktSpalte("E-Mail", veranstalter.email)
KontaktSpalte("Telefon", veranstalter.telefon)
KontaktSpalte("Adresse", veranstalter.adresse)
Column {
Text("Login-Status", fontSize = 11.sp, color = Color(0xFF9CA3AF))
Spacer(Modifier.height(2.dp))
LoginStatusBadge(veranstalter.loginStatus)
}
KontaktSpalte("Mitglied seit", veranstalter.mitgliedSeit)
}
}
}
// Profil bearbeiten
OutlinedButton(
onClick = { /* TODO */ },
border = BorderStroke(1.dp, Color(0xFFD1D5DB)),
) {
Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(4.dp))
Text("Profil bearbeiten", fontSize = 13.sp)
}
}
}
// ── Aktionsleiste ────────────────────────────────────────────────────
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(
onClick = onVeranstaltungNeu,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(4.dp))
Text("Neue Veranstaltung")
}
OutlinedTextField(
value = suchtext,
onValueChange = { suchtext = it },
placeholder = { Text("Suche nach Name, Ort oder Turnier-Nr.", fontSize = 12.sp) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null, modifier = Modifier.size(18.dp)) },
modifier = Modifier.weight(1f).height(44.dp),
singleLine = true,
)
// Status-Filter-Chips
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
VeranstaltungStatusFilter.entries.forEach { filter ->
val isActive = statusFilter == filter
FilterChip(
selected = isActive,
onClick = { statusFilter = filter },
label = {
Text(
text = when (filter) {
VeranstaltungStatusFilter.ALLE -> "Alle"
VeranstaltungStatusFilter.VORBEREITUNG -> "Vorbereitung"
VeranstaltungStatusFilter.LIVE -> "Live"
VeranstaltungStatusFilter.ABGESCHLOSSEN -> "Abgeschlossen"
},
fontSize = 12.sp,
)
},
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = PrimaryBlue,
selectedLabelColor = Color.White,
),
)
}
}
}
// ── Veranstaltungs-Liste ─────────────────────────────────────────────
LazyColumn(
modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 16.dp),
) {
items(gefiltert) { veranstaltung ->
VeranstaltungListRow(
veranstaltung = veranstaltung,
onOeffnen = { onVeranstaltungOeffnen(veranstaltung.id) },
)
}
}
}
}
@Composable
private fun KontaktSpalte(label: String, wert: String) {
Column {
Text(label, fontSize = 11.sp, color = Color(0xFF9CA3AF))
Text(wert, fontSize = 12.sp, color = Color(0xFF374151))
}
}
@Composable
private fun VeranstaltungListRow(
veranstaltung: VeranstaltungListUiModel,
onOeffnen: () -> Unit,
) {
val statusColor = when (veranstaltung.status) {
VeranstaltungStatus.VORBEREITUNG -> StatusVorbereitungColor
VeranstaltungStatus.LIVE -> StatusLiveColor
VeranstaltungStatus.ABGESCHLOSSEN -> StatusAbgeschlossenColor
}
val statusText = when (veranstaltung.status) {
VeranstaltungStatus.VORBEREITUNG -> "Vorbereitung"
VeranstaltungStatus.LIVE -> "Live"
VeranstaltungStatus.ABGESCHLOSSEN -> "Abgeschlossen"
}
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.White,
border = BorderStroke(1.dp, Color(0xFFE5E7EB)),
shape = MaterialTheme.shapes.small,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
// Status-Badge
Surface(
shape = MaterialTheme.shapes.extraSmall,
color = statusColor.copy(alpha = 0.15f),
modifier = Modifier.width(100.dp),
) {
Text(
text = statusText,
color = statusColor,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
)
}
Spacer(Modifier.width(12.dp))
// Name + Meta
Column(modifier = Modifier.weight(1f)) {
Text(
text = veranstaltung.name,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
)
Spacer(Modifier.height(3.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Text("📅 ${veranstaltung.datum}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("📍 ${veranstaltung.ort}", fontSize = 12.sp, color = Color(0xFF6B7280))
Text("🏆 ${veranstaltung.turnierAnzahl} Turniere", fontSize = 12.sp, color = Color(0xFF6B7280))
}
}
// Statistiken
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
StatSpalte("Nennungen", "${veranstaltung.nennungen}")
StatSpalte("Bewerbe", "${veranstaltung.bewerbe}")
StatSpalte("Letzte Aktivität", veranstaltung.letzteAktivitaet)
}
Spacer(Modifier.width(12.dp))
// Bearbeiten-Icon
IconButton(onClick = onOeffnen) {
Icon(
Icons.Default.Edit,
contentDescription = "Öffnen",
tint = Color(0xFF9CA3AF),
modifier = Modifier.size(18.dp),
)
}
}
}
}
@Composable
private fun StatSpalte(label: String, wert: String) {
Column(horizontalAlignment = Alignment.End) {
Text(label, fontSize = 10.sp, color = Color(0xFF9CA3AF))
Text(wert, fontSize = 14.sp, fontWeight = FontWeight.Bold)
}
}
// --- UI-Modelle ---
enum class VeranstaltungStatusFilter { ALLE, VORBEREITUNG, LIVE, ABGESCHLOSSEN }
data class VeranstalterDetailUiModel(
val id: Long,
val name: String,
val oepsNummer: String,
val ansprechpartner: String,
val email: String,
val telefon: String,
val adresse: String,
val loginStatus: LoginStatus,
val mitgliedSeit: String,
)
data class VeranstaltungListUiModel(
val id: Long,
val name: String,
val datum: String,
val ort: String,
val turnierAnzahl: Int,
val nennungen: Int,
val bewerbe: Int,
val letzteAktivitaet: String,
val status: VeranstaltungStatus,
)
@@ -0,0 +1,231 @@
package at.mocode.veranstalter.feature.presentation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
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 androidx.compose.ui.unit.sp
/**
* Formular zum Anlegen eines neuen Veranstalters (Vision_03: Screenshot 21).
*
* Layout:
* - Info-Banner: "Login-Daten werden automatisch verschickt"
* - Abschnitt "Vereinsdaten": Vereinsname*, OEPS-Nummer*
* - Abschnitt "Kontaktdaten": Ansprechpartner*, E-Mail*, Telefon
* - Abschnitt "Adresse": Straße & Hausnummer, PLZ + Ort
* - Footer-Buttons: Abbrechen | Veranstalter anlegen & Login-Daten senden
*/
@Composable
fun VeranstalterNeuScreen(
onAbbrechen: () -> Unit,
onSpeichern: (vereinsname: String, oepsNummer: String, email: String) -> Unit,
) {
var vereinsname by remember { mutableStateOf("") }
var oepsNummer by remember { mutableStateOf("") }
var ansprechpartner by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var telefon by remember { mutableStateOf("") }
var strasse by remember { mutableStateOf("") }
var plz by remember { mutableStateOf("") }
var ort by remember { mutableStateOf("") }
val isValid = vereinsname.isNotBlank() && oepsNummer.isNotBlank() &&
ansprechpartner.isNotBlank() && email.isNotBlank()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
// Header
Column(modifier = Modifier.padding(horizontal = 40.dp, vertical = 24.dp)) {
Text(
text = "Neuen Veranstalter anlegen",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(4.dp))
Text(
text = "Legen Sie einen neuen Veranstalter (Verein) mit OEPS-Daten an. Nach dem Speichern werden automatisch Login-Daten generiert.",
fontSize = 13.sp,
color = Color(0xFF6B7280),
)
}
// Info-Banner
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 40.dp),
color = Color(0xFFEFF6FF),
shape = MaterialTheme.shapes.medium,
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
Icons.Default.Info,
contentDescription = null,
tint = Color(0xFF2563EB),
modifier = Modifier.size(20.dp),
)
Column {
Text(
text = "Login-Daten werden automatisch verschickt",
fontWeight = FontWeight.SemiBold,
fontSize = 13.sp,
color = Color(0xFF1E40AF),
)
Spacer(Modifier.height(2.dp))
Text(
text = "Nach dem Anlegen werden Login-Daten generiert und an die angegebene E-Mail-Adresse verschickt. Der Veranstalter kann dann sein Profil selbst vervollständigen.",
fontSize = 12.sp,
color = Color(0xFF1E40AF),
)
}
}
}
Spacer(Modifier.height(24.dp))
// Formular-Card
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 40.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
) {
Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
// --- Vereinsdaten ---
Text("Vereinsdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
OutlinedTextField(
value = vereinsname,
onValueChange = { vereinsname = it },
label = { Text("Vereinsname *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
Column {
OutlinedTextField(
value = oepsNummer,
onValueChange = { oepsNummer = it },
label = { Text("OEPS-Nummer *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
Text(
text = "Offizielle Vereinsnummer des OEPS",
fontSize = 11.sp,
color = Color(0xFF2563EB),
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
)
}
HorizontalDivider()
// --- Kontaktdaten ---
Text("Kontaktdaten", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
OutlinedTextField(
value = ansprechpartner,
onValueChange = { ansprechpartner = it },
label = { Text("Ansprechpartner *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
Column {
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("E-Mail *") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
Text(
text = "Login-Daten werden an diese Adresse verschickt",
fontSize = 11.sp,
color = Color(0xFF6B7280),
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
)
}
OutlinedTextField(
value = telefon,
onValueChange = { telefon = it },
label = { Text("Telefon") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
HorizontalDivider()
// --- Adresse ---
Text("Adresse", fontWeight = FontWeight.SemiBold, fontSize = 14.sp)
OutlinedTextField(
value = strasse,
onValueChange = { strasse = it },
label = { Text("Straße & Hausnummer") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(
value = plz,
onValueChange = { plz = it },
label = { Text("PLZ") },
modifier = Modifier.width(120.dp),
singleLine = true,
)
OutlinedTextField(
value = ort,
onValueChange = { ort = it },
label = { Text("Ort") },
modifier = Modifier.weight(1f),
singleLine = true,
)
}
}
}
Spacer(Modifier.height(24.dp))
// Footer-Buttons
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 40.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedButton(onClick = onAbbrechen) {
Text("Abbrechen")
}
Spacer(Modifier.width(12.dp))
Button(
onClick = { onSpeichern(vereinsname, oepsNummer, email) },
enabled = isValid,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF1E3A8A)),
) {
Text("Veranstalter anlegen & Login-Daten senden")
}
}
Spacer(Modifier.height(24.dp))
}
}