Refactor Veranstalter and Veranstaltung flows: add VeranstalterProfil UI, event creation callback, profile enhancements, and save-enable matrix logic. Extend ZNS import and branding workflows.
Some checks failed
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Has been cancelled
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Has been cancelled

This commit is contained in:
Stefan Mogeritsch 2026-04-01 02:49:22 +02:00
parent f44b2c8126
commit 09debdef86
14 changed files with 1163 additions and 43 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,409 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ergebnisliste</title>
<style>
body {
font-family: 'Tahoma', 'Verdana', sans-serif;
background-color: #F0F0F0;
margin: 0;
padding: 5px;
box-sizing: border-box;
color: black;
font-size: 11px;
}
#window {
border: 1px solid #999;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
overflow: hidden;
background-color: #F0F0F0;
}
#title-bar {
background: linear-gradient(to right, #005A9C 0%, #0099CC 100%);
color: white;
padding: 3px 6px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #CCC;
}
#title-controls {
display: flex;
gap: 2px;
}
.title-btn {
width: 16px;
height: 16px;
background-color: white;
color: black;
border: 1px solid #333;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-weight: normal;
}
#main-content {
padding: 5px;
display: grid;
grid-template-columns: 3fr 1fr;
gap: 5px;
}
#left-column {
display: flex;
flex-direction: column;
gap: 5px;
}
.grid-header {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 1px;
background-color: #EAEAEA;
border: 1px solid #CCC;
}
.grid-item {
border: 1px solid #CCC;
padding: 2px;
text-align: center;
font-size: 10px;
}
.pane {
background-color: white;
border: 1px solid #CCC;
min-height: 200px;
overflow: auto;
}
.toolbar {
display: flex;
gap: 8px;
align-items: center;
background-color: #EAEAEA;
padding: 3px;
border: 1px solid #CCC;
border-bottom: none;
}
.tool-link {
color: black;
text-decoration: none;
cursor: pointer;
}
.tool-link:hover {
text-decoration: underline;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 10px;
}
th {
background-color: #EAEAEA;
border: 1px solid #CCC;
padding: 2px 4px;
text-align: left;
font-weight: normal;
}
td {
border-bottom: 1px solid #EEE;
padding: 2px 4px;
}
#middle-form {
background-color: #F0F0F0;
border: 1px solid #CCC;
padding: 5px;
display: flex;
align-items: flex-start;
gap: 10px;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 3px;
}
.form-inputs {
display: flex;
flex-direction: column;
gap: 3px;
align-items: flex-start;
}
.input-pair {
display: flex;
gap: 3px;
}
.small-input {
width: 25px;
border: 1px solid #AAA;
text-align: center;
}
.form-label {
font-size: 9px;
color: #555;
pointer-events: none;
}
.form-select {
width: 80px;
border: 1px solid #AAA;
}
.button-group {
display: flex;
gap: 3px;
}
.windows-btn {
background-color: #EAEAEA;
border: 1px solid #AAA;
padding: 2px 8px;
cursor: pointer;
box-shadow: 1px 1px 1px rgba(0,0,0,0.1);
}
.windows-btn:hover {
background-color: #F5F5F5;
}
.windows-btn:disabled {
opacity: 0.5;
cursor: default;
}
#right-panels {
display: flex;
flex-direction: column;
gap: 5px;
}
.right-panel {
background-color: #F0F0F0;
border: 1px solid #CCC;
padding: 5px;
}
.panel-title {
margin-bottom: 5px;
font-weight: bold;
}
.control-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 3px;
}
.right-label {
width: 100px;
}
.number-controls {
display: flex;
gap: 1px;
align-items: center;
}
.number-btn {
width: 14px;
height: 14px;
background-color: white;
border: 1px solid #AAA;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
font-size: 12px;
padding: 0;
}
.checkbox-container {
display: flex;
justify-content: flex-end;
width: calc(100% - 100px);
}
.right-panel-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3px;
margin-top: 5px;
}
</style>
</head>
<body>
<div id="window">
<div id="title-bar">
<span>Ergebnisliste</span>
<div id="title-controls">
<div class="title-btn">_</div>
<div class="title-btn"></div>
<div class="title-btn">x</div>
</div>
</div>
<div id="main-content">
<div id="left-column">
<div class="grid-header">
<div class="grid-item">1</div><div class="grid-item">2</div><div class="grid-item">3</div><div class="grid-item">4</div><div class="grid-item">5</div><div class="grid-item">5</div>
<div class="grid-item">6</div><div class="grid-item">7</div><div class="grid-item">8</div><div class="grid-item">9</div><div class="grid-item">10</div><div class="grid-item"></div>
<div class="grid-item">11</div><div class="grid-item">12</div><div class="grid-item"></div><div class="grid-item"></div><div class="grid-item"></div><div class="grid-item"></div>
<div class="grid-item"></div><div class="grid-item"></div><div class="grid-item"></div><div class="grid-item"></div><div class="grid-item"></div><div class="grid-item"></div>
</div>
<div>
<div class="toolbar">
<span class="tool-link">Aktualisieren</span>
<span class="tool-link">Platzierte</span>
<span class="tool-link">Suchen &#x25BC;</span>
<span style="color: #666;">0 gefunden</span>
<span class="tool-link">Bearbeiten</span>
</div>
<div class="pane">
<table>
<thead>
<tr>
<th>Ergebnis</th><th>Nr.</th><th>KopfNr</th><th>Pferd</th><th>Reiter</th><th>K</th><th>Q</th><th>Platziert</th><th>Wertung</th><th>PGp</th><th>ZGp</th><th>Gesamtnote</th><th>Geldpreis</th><th>Pl. E</th><th>Pl. H</th><th>Pl. C</th><th>Pl. M</th>
</tr>
</thead>
</table>
</div>
</div>
<div id="middle-form">
<div class="radio-group">
<label><input type="radio" name="parcours" checked> Grundparcours</label>
<label><input type="radio" name="parcours"> Stechen 1</label>
<label><input type="radio" name="parcours"> Stechen 2</label>
<label><input type="radio" name="parcours"> Stechen 3</label>
</div>
<div class="form-inputs">
<div class="input-pair">
<div>
<input type="text" class="small-input">
<div class="form-label">Fehler</div>
</div>
<div>
<input type="text" class="small-input">
<div class="form-label">Zeit</div>
</div>
</div>
</div>
<div>
<select class="form-select"></select>
</div>
<div class="button-group">
<button class="windows-btn" disabled>Speichern</button>
<button class="windows-btn">Nächster</button>
<button class="windows-btn" disabled>Abbrechen</button>
</div>
</div>
<div>
<div class="toolbar">
<span class="tool-link">Aktualisieren</span>
<span class="tool-link">Starter</span>
<span class="tool-link">Suchen &#x25BC;</span>
<span class="tool-link">Bearbeiten</span>
</div>
<div class="pane">
<table>
<thead>
<tr>
<th>Pos.</th><th>Nr.</th><th>KopfNr</th><th>Pferd</th><th>Reiter</th><th>K</th><th>Q</th><th>Bemerkung</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
<div id="right-panels">
<div class="right-panel">
<div class="panel-title">Platzierung & Geldpreis:</div>
<div class="control-row">
<span class="right-label">Anzahl Platzierte:</span>
<div class="number-controls">
<button class="number-btn">-</button>
<input type="text" class="small-input">
<button class="number-btn">+</button>
</div>
</div>
<div class="control-row">
<span class="right-label">Geldpreis:</span>
<span>---</span>
</div>
<div class="control-row">
<span class="right-label">Kaderreiter Extra:</span>
<div class="checkbox-container">
<input type="checkbox">
</div>
</div>
<div class="control-row">
<span class="right-label">Anzahl platzierte Kaderreiter:</span>
<input type="text" class="small-input">
</div>
<div class="control-row">
<span class="right-label">Geldpreis für Kaderreiter:</span>
<span>---</span>
</div>
<div class="control-row">
<span class="right-label">Summe Geldpreise:</span>
<span>---</span>
</div>
</div>
<div class="right-panel">
<div class="panel-title">Bewerb:</div>
<div class="right-panel-buttons">
<button class="windows-btn">Abschließen</button>
<button class="windows-btn">Öffnen</button>
</div>
</div>
<div class="right-panel">
<div class="panel-title">Ergebnisliste:</div>
<div class="right-panel-buttons">
<button class="windows-btn">Import</button>
<button class="windows-btn">Pfad</button>
<button class="windows-btn">Export</button>
<button class="windows-btn">Drucken</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nennmaske Entwurf</title>
<style>
:root {
--bg-color: #f4f7f6;
--border-color: #d1d5db;
--primary-blue: #3f51b5;
--text-main: #333;
--header-bg: #ffffff;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
margin: 0;
padding: 10px;
font-size: 13px;
color: var(--text-main);
}
/* Layout Struktur */
.container {
display: grid;
grid-template-rows: 1fr auto 1fr;
gap: 10px;
height: 95vh;
}
.top-row {
display: grid;
grid-template-columns: 1fr 1fr 1.2fr;
gap: 10px;
}
.bottom-row {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 10px;
}
/* Gemeinsame Panel-Styles */
.panel {
background: white;
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
border-radius: 4px;
overflow: hidden;
}
.panel-header {
padding: 8px;
background: #fff;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 5px;
}
.panel-content {
flex-grow: 1;
overflow-y: auto;
background: #fff;
position: relative;
}
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #888;
}
/* Formular Elemente */
input[type="text"] {
flex-grow: 1;
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 3px;
}
button {
padding: 5px 12px;
border: 1px solid var(--border-color);
background: white;
cursor: pointer;
border-radius: 3px;
}
.btn-primary {
border-color: var(--primary-blue);
color: var(--primary-blue);
}
.action-bar-center {
display: flex;
justify-content: center;
gap: 10px;
padding: 10px 0;
}
.nav-btn {
background: var(--primary-blue);
color: white;
border: none;
padding: 8px 20px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 5px;
}
/* Tabellen Styles */
table {
width: 100%;
border-collapse: collapse;
}
th {
background: #f9fafb;
text-align: left;
padding: 8px;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
}
td {
padding: 6px 8px;
border-bottom: 1px solid #f0f0f0;
}
.row-selected {
background-color: #fff9c4;
}
/* Spezifische Anpassungen */
.footer-btns {
padding: 8px;
display: flex;
gap: 10px;
border-top: 1px solid var(--border-color);
}
.footer-btns button {
flex: 1;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
}
.tab {
padding: 8px 15px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab.active {
color: var(--primary-blue);
border-bottom: 2px solid var(--primary-blue);
font-weight: bold;
}
.qty-input {
width: 40px;
text-align: center;
border: 1px solid var(--border-color);
}
</style>
</head>
<body>
<div class="container">
<div class="top-row">
<div class="panel">
<div class="panel-header">
<strong>Pferd:</strong>
<input type="text" placeholder="Kopfnummer oder Name">
<button>...</button>
<button>Leeren</button>
</div>
<div class="panel-content">
<div class="empty-state">Keine Ergebnisse</div>
</div>
<div class="footer-btns">
<button class="btn-primary">Neu</button>
<button disabled>Bearbeiten</button>
</div>
</div>
<div class="panel">
<div class="panel-header">
<strong>Reiter:</strong>
<input type="text" placeholder="Vorname und/oder Nachname">
<button>...</button>
<button>Leeren</button>
</div>
<div class="panel-content">
<div class="empty-state">Keine Ergebnisse</div>
</div>
<div class="footer-btns">
<button class="btn-primary">Neu</button>
<button disabled>Bearbeiten</button>
</div>
</div>
<div class="panel">
<div class="tabs">
<div class="tab active">VERKAUF</div>
<div class="tab">BUCHUNGEN</div>
</div>
<div class="panel-header" style="justify-content: space-between;">
<span>11 Artikel</span>
<div>
<a href="#" style="font-size: 11px; margin-right: 10px;">Rückgängig</a>
<a href="#" style="font-size: 11px; font-weight: bold;">Speichern</a>
</div>
</div>
<div class="panel-content">
<table>
<thead>
<tr>
<th>KNr</th>
<th>+/-</th>
<th>Menge</th>
<th>Buchungstext</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
<tr><td></td><td>+</td><td><input class="qty-input" value="0"></td><td>Belastung</td><td>0.00</td></tr>
<tr class="row-selected"><td></td><td>+</td><td><input class="qty-input" value="0"></td><td>Gutschrift</td><td>0.00</td></tr>
<tr><td></td><td>+</td><td><input class="qty-input" value="0"></td><td>Boxenpauschale</td><td>0.00</td></tr>
<tr><td></td><td>+</td><td><input class="qty-input" value="0"></td><td>Ansage</td><td>0.00</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="action-bar-center">
<button class="nav-btn">☰ Startliste</button>
<button class="nav-btn">🏆 Ergebnisse</button>
<button class="nav-btn">📄 Abrechnung</button>
</div>
<div class="bottom-row">
<div class="panel">
<div class="tabs">
<div class="tab active">REITER</div>
<div class="tab">PFERD</div>
<div class="tab">BEWERBE</div>
</div>
<div class="panel-header" style="justify-content: space-between; background: #fafafa;">
<span>0 Nennungen</span>
<div style="color: var(--primary-blue);">
<span style="margin-right: 10px; cursor: pointer;">Positionieren</span>
<span style="cursor: pointer;">Stornieren</span>
</div>
</div>
<div class="panel-content">
<table>
<thead>
<tr>
<th>Tag</th>
<th>Pl.</th>
<th>Bewerb</th>
<th>Bewerbsname</th>
<th>Bemerkung</th>
<th>Pferd</th>
</tr>
</thead>
</table>
<div class="empty-state" style="margin-top: 50px;">Keine Nennungen vorhanden</div>
</div>
</div>
<div class="panel">
<div class="panel-header" style="justify-content: space-between;">
<strong>Bewerbsübersicht</strong>
</div>
<div class="panel-header" style="justify-content: space-between; font-size: 11px;">
<span>12 Bewerbe</span>
<span style="color: var(--primary-blue);">Filtern</span>
</div>
<div class="panel-content">
<table>
<thead>
<tr>
<th>Tag</th>
<th>Pl.</th>
<th>Bew.</th>
<th>Beginn</th>
<th>Nenn.</th>
<th>Bewerbsname</th>
</tr>
</thead>
<tbody>
<tr><td>So</td><td>1</td><td><strong>1</strong></td><td>08:00</td><td>0</td><td>Dressurreiterprüfung Ratepass</td></tr>
<tr><td>So</td><td>1</td><td><strong>2</strong></td><td>08:20</td><td>0</td><td>Dressurreiterprüfung Katecnadel</td></tr>
<tr><td>So</td><td>1</td><td><strong>3</strong></td><td>08:40</td><td>0</td><td>Dressurreiterprüfung Idf.</td></tr>
<tr><td>So</td><td>1</td><td><strong>4</strong></td><td>09:00</td><td>0</td><td>Dressurprüfung Idf.</td></tr>
</tbody>
</table>
</div>
<div style="padding: 5px; text-align: center; font-size: 11px; color: #777;">
Bitte wählen Sie zuerst ein Pferd und einen Reiter aus
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@ -0,0 +1,47 @@
# 🧹 Curator Session Log (2026-04-01)
## Zusammenfassung
- Flow-Entscheidung bestätigt: Grüner Pfad aktiv, roter Pfad verworfen. "+ Neues Turnier" führt direkt zum Tab „STAMMDATEN“ v2 mit TurnierNr.-Gatekeeping.
- Keine Codeänderungen in dieser Sitzung; Build zuletzt grün. Entscheidungen und nächste Schritte dokumentiert.
## Beschlossene UI/Flow-Regeln
- Turnieranlage
- Einstieg: "+ Neues Turnier" → direkt „Turnier Detail v2“ Tab „STAMMDATEN“.
- Gatekeeping: 5stellige TurnierNr. eingeben + Bestätigungsdialog (danach immutable).
- Save-Enable-Matrix: aktiv nur wenn (Nr bestätigt ∧ ZNS geladen ∧ Datum gültig).
- ZNS-Status
- Panel immer sichtbar, zeigt Quelle, `payloadVersion`, Zeitstempel.
- „ImportLog“ Dialog mit den letzten 5 Einträgen (Erfolg/Fehler, Kurzmeldung).
- Kategorien & Pony
- Mehrfach-Kategorien wie vormittags vereinbart; Pony über KategorienSuffix „P“ (kein separater Switch).
- Kategorien-UI wird gruppiert (z. B. Dressur/Springen).
- Datum/Ort
- Datum im zulässigen Veranstaltungszeitraum; Hinweis: „Muss zwischen [vonbis] liegen“.
- Abweichender TurnierOrt: SoftWarnung (kein HardBlock).
- Branding
- Feld „Titel“ optional. DefaultVorschlag: „[Kategorien] [VereinOrt] [Bundesland]“ (Fallback über Veranstalterdaten).
- „TurnierLogo“ optional; Fallback = VeranstalterLogo.
## Veranstalter-Flow
- Nach „Schritt 2: Vereinsdaten bestätigen“ → Weiterleitung zum „VeranstalterProfil“.
- VeranstalterProfil: minimale Felder (LogoURL, Ansprechpartner, EMail, Telefon, Adresse), CTA „+ Neue Veranstaltung“.
- Von dort → VeranstaltungWizard Schritt 2 („Basisdaten“). Feld „VeranstaltungsLogo“ optional; Fallbacks: VeranstaltungsLogo → VeranstalterLogo → Default.
## Footer-Onboarding
- Online/OfflineStatus anzeigen.
- GeräteVerbindung (z. B. „RichterTurm“) anzeigen, klickbar für Details.
- ChatTrigger anzeigen, wenn mindestens ein weiteres Gerät verbunden ist.
## Nächste Schritte (ToDo)
- Routing final auf Stammdaten v2 festziehen; alte Pfade entfernen.
- SaveEnableMatrix implementieren; ZNSPanel inkl. ImportLog.
- KategorienUI konsolidieren und gruppieren; DefaultTitel generieren; OrtSoftwarnung.
- VeranstalterProfil & Übersicht finalisieren; CTAFlow prüfen.
- FooterOnboarding integrieren (Status, Geräte, ChatTrigger).
## Artefakte/Referenzen
- docs/06_Frontend/flow-wechsel.png (neuer Flow grüner Pfeil)
- docs/06_Frontend/flow-fehler.png (Bruchstellen im alten Flow)
- docs/99_Journal/2026-03-31_Session_Log_Event_First_Workflow.md
- docs/99_Journal/2026-03-30_Session_Log_ZNS_Documentation.md
- docs/99_Journal/2026-03-30_Session_Log_Masterdata_OETO_Consolidation.md

View File

@ -33,6 +33,13 @@ fun TurnierDetailScreen(
veranstaltungId: Long, veranstaltungId: Long,
turnierId: Long, turnierId: Long,
onBack: () -> Unit, onBack: () -> Unit,
eventVon: String? = null,
eventBis: String? = null,
eventOrt: String? = null,
veranstalterName: String? = null,
veranstalterOrt: String? = null,
veranstalterBundesland: String? = null,
veranstalterLogoUrl: String? = null,
) { ) {
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
@ -80,7 +87,16 @@ fun TurnierDetailScreen(
// Tab-Inhalte // Tab-Inhalte
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
when (selectedTab) { when (selectedTab) {
0 -> StammdatenTabContent(turnierId = turnierId) 0 -> StammdatenTabContent(
turnierId = turnierId,
eventVon = eventVon,
eventBis = eventBis,
eventOrt = eventOrt,
veranstalterName = veranstalterName,
veranstalterOrt = veranstalterOrt,
veranstalterBundesland = veranstalterBundesland,
veranstalterLogoUrl = veranstalterLogoUrl,
)
1 -> OrganisationTabContent() 1 -> OrganisationTabContent()
2 -> BewerbeTabContent() 2 -> BewerbeTabContent()
3 -> ArtikelTabContent() 3 -> ArtikelTabContent()

View File

@ -29,13 +29,26 @@ private val AccentBlue = Color(0xFF3B82F6)
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun StammdatenTabContent(turnierId: Long) { fun StammdatenTabContent(
turnierId: Long,
eventVon: String? = null,
eventBis: String? = null,
eventOrt: String? = null,
veranstalterName: String? = null,
veranstalterOrt: String? = null,
veranstalterBundesland: String? = null,
veranstalterLogoUrl: String? = null,
) {
// In einer echten App würden wir diese Daten aus einem ViewModel laden. // In einer echten App würden wir diese Daten aus einem ViewModel laden.
// Hier simulieren wir den State basierend auf den Anforderungen. // Hier simulieren wir den State basierend auf den Anforderungen.
var turnierNr by remember { mutableStateOf("") } var turnierNr by remember { mutableStateOf("") }
var nrConfirmed by remember { mutableStateOf(false) } var nrConfirmed by remember { mutableStateOf(false) }
var showNrConfirm by remember { mutableStateOf(false) }
var znsDataLoaded by remember { mutableStateOf(false) } var znsDataLoaded by remember { mutableStateOf(false) }
var znsPayloadVersion by remember { mutableStateOf<String?>(null) }
var znsImportedAt by remember { mutableStateOf<String?>(null) }
val znsImportHistory = remember { mutableStateListOf<Triple<String, String, Boolean>>() } // (source, payloadVersion, ok)
var typ by remember { mutableStateOf("ÖTO (National)") } var typ by remember { mutableStateOf("ÖTO (National)") }
val sparten = remember { mutableStateListOf<String>() } val sparten = remember { mutableStateListOf<String>() }
@ -48,9 +61,11 @@ fun StammdatenTabContent(turnierId: Long) {
var titel by remember { mutableStateOf("") } var titel by remember { mutableStateOf("") }
var subTitel by remember { mutableStateOf("") } var subTitel by remember { mutableStateOf("") }
var turnierLogoUrl by remember { mutableStateOf("") }
val sponsoren = remember { mutableStateListOf<String>() } val sponsoren = remember { mutableStateListOf<String>() }
var showZnsDialog by remember { mutableStateOf(false) } var showZnsDialog by remember { mutableStateOf(false) }
var showZnsLog by remember { mutableStateOf(false) }
// Hilfs-States für DatePicker // Hilfs-States für DatePicker
var showDatePickerVon by remember { mutableStateOf(false) } var showDatePickerVon by remember { mutableStateOf(false) }
@ -79,7 +94,7 @@ fun StammdatenTabContent(turnierId: Long) {
) )
if (!nrConfirmed) { if (!nrConfirmed) {
Button( Button(
onClick = { nrConfirmed = true }, onClick = { showNrConfirm = true },
enabled = turnierNr.length == 5, enabled = turnierNr.length == 5,
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue) colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue)
) { ) {
@ -88,9 +103,9 @@ fun StammdatenTabContent(turnierId: Long) {
} else { } else {
InputChip( InputChip(
selected = true, selected = true,
onClick = { nrConfirmed = false }, onClick = { },
label = { Text("Bestätigt") }, label = { Text("Bestätigt") },
trailingIcon = { Icon(Icons.Default.Edit, contentDescription = null, modifier = Modifier.size(16.dp)) } trailingIcon = { Icon(Icons.Default.Check, contentDescription = null, modifier = Modifier.size(16.dp)) }
) )
} }
} }
@ -108,11 +123,13 @@ fun StammdatenTabContent(turnierId: Long) {
FilterChip( FilterChip(
selected = typ == "ÖTO (National)", selected = typ == "ÖTO (National)",
onClick = { typ = "ÖTO (National)" }, onClick = { typ = "ÖTO (National)" },
enabled = nrConfirmed,
label = { Text("ÖTO (National)") } label = { Text("ÖTO (National)") }
) )
FilterChip( FilterChip(
selected = typ == "FEI (International)", selected = typ == "FEI (International)",
onClick = { typ = "FEI (International)" }, onClick = { typ = "FEI (International)" },
enabled = nrConfirmed,
label = { Text("FEI (International)") } label = { Text("FEI (International)") }
) )
} }
@ -123,16 +140,18 @@ fun StammdatenTabContent(turnierId: Long) {
Button( Button(
onClick = { showZnsDialog = true }, onClick = { showZnsDialog = true },
colors = ButtonDefaults.buttonColors(containerColor = AccentBlue) colors = ButtonDefaults.buttonColors(containerColor = AccentBlue)
, enabled = nrConfirmed
) { ) {
Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.CloudDownload, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Import via Internet") Text("Import via Internet")
} }
OutlinedButton(onClick = { showZnsDialog = true }) { OutlinedButton(onClick = { showZnsDialog = true }, enabled = nrConfirmed) {
Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Usb, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Import via USB") Text("Import via USB")
} }
TextButton(onClick = { showZnsLog = true }, enabled = nrConfirmed) { Text("Import-Log anzeigen…") }
} }
val znsStatusColor = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error val znsStatusColor = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
@ -149,6 +168,17 @@ fun StammdatenTabContent(turnierId: Long) {
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 13.sp fontSize = 13.sp
) )
if (znsDataLoaded) {
Spacer(Modifier.width(8.dp))
Text(
listOfNotNull(
znsPayloadVersion?.let { "Version: $it" },
znsImportedAt?.let { "Zeit: $it" },
).joinToString(""),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp
)
}
} }
} }
} }
@ -160,11 +190,13 @@ fun StammdatenTabContent(turnierId: Long) {
FilterChip( FilterChip(
selected = sparten.contains("Dressur"), selected = sparten.contains("Dressur"),
onClick = { if (sparten.contains("Dressur")) sparten.remove("Dressur") else sparten.add("Dressur") }, onClick = { if (sparten.contains("Dressur")) sparten.remove("Dressur") else sparten.add("Dressur") },
enabled = nrConfirmed,
label = { Text("Dressur") } label = { Text("Dressur") }
) )
FilterChip( FilterChip(
selected = sparten.contains("Springen"), selected = sparten.contains("Springen"),
onClick = { if (sparten.contains("Springen")) sparten.remove("Springen") else sparten.add("Springen") }, onClick = { if (sparten.contains("Springen")) sparten.remove("Springen") else sparten.add("Springen") },
enabled = nrConfirmed,
label = { Text("Springen") } label = { Text("Springen") }
) )
} }
@ -177,6 +209,7 @@ fun StammdatenTabContent(turnierId: Long) {
FilterChip( FilterChip(
selected = klassen.contains(k), selected = klassen.contains(k),
onClick = { if (klassen.contains(k)) klassen.remove(k) else klassen.add(k) }, onClick = { if (klassen.contains(k)) klassen.remove(k) else klassen.add(k) },
enabled = nrConfirmed,
label = { Text(k) } label = { Text(k) }
) )
} }
@ -197,64 +230,112 @@ fun StammdatenTabContent(turnierId: Long) {
if (suggested.isEmpty()) { if (suggested.isEmpty()) {
Text("Bitte Sparte und Klasse wählen", color = Color.Gray, fontSize = 13.sp) Text("Bitte Sparte und Klasse wählen", color = Color.Gray, fontSize = 13.sp)
} else { } else {
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { // Gruppiere nach Sparte (CDN/CSN)
suggested.forEach { c -> val grouped = suggested.groupBy { if (it.startsWith("CDN")) "Dressur" else "Springen" }
InputChip( grouped.forEach { (gruppe, eintraege) ->
selected = kat.contains(c), Text(gruppe, fontWeight = FontWeight.SemiBold, color = PrimaryBlue)
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) }, Spacer(Modifier.height(4.dp))
label = { Text(c) } FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
) eintraege.sorted().forEach { c ->
InputChip(
selected = kat.contains(c),
onClick = { if (kat.contains(c)) kat.remove(c) else kat.add(c) },
enabled = nrConfirmed,
label = { Text(c) }
)
}
} }
Spacer(Modifier.height(8.dp))
} }
} }
} }
FormRow("Zeitraum:") { FormRow("Zeitraum:") {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
val vonMod = if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerVon = true } else Modifier.width(160.dp)
OutlinedTextField( OutlinedTextField(
value = von, value = von,
onValueChange = {}, onValueChange = {},
label = { Text("Von") }, label = { Text("Von") },
modifier = Modifier.width(160.dp).clickable { showDatePickerVon = true }, modifier = vonMod,
readOnly = true, readOnly = true,
enabled = nrConfirmed,
trailingIcon = { Icon(Icons.Default.DateRange, null) } trailingIcon = { Icon(Icons.Default.DateRange, null) }
) )
Text("bis") Text("bis")
val bisMod = if (nrConfirmed) Modifier.width(160.dp).clickable { showDatePickerBis = true } else Modifier.width(160.dp)
OutlinedTextField( OutlinedTextField(
value = bis, value = bis,
onValueChange = {}, onValueChange = {},
label = { Text("Bis") }, label = { Text("Bis") },
modifier = Modifier.width(160.dp).clickable { showDatePickerBis = true }, modifier = bisMod,
readOnly = true, readOnly = true,
enabled = nrConfirmed,
trailingIcon = { Icon(Icons.Default.DateRange, null) } trailingIcon = { Icon(Icons.Default.DateRange, null) }
) )
} }
Text("Hinweis: Muss innerhalb des Veranstaltungs-Zeitraums liegen.", fontSize = 11.sp, color = Color.Gray) val rangeText = if (eventVon != null && eventBis != null) "Muss zwischen $eventVon $eventBis liegen." else "Muss innerhalb des Veranstaltungs-Zeitraums liegen."
} Text(rangeText, fontSize = 11.sp, color = Color.Gray)
FormRow("Ort:") {
OutlinedTextField(
value = ort,
onValueChange = { ort = it },
label = { Text("Austragungsort") },
modifier = Modifier.fillMaxWidth(),
supportingText = { Text("Muss mit Veranstaltungsort übereinstimmen.") }
)
} }
} }
// ── Branding (Schritt 3 Logik) ─────────────────────────────────────── // ── Branding (Schritt 3 Logik) ───────────────────────────────────────
SectionCard(title = "Turnier-Branding") { SectionCard(title = "Turnier-Branding") {
// Default-Titel-Vorschlag: [Kategorien] [Verein-Ort] [Bundesland]
val defaultTitle = remember(kat.size, veranstalterOrt, veranstalterBundesland) {
val cats = if (kat.isEmpty()) "" else kat.sorted().joinToString(" ")
listOfNotNull(cats.ifBlank { null },
listOfNotNull(veranstalterOrt, veranstalterBundesland).filter { it.isNotBlank() }.joinToString(" ")
.takeIf { it.isNotBlank() }
).joinToString(" ")
}
OutlinedTextField( OutlinedTextField(
value = titel, value = titel,
onValueChange = { titel = it }, onValueChange = { titel = it },
label = { Text("Titel") }, label = { Text("Titel") },
placeholder = { if (defaultTitle.isNotBlank()) Text(defaultTitle) },
enabled = nrConfirmed,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
OutlinedTextField( OutlinedTextField(
value = subTitel, value = subTitel,
onValueChange = { subTitel = it }, onValueChange = { subTitel = it },
label = { Text("Sub-Titel") }, label = { Text("Sub-Titel") },
enabled = nrConfirmed,
modifier = Modifier.fillMaxWidth()
)
// Ort im Branding-Bereich platzieren (mit Soft-Warnung bei Abweichung zum Veranstaltungsort)
FormRow("Ort:") {
OutlinedTextField(
value = ort,
onValueChange = { ort = it },
label = { Text("Austragungsort") },
enabled = nrConfirmed,
modifier = Modifier.fillMaxWidth(),
supportingText = {
if (eventOrt != null && ort.isNotBlank() && ort.trim() != eventOrt.trim()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Default.Warning, contentDescription = null, tint = Color(0xFFF59E0B), modifier = Modifier.size(14.dp))
Spacer(Modifier.width(4.dp))
Text("Abweichung zum Veranstaltungsort ($eventOrt) bitte prüfen.", color = Color(0xFFF59E0B))
}
} else {
Text("Muss mit Veranstaltungsort übereinstimmen.")
}
}
)
}
// Turnier-Logo mit Fallback auf Veranstalterlogo
OutlinedTextField(
value = turnierLogoUrl,
onValueChange = { turnierLogoUrl = it },
label = { Text("Turnier-Logo (URL/Pfad)") },
enabled = nrConfirmed,
supportingText = {
Text("Wenn leer: verwende Veranstalter-Logo${if (!veranstalterLogoUrl.isNullOrBlank()) " ($veranstalterLogoUrl)" else ""}.")
},
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@ -264,11 +345,12 @@ fun StammdatenTabContent(turnierId: Long) {
InputChip( InputChip(
selected = true, selected = true,
onClick = { sponsoren.remove(s) }, onClick = { sponsoren.remove(s) },
enabled = nrConfirmed,
label = { Text(s) }, label = { Text(s) },
trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(14.dp)) } trailingIcon = { Icon(Icons.Default.Close, null, modifier = Modifier.size(14.dp)) }
) )
} }
TextButton(onClick = { sponsoren.add("Neuer Sponsor") }) { TextButton(onClick = { sponsoren.add("Neuer Sponsor") }, enabled = nrConfirmed) {
Text("+ Hinzufügen") Text("+ Hinzufügen")
} }
} }
@ -276,10 +358,46 @@ fun StammdatenTabContent(turnierId: Long) {
} }
// ── Footer ────────────────────────────────────────────────────────── // ── Footer ──────────────────────────────────────────────────────────
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
// Save-Enable-Matrix (kleine Checkliste)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
AssistChip(onClick = {}, label = { Text("Nr bestätigt") }, leadingIcon = {
Icon(if (nrConfirmed) Icons.Default.Check else Icons.Default.Close, null, tint = if (nrConfirmed) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
})
AssistChip(onClick = {}, label = { Text("ZNS geladen") }, leadingIcon = {
Icon(if (znsDataLoaded) Icons.Default.Check else Icons.Default.Close, null, tint = if (znsDataLoaded) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
})
val dateOk = remember(von, bis, eventVon, eventBis) {
try {
if (eventVon == null || eventBis == null || von.isBlank()) true else {
val evV = LocalDate.parse(eventVon)
val evB = LocalDate.parse(eventBis)
val tV = LocalDate.parse(von)
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
}
} catch (e: Exception) { false }
}
AssistChip(onClick = {}, label = { Text("Datum gültig") }, leadingIcon = {
Icon(if (dateOk) Icons.Default.Check else Icons.Default.Close, null, tint = if (dateOk) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error)
})
}
Button( Button(
onClick = { /* Speichern */ }, onClick = { /* Speichern */ },
enabled = nrConfirmed && znsDataLoaded && kat.isNotEmpty() && von.isNotBlank() && titel.isNotBlank(), enabled = run {
val base = nrConfirmed && znsDataLoaded && kat.isNotEmpty() && von.isNotBlank()
val dateValid = try {
if (eventVon == null || eventBis == null || von.isBlank()) true else {
val evV = LocalDate.parse(eventVon)
val evB = LocalDate.parse(eventBis)
val tV = LocalDate.parse(von)
val tB = if (bis.isBlank()) tV else LocalDate.parse(bis)
!tV.isBefore(evV) && !tB.isAfter(evB) && !tB.isBefore(tV)
}
} catch (e: Exception) { false }
base && dateValid
},
colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue), colors = ButtonDefaults.buttonColors(containerColor = PrimaryBlue),
modifier = Modifier.padding(bottom = 24.dp) modifier = Modifier.padding(bottom = 24.dp)
) { ) {
@ -297,7 +415,13 @@ fun StammdatenTabContent(turnierId: Long) {
title = { Text("ZNS Import") }, title = { Text("ZNS Import") },
text = { Text("Simuliere ZNS-Stammdaten Import für Turnier #$turnierNr...") }, text = { Text("Simuliere ZNS-Stammdaten Import für Turnier #$turnierNr...") },
confirmButton = { confirmButton = {
TextButton(onClick = { znsDataLoaded = true; showZnsDialog = false }) { Text("Importieren") } TextButton(onClick = {
znsDataLoaded = true
znsPayloadVersion = "v2.4"
znsImportedAt = java.time.Instant.now().toString()
znsImportHistory.add(Triple("Internet/USB", znsPayloadVersion!!, true))
showZnsDialog = false
}) { Text("Importieren") }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showZnsDialog = false }) { Text("Abbrechen") } TextButton(onClick = { showZnsDialog = false }) { Text("Abbrechen") }
@ -305,6 +429,40 @@ fun StammdatenTabContent(turnierId: Long) {
) )
} }
if (showNrConfirm) {
AlertDialog(
onDismissRequest = { showNrConfirm = false },
title = { Text("Turnier-Nummer bestätigen?") },
text = { Text("Die Turnier-Nr. ist nach der Bestätigung nicht mehr änderbar.") },
confirmButton = {
TextButton(onClick = { nrConfirmed = true; showNrConfirm = false }) { Text("Ja, bestätigen") }
},
dismissButton = {
TextButton(onClick = { showNrConfirm = false }) { Text("Abbrechen") }
}
)
}
if (showZnsLog) {
AlertDialog(
onDismissRequest = { showZnsLog = false },
title = { Text("ZNS Import-Log (letzte 5)") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
if (znsImportHistory.isEmpty()) {
Text("Keine Einträge vorhanden.", color = Color.Gray)
} else {
znsImportHistory.takeLast(5).asReversed().forEach { (src, ver, ok) ->
val c = if (ok) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error
Text("$src Version $ver ${if (ok) "OK" else "Fehler"}", color = c, fontSize = 13.sp)
}
}
}
},
confirmButton = { TextButton(onClick = { showZnsLog = false }) { Text("Schließen") } }
)
}
if (showDatePickerVon) { if (showDatePickerVon) {
val state = rememberDatePickerState() val state = rememberDatePickerState()
DatePickerDialog( DatePickerDialog(

View File

@ -5,10 +5,17 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.Devices
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -57,11 +64,14 @@ fun DesktopMainLayout(
onNavigate = onNavigate, onNavigate = onNavigate,
onLogout = onLogout, onLogout = onLogout,
) )
Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
DesktopContentArea( Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
currentScreen = currentScreen, DesktopContentArea(
onNavigate = onNavigate, currentScreen = currentScreen,
) onNavigate = onNavigate,
)
}
DesktopFooterBar()
} }
} }
} }
@ -259,6 +269,20 @@ private fun DesktopTopBar(
} }
} }
// Hilfsfunktion: OEPS-Bundeslandcode → Abkürzung
private fun mapOepsToBundesland(code: String): String = when (code.uppercase()) {
"OOE" -> ""
"NOE" -> ""
"ST" -> "Stmk."
"W" -> "Wien"
"BGLD", "B" -> "Bgld."
"K" -> "Ktn."
"S" -> "Sbg."
"T" -> "Tirol"
"V" -> "Vbg."
else -> code
}
@Composable @Composable
private fun BreadcrumbSeparator() { private fun BreadcrumbSeparator() {
Text( Text(
@ -344,7 +368,8 @@ private fun DesktopContentArea(
if (vId == 0L) onNavigate(AppScreen.Veranstaltungen) if (vId == 0L) onNavigate(AppScreen.Veranstaltungen)
else onNavigate(AppScreen.VeranstalterDetail(vId)) else onNavigate(AppScreen.VeranstalterDetail(vId))
}, },
onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) } onSaved = { evtId, finalVId -> onNavigate(AppScreen.VeranstaltungUebersicht(finalVId, evtId)) },
onVeranstalterCreated = { newVId -> onNavigate(AppScreen.VeranstalterDetail(newVId)) }
) )
} }
is AppScreen.VeranstaltungUebersicht -> { is AppScreen.VeranstaltungUebersicht -> {
@ -365,7 +390,20 @@ private fun DesktopContentArea(
veranstalterId = vId, veranstalterId = vId,
veranstaltungId = evtId, veranstaltungId = evtId,
onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) }, onBack = { onNavigate(AppScreen.VeranstalterDetail(vId)) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(evtId)) }, onTurnierNeu = {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(vId).firstOrNull { it.id == evtId }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(evtId)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.TurnierV2(
id = newId,
veranstaltungId = evtId,
turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis,
)
at.mocode.desktop.v2.TurnierStoreV2.add(evtId, draft)
onNavigate(AppScreen.TurnierDetail(evtId, newId))
},
onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) }, onTurnierOpen = { tId -> onNavigate(AppScreen.TurnierDetail(evtId, tId)) },
) )
} }
@ -375,7 +413,23 @@ private fun DesktopContentArea(
is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen( is AppScreen.VeranstaltungDetail -> VeranstaltungDetailScreen(
veranstaltungId = currentScreen.id, veranstaltungId = currentScreen.id,
onBack = { onNavigate(AppScreen.Veranstaltungen) }, onBack = { onNavigate(AppScreen.Veranstaltungen) },
onTurnierNeu = { onNavigate(AppScreen.TurnierNeu(currentScreen.id)) }, onTurnierNeu = {
val v = at.mocode.desktop.v2.StoreV2.vereine.firstOrNull { vv ->
at.mocode.desktop.v2.StoreV2.eventsFor(vv.id).any { it.id == currentScreen.id }
}
val veranstaltung = v?.let { at.mocode.desktop.v2.StoreV2.eventsFor(it.id).firstOrNull { e -> e.id == currentScreen.id } }
val list = at.mocode.desktop.v2.TurnierStoreV2.list(currentScreen.id)
val newId = (list.maxOfOrNull { it.id } ?: 0L) + 1L
val draft = at.mocode.desktop.v2.TurnierV2(
id = newId,
veranstaltungId = currentScreen.id,
turnierNr = 0,
datumVon = veranstaltung?.datumVon ?: "",
datumBis = veranstaltung?.datumBis,
)
at.mocode.desktop.v2.TurnierStoreV2.add(currentScreen.id, draft)
onNavigate(AppScreen.TurnierDetail(currentScreen.id, newId))
},
onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) }, onTurnierOeffnen = { tid -> onNavigate(AppScreen.TurnierDetail(currentScreen.id, tid)) },
) )
is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen( is AppScreen.VeranstaltungNeu -> VeranstaltungNeuScreen(
@ -395,10 +449,20 @@ private fun DesktopContentArea(
onBack = { onNavigate(AppScreen.Veranstaltungen) } onBack = { onNavigate(AppScreen.Veranstaltungen) }
) )
} else { } else {
val veranstaltung = at.mocode.desktop.v2.StoreV2.eventsFor(parent.id).firstOrNull { it.id == evtId }
val blCode = parent.oepsNummer.split("-").getOrNull(1) ?: ""
val bundesland = mapOepsToBundesland(blCode)
TurnierDetailScreen( TurnierDetailScreen(
veranstaltungId = evtId, veranstaltungId = evtId,
turnierId = currentScreen.turnierId, turnierId = currentScreen.turnierId,
onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) }, onBack = { onNavigate(AppScreen.VeranstaltungUebersicht(parent.id, evtId)) },
eventVon = veranstaltung?.datumVon,
eventBis = veranstaltung?.datumBis,
eventOrt = veranstaltung?.ort,
veranstalterName = parent.name,
veranstalterOrt = parent.ort,
veranstalterBundesland = bundesland,
veranstalterLogoUrl = veranstaltung?.logoUrl,
) )
} }
} }
@ -455,3 +519,48 @@ private fun DesktopContentArea(
) )
} }
} }
@Composable
private fun DesktopFooterBar() {
// Stub-Status für MVP
val online = remember { mutableStateOf(true) }
val deviceConnected = remember { mutableStateOf(true) }
val deviceName = "Richter-Turm"
Row(
modifier = Modifier
.fillMaxWidth()
.height(36.dp)
.background(Color(0xFFF3F4F6))
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = if (online.value) Icons.Filled.Wifi else Icons.Filled.WifiOff,
contentDescription = null,
tint = if (online.value) Color(0xFF059669) else Color(0xFFDC2626)
)
Spacer(Modifier.width(6.dp))
Text(if (online.value) "Online" else "Offline", color = Color(0xFF374151), fontSize = 12.sp)
Spacer(Modifier.width(16.dp))
Icon(Icons.Filled.Devices, contentDescription = null, tint = if (deviceConnected.value) Color(0xFF2563EB) else Color(0xFF9CA3AF))
Spacer(Modifier.width(6.dp))
Text(
if (deviceConnected.value) "Verbunden: $deviceName" else "Kein Gerät verbunden",
color = Color(0xFF374151),
fontSize = 12.sp
)
}
Row(verticalAlignment = Alignment.CenterVertically) {
if (deviceConnected.value) {
OutlinedButton(onClick = { /* öffne Chat-Panel */ }, contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp)) {
Icon(Icons.Filled.Chat, contentDescription = null, tint = Color(0xFF2563EB))
Spacer(Modifier.width(6.dp))
Text("Chat", color = Color(0xFF2563EB), fontSize = 12.sp)
}
}
}
}
}

