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
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+259
@@ -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,
|
||||
)
|
||||
+379
@@ -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,
|
||||
)
|
||||
+231
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user