chore: implementiere Logo-Upload-Zone mit Base64-Unterstützung, verbessere Vereinsverwaltung mit kompakten Feldern und nutzerspezifischen Uploadoptionen, optimiere Desktop-UX und Navigation

This commit is contained in:
2026-04-20 01:20:16 +02:00
parent dfaa2e8545
commit 85ac1cae9c
12 changed files with 485 additions and 70 deletions
@@ -0,0 +1,197 @@
package at.mocode.frontend.core.designsystem.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarMonth
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.unit.dp
import androidx.compose.ui.window.Dialog
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
/**
* Ein einfacher DatePicker-Dialog für Compose Desktop.
* Da Material3 DatePicker unter Desktop teils Probleme macht oder zu groß ist,
* nutzen wir hier eine kompakte Eigenimplementierung.
*/
@Composable
fun MsDatePickerField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
isError: Boolean = false,
errorMessage: String? = null
) {
var showDialog by remember { mutableStateOf(false) }
Box(modifier = modifier) {
MsTextField(
value = value,
onValueChange = { /* Schreibgeschützt via Dialog */ },
label = label,
placeholder = "YYYY-MM-DD",
readOnly = true,
isError = isError,
errorMessage = errorMessage,
trailingIcon = Icons.Default.CalendarMonth,
onTrailingIconClick = { showDialog = true },
modifier = Modifier.clickable { showDialog = true }
)
if (showDialog) {
MsDatePickerDialog(
initialDate = value,
onDismiss = { showDialog = false },
onDateSelected = {
onValueChange(it)
showDialog = false
}
)
}
}
}
@Composable
fun MsDatePickerDialog(
initialDate: String,
onDismiss: () -> Unit,
onDateSelected: (String) -> Unit
) {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val parsedDate = try {
LocalDate.parse(initialDate)
} catch (_: Exception) {
now
}
var currentMonth by remember { mutableStateOf(parsedDate.month) }
var currentYear by remember { mutableStateOf(parsedDate.year) }
Dialog(onDismissRequest = onDismiss) {
Card(
modifier = Modifier.width(300.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(Modifier.padding(16.dp)) {
// Header: Monat/Jahr Auswahl
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
if (currentMonth == Month.JANUARY) {
currentMonth = Month.DECEMBER
currentYear--
} else {
val months = Month.entries
currentMonth = months[currentMonth.ordinal - 1]
}
}) {
Text("<")
}
Text(
"${currentMonth.name} $currentYear",
style = MaterialTheme.typography.titleMedium
)
IconButton(onClick = {
if (currentMonth == Month.DECEMBER) {
currentMonth = Month.JANUARY
currentYear++
} else {
val months = Month.entries
currentMonth = months[currentMonth.ordinal + 1]
}
}) {
Text(">")
}
}
Spacer(Modifier.height(8.dp))
// Kalender-Grid
val daysInMonth = getDaysInMonth(currentMonth, currentYear)
val firstDayOfWeek = LocalDate(currentYear, currentMonth, 1).dayOfWeek.ordinal // 0=Monday
Row(Modifier.fillMaxWidth()) {
listOf("Mo", "Di", "Mi", "Do", "Fr", "Sa", "So").forEach {
Text(
it,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.labelSmall,
color = Color.Gray,
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
}
val totalSlots = 42 // 6 Wochen
Column {
for (week in 0 until 6) {
Row(Modifier.fillMaxWidth()) {
for (day in 0 until 7) {
val slotIndex = week * 7 + day
val dayNum = slotIndex - firstDayOfWeek + 1
if (dayNum in 1..daysInMonth) {
val isSelected = parsedDate.day == dayNum &&
parsedDate.month == currentMonth &&
parsedDate.year == currentYear
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f)
.padding(2.dp)
.background(
if (isSelected) MaterialTheme.colorScheme.primary
else Color.Transparent,
MaterialTheme.shapes.small
)
.clickable {
val selected = LocalDate(currentYear, currentMonth, dayNum)
onDateSelected(selected.toString())
},
contentAlignment = Alignment.Center
) {
Text(
dayNum.toString(),
style = MaterialTheme.typography.bodySmall,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onSurface
)
}
} else {
Spacer(Modifier.weight(1f).aspectRatio(1f))
}
}
}
}
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = onDismiss) { Text("Abbrechen") }
}
}
}
}
}
private fun getDaysInMonth(month: Month, year: Int): Int {
return when (month) {
Month.FEBRUARY -> if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) 29 else 28
Month.APRIL, Month.JUNE, Month.SEPTEMBER, Month.NOVEMBER -> 30
else -> 31
}
}