Implement "Zeitplan" feature in tournament details: add TurnierZeitplanTab.kt, update navigation, and integrate visual scheduling with drag-and-drop support. Relocate Detekt configuration.
This commit is contained in:
parent
b91d1953a4
commit
52bc8f3fbe
|
|
@ -171,7 +171,7 @@ subprojects {
|
||||||
buildUponDefaultConfig = true
|
buildUponDefaultConfig = true
|
||||||
allRules = false
|
allRules = false
|
||||||
autoCorrect = false
|
autoCorrect = false
|
||||||
config.setFrom(files(rootProject.file("config/detekt/detekt.yml")))
|
config.setFrom(files(rootProject.file("config/quality/detekt/detekt.yml")))
|
||||||
basePath = rootDir.absolutePath
|
basePath = rootDir.absolutePath
|
||||||
}
|
}
|
||||||
tasks.withType<Detekt>().configureEach {
|
tasks.withType<Detekt>().configureEach {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 🧹 Curator Log - 2026-04-11
|
||||||
|
|
||||||
|
## 📅 Session Info
|
||||||
|
- **Datum:** 2026-04-11
|
||||||
|
- **Agenten:** 🏗️ Lead Architect, 👷 Backend Developer, 🎨 Frontend Expert, 🧹 Curator
|
||||||
|
- **Fokus:** Implementierung Zeitplan-Optimierung (Frontend Prototyp)
|
||||||
|
|
||||||
|
## 🏗️ Architektur-Entscheidungen
|
||||||
|
- **Komponente:** `TurnierZeitplanTab.kt` wurde als zentraler Ort für die visuelle Zeitplanung geschaffen.
|
||||||
|
- **Layout:** 14-Stunden-Raster (07:00 - 21:00) mit feststehender Zeitachse und scrollbarem Gitter.
|
||||||
|
- **Interaktion:** Vertikales Drag & Drop mit 5-Minuten-Snapping zur präzisen Planung.
|
||||||
|
|
||||||
|
## 👷 Backend/Integration (Status)
|
||||||
|
- **Backend:** Logik in `BewerbService` bereits vorhanden (vorherige Sessions).
|
||||||
|
- **Frontend-Store:** Aktuell noch Mock-Daten (`ZeitplanItemUi`), Anbindung an `BewerbViewModel` steht noch aus.
|
||||||
|
|
||||||
|
## 🎨 Frontend (Details)
|
||||||
|
- **Modul:** `frontend:features:turnier-feature`
|
||||||
|
- **Datei:** `TurnierZeitplanTab.kt` (Neu)
|
||||||
|
- **Anpassung:** `TurnierDetailScreen.kt` um Tab "ZEITPLAN" erweitert.
|
||||||
|
|
||||||
|
## 🧹 Curator Status & Cleanup
|
||||||
|
- ✅ Neue UI-Komponente erstellt und syntaktisch korrigiert.
|
||||||
|
- ✅ Navigation im Turnier-Detail angepasst.
|
||||||
|
- 📂 Nächster Schritt: Mapping von `BewerbUiModel` auf `ZeitplanItemUi`.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Erstellt durch den Curator.*
|
||||||
|
|
@ -56,6 +56,7 @@ fun TurnierDetailScreen(
|
||||||
"ARTIKEL",
|
"ARTIKEL",
|
||||||
"ABRECHNUNG",
|
"ABRECHNUNG",
|
||||||
"NENNUNGEN",
|
"NENNUNGEN",
|
||||||
|
"ZEITPLAN",
|
||||||
"STARTLISTEN",
|
"STARTLISTEN",
|
||||||
"ERGEBNISLISTEN",
|
"ERGEBNISLISTEN",
|
||||||
)
|
)
|
||||||
|
|
@ -105,8 +106,9 @@ fun TurnierDetailScreen(
|
||||||
3 -> ArtikelTabContent()
|
3 -> ArtikelTabContent()
|
||||||
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
|
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
|
||||||
5 -> NennungenTabContent(onAbrechnungClick = { selectedTab = 4 })
|
5 -> NennungenTabContent(onAbrechnungClick = { selectedTab = 4 })
|
||||||
6 -> StartlistenTabContent()
|
6 -> ZeitplanTabContent(turnierId = turnierId)
|
||||||
7 -> ErgebnislistenTabContent()
|
7 -> StartlistenTabContent()
|
||||||
|
8 -> ErgebnislistenTabContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,4 +118,5 @@ fun TurnierDetailScreen(
|
||||||
// TurnierBewerbeTab.kt → BewerbeTabContent()
|
// TurnierBewerbeTab.kt → BewerbeTabContent()
|
||||||
// TurnierNennungenTab.kt → NennungenTabContent()
|
// TurnierNennungenTab.kt → NennungenTabContent()
|
||||||
// TurnierStartlistenTab.kt → StartlistenTabContent()
|
// TurnierStartlistenTab.kt → StartlistenTabContent()
|
||||||
|
// TurnierZeitplanTab.kt → ZeitplanTabContent()
|
||||||
// TurnierErgebnislistenTab.kt → ErgebnislistenTabContent()
|
// TurnierErgebnislistenTab.kt → ErgebnislistenTabContent()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
package at.mocode.turnier.feature.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
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.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private val ZeitplanBlue = Color(0xFF1E3A8A)
|
||||||
|
private val ZeitplanBg = Color(0xFFF8FAFC)
|
||||||
|
private val SlotBorder = Color(0xFFE2E8F0)
|
||||||
|
private val HourLabelColor = Color(0xFF64748B)
|
||||||
|
|
||||||
|
// Konfiguration für den Zeitstrahl
|
||||||
|
private const val START_HOUR = 7
|
||||||
|
private const val END_HOUR = 20
|
||||||
|
private val HOUR_HEIGHT = 80.dp
|
||||||
|
private val MINUTE_HEIGHT = HOUR_HEIGHT / 60
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZEITPLAN-Tab gemäß Konzept „Zeitplan-Optimierung“.
|
||||||
|
*
|
||||||
|
* Visuelle Kalender-Ansicht mit Drag & Drop Support.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ZeitplanTabContent(
|
||||||
|
turnierId: Long
|
||||||
|
) {
|
||||||
|
var items by remember { mutableStateOf(sampleZeitplanItems()) }
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize().background(ZeitplanBg)) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// Header / Toolbar
|
||||||
|
ZeitplanToolbar()
|
||||||
|
|
||||||
|
Row(modifier = Modifier.weight(1f)) {
|
||||||
|
// Zeit-Achse (feststehend)
|
||||||
|
ZeitAchse()
|
||||||
|
|
||||||
|
// Content (scrollbar)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
// Hintergrund-Gitter
|
||||||
|
ZeitplanGitter()
|
||||||
|
|
||||||
|
// Bewerbe / Blöcke
|
||||||
|
items.forEachIndexed { index, item ->
|
||||||
|
DraggableBewerbBox(
|
||||||
|
item = item,
|
||||||
|
onPositionChange = { newMinutes ->
|
||||||
|
items = items.toMutableList().apply {
|
||||||
|
this[index] = item.copy(startMinutes = newMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ZeitplanToolbar() {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Text("Zeitplan-Optimierung", fontWeight = FontWeight.Bold, fontSize = 16.sp, color = ZeitplanBlue)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
// Platz-Filter (Mock)
|
||||||
|
Text("Platz:", fontSize = 13.sp)
|
||||||
|
AssistChip(onClick = {}, label = { Text("Hauptplatz") })
|
||||||
|
AssistChip(onClick = {}, label = { Text("Viereck 1") }, leadingIcon = { Text("✓", fontSize = 12.sp) })
|
||||||
|
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { /* Speichern */ },
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = ZeitplanBlue)
|
||||||
|
) {
|
||||||
|
Text("Änderungen speichern", fontSize = 13.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ZeitAchse() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(60.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(Color.White)
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxHeight().width(59.dp).background(Color.White)) {
|
||||||
|
Column {
|
||||||
|
for (hour in START_HOUR..END_HOUR) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.height(HOUR_HEIGHT).fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${hour.toString().padStart(2, '0')}:00",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = HourLabelColor,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ZeitplanGitter() {
|
||||||
|
Column {
|
||||||
|
for (hour in START_HOUR..END_HOUR) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(HOUR_HEIGHT)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter), color = SlotBorder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DraggableBewerbBox(
|
||||||
|
item: ZeitplanItemUi,
|
||||||
|
onPositionChange: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
// Berechnung der Position basierend auf den Startminuten seit START_HOUR
|
||||||
|
val relativeMinutes = item.startMinutes - (START_HOUR * 60)
|
||||||
|
val topOffset = (relativeMinutes * MINUTE_HEIGHT.value).dp
|
||||||
|
val height = (item.durationMinutes * MINUTE_HEIGHT.value).dp
|
||||||
|
|
||||||
|
var offsetX by remember { mutableStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableStateOf(0f) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(y = topOffset)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(height)
|
||||||
|
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(item.color.copy(alpha = 0.15f))
|
||||||
|
.border(1.dp, item.color, RoundedCornerShape(6.dp))
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragEnd = {
|
||||||
|
// Snapping auf 5 Minuten Intervalle
|
||||||
|
val movedMinutes = (offsetY / MINUTE_HEIGHT.toPx()).roundToInt()
|
||||||
|
val newTotalMinutes = item.startMinutes + movedMinutes
|
||||||
|
val snappedMinutes = (newTotalMinutes / 5) * 5
|
||||||
|
|
||||||
|
onPositionChange(snappedMinutes)
|
||||||
|
offsetX = 0f
|
||||||
|
offsetY = 0f
|
||||||
|
},
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
// Nur vertikales Dragging für den Zeitplan vorerst
|
||||||
|
offsetY += dragAmount.y
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = item.timeString,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = item.color
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Bewerb ${item.nummer}",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = Color.Black
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = item.name,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
color = Color.DarkGray
|
||||||
|
)
|
||||||
|
if (item.hasConflict) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.End),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("⚠️", fontSize = 12.sp)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text("Konflikt", fontSize = 10.sp, color = Color.Red, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ZeitplanItemUi(
|
||||||
|
val id: Long,
|
||||||
|
val nummer: Int,
|
||||||
|
val name: String,
|
||||||
|
val startMinutes: Int, // Minuten seit 00:00
|
||||||
|
val durationMinutes: Int,
|
||||||
|
val color: Color = ZeitplanBlue,
|
||||||
|
val hasConflict: Boolean = false
|
||||||
|
) {
|
||||||
|
val timeString: String
|
||||||
|
get() {
|
||||||
|
val h = startMinutes / 60
|
||||||
|
val m = startMinutes % 60
|
||||||
|
return "${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sampleZeitplanItems() = listOf(
|
||||||
|
ZeitplanItemUi(1, 1, "Dressurreiterprüfung Reiterpass", 8 * 60, 45),
|
||||||
|
ZeitplanItemUi(2, 2, "Dressurreiterprüfung Reitenadel", 8 * 60 + 50, 60, hasConflict = true),
|
||||||
|
ZeitplanItemUi(3, 3, "Dressurprüfung Kl. A (Aufgabe A2)", 10 * 60 + 30, 90, color = Color(0xFF059669)),
|
||||||
|
ZeitplanItemUi(4, 4, "Mittagspause", 12 * 60 + 30, 45, color = Color(0xFFD97706)),
|
||||||
|
ZeitplanItemUi(5, 5, "Dressurreiterprüfung Kl. L", 13 * 60 + 30, 120, color = Color(0xFF7C3AED)),
|
||||||
|
)
|
||||||
|
|
@ -70,6 +70,7 @@ include(":backend:infrastructure:security")
|
||||||
include(":backend:infrastructure:zns-importer")
|
include(":backend:infrastructure:zns-importer")
|
||||||
|
|
||||||
// === BACKEND - SERVICES ===
|
// === BACKEND - SERVICES ===
|
||||||
|
|
||||||
// --- ENTRIES (Nennungen) ---
|
// --- ENTRIES (Nennungen) ---
|
||||||
include(":backend:services:entries:entries-api")
|
include(":backend:services:entries:entries-api")
|
||||||
include(":backend:services:entries:entries-domain")
|
include(":backend:services:entries:entries-domain")
|
||||||
|
|
@ -95,7 +96,7 @@ include(":backend:services:masterdata:masterdata-infrastructure")
|
||||||
include(":backend:services:masterdata:masterdata-service")
|
include(":backend:services:masterdata:masterdata-service")
|
||||||
|
|
||||||
// --- BILLING (Kassa, Zahlungen & Rechnungen) ---
|
// --- BILLING (Kassa, Zahlungen & Rechnungen) ---
|
||||||
include(":backend:services:billing:billing-api")
|
// include(":backend:services:billing:billing-api")
|
||||||
include(":backend:services:billing:billing-domain")
|
include(":backend:services:billing:billing-domain")
|
||||||
include(":backend:services:billing:billing-service")
|
include(":backend:services:billing:billing-service")
|
||||||
|
|
||||||
|
|
@ -131,7 +132,6 @@ include(":frontend:core:local-db")
|
||||||
include(":frontend:core:sync")
|
include(":frontend:core:sync")
|
||||||
|
|
||||||
// --- FEATURES ---
|
// --- FEATURES ---
|
||||||
// include(":frontend:features:members-feature")
|
|
||||||
include(":frontend:features:ping-feature")
|
include(":frontend:features:ping-feature")
|
||||||
include(":frontend:features:nennung-feature")
|
include(":frontend:features:nennung-feature")
|
||||||
include(":frontend:features:zns-import-feature")
|
include(":frontend:features:zns-import-feature")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user