View File

@ -125,6 +125,57 @@ fun VeranstalterDetailV2(
Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") } Button(onClick = onNeuVeranstaltung) { Text("+ Neue Veranstaltung") }
} }
// Profil-Bereich (Logo URL, Ansprechpartner, Kontakt, Adresse)
val verein = remember(veranstalterId) { StoreV2.vereine.firstOrNull { it.id == veranstalterId } }
if (verein != null) {
Card {
Column(Modifier.fillMaxWidth().padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("VeranstalterProfil", style = MaterialTheme.typography.titleMedium)
OutlinedTextField(
value = verein.logoUrl ?: "",
onValueChange = { verein.logoUrl = it.ifBlank { null } },
label = { Text("LogoURL (optional)") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = verein.ansprechpartner ?: "",
onValueChange = { verein.ansprechpartner = it.ifBlank { null } },
label = { Text("Ansprechpartner (optional)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = verein.telefon ?: "",
onValueChange = { verein.telefon = it.ifBlank { null } },
label = { Text("Telefon (optional)") },
modifier = Modifier.weight(1f)
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = verein.email ?: "",
onValueChange = { verein.email = it.ifBlank { null } },
label = { Text("EMail (optional)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = verein.oepsNummer,
onValueChange = { verein.oepsNummer = it },
label = { Text("OEPSNummer") },
modifier = Modifier.weight(1f)
)
}
OutlinedTextField(
value = verein.adresse ?: "",
onValueChange = { verein.adresse = it.ifBlank { null } },
label = { Text("Adresse (optional)") },
modifier = Modifier.fillMaxWidth(),
minLines = 2
)
}
}
}
val events = StoreV2.eventsFor(veranstalterId) val events = StoreV2.eventsFor(veranstalterId)
if (events.isEmpty()) Text("Noch keine Veranstaltungen angelegt.", color = Color(0xFF6B7280)) if (events.isEmpty()) Text("Noch keine Veranstaltungen angelegt.", color = Color(0xFF6B7280))

View File

@ -5,9 +5,15 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
data class Verein( data class Verein(
val id: Long, val id: Long,
val name: String, var name: String,
val oepsNummer: String, var oepsNummer: String,
val ort: String, var ort: String,
// Profil-Felder (minimal laut Abstimmung)
var logoUrl: String? = null,
var ansprechpartner: String? = null,
var email: String? = null,
var telefon: String? = null,
var adresse: String? = null,
) )
data class VeranstaltungV2( data class VeranstaltungV2(

View File

@ -297,6 +297,7 @@ fun VeranstaltungKonfigV2(
veranstalterId: Long = 0, veranstalterId: Long = 0,
onBack: () -> Unit, onBack: () -> Unit,
onSaved: (Long, Long) -> Unit, // eventId, veranstalterId onSaved: (Long, Long) -> Unit, // eventId, veranstalterId
onVeranstalterCreated: (Long) -> Unit = {}, // Neuer Flow: nach Vereinsanlage ins Profil
) { ) {
DesktopThemeV2 { DesktopThemeV2 {
var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) } var currentStep by remember { mutableStateOf(if (veranstalterId == 0L) 1 else 2) }
@ -466,9 +467,9 @@ fun VeranstaltungKonfigV2(
VeranstalterAnlegenWizard( VeranstalterAnlegenWizard(
onCancel = { showVereinNeu = false }, onCancel = { showVereinNeu = false },
onVereinCreated = { newId -> onVereinCreated = { newId ->
selectedVereinId = newId // Neuer gewünschter Flow: nach Schritt 2 ins VeranstalterProfil wechseln
showVereinNeu = false showVereinNeu = false
currentStep = 2 onVeranstalterCreated(newId)
} }
) )
} }