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
|
||||
allRules = 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
|
||||
}
|
||||
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",
|
||||
"ABRECHNUNG",
|
||||
"NENNUNGEN",
|
||||
"ZEITPLAN",
|
||||
"STARTLISTEN",
|
||||
"ERGEBNISLISTEN",
|
||||
)
|
||||
|
|
@ -105,8 +106,9 @@ fun TurnierDetailScreen(
|
|||
3 -> ArtikelTabContent()
|
||||
4 -> AbrechnungTabContent(veranstaltungId = veranstaltungId)
|
||||
5 -> NennungenTabContent(onAbrechnungClick = { selectedTab = 4 })
|
||||
6 -> StartlistenTabContent()
|
||||
7 -> ErgebnislistenTabContent()
|
||||
6 -> ZeitplanTabContent(turnierId = turnierId)
|
||||
7 -> StartlistenTabContent()
|
||||
8 -> ErgebnislistenTabContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,4 +118,5 @@ fun TurnierDetailScreen(
|
|||
// TurnierBewerbeTab.kt → BewerbeTabContent()
|
||||
// TurnierNennungenTab.kt → NennungenTabContent()
|
||||
// TurnierStartlistenTab.kt → StartlistenTabContent()
|
||||
// TurnierZeitplanTab.kt → ZeitplanTabContent()
|
||||
// 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")
|
||||
|
||||
// === BACKEND - SERVICES ===
|
||||
|
||||
// --- ENTRIES (Nennungen) ---
|
||||
include(":backend:services:entries:entries-api")
|
||||
include(":backend:services:entries:entries-domain")
|
||||
|
|
@ -95,7 +96,7 @@ include(":backend:services:masterdata:masterdata-infrastructure")
|
|||
include(":backend:services:masterdata:masterdata-service")
|
||||
|
||||
// --- 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-service")
|
||||
|
||||
|
|
@ -131,7 +132,6 @@ include(":frontend:core:local-db")
|
|||
include(":frontend:core:sync")
|
||||
|
||||
// --- FEATURES ---
|
||||
// include(":frontend:features:members-feature")
|
||||
include(":frontend:features:ping-feature")
|
||||
include(":frontend:features:nennung-feature")
|
||||
include(":frontend:features:zns-import-feature")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user