Increment version code and name to 4 and 0.8.0-alpha3 respectively. Update SickEntryDto to change sickTypeId from Int to String. Enhance WeekOverviewDto and WeekDayDto with new fields for non-working details. Refactor TimeClockApp and AdminScreens for improved state management and UI updates. Introduce new dropdown components for selecting year and title type in SettingsScreens. Update mock data for consistency in status display.
This commit is contained in:
@@ -23,8 +23,8 @@ android {
|
||||
applicationId = "de.tsschulz.timeclock"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 3
|
||||
versionName = "0.8.0-alpha2"
|
||||
versionCode = 4
|
||||
versionName = "0.8.0-alpha3"
|
||||
buildConfigField("String", "API_BASE_URL", "\"${apiBaseUrl.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -24,7 +24,7 @@ data class SickEntryDto(
|
||||
val id: String,
|
||||
val startDate: String? = null,
|
||||
val endDate: String? = null,
|
||||
val sickTypeId: Int? = null,
|
||||
val sickTypeId: String? = null,
|
||||
val sickTypeName: String? = null,
|
||||
)
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ data class WeekOverviewDto(
|
||||
val weekEnd: String? = null,
|
||||
val weekTotal: String? = null,
|
||||
val totalAll: String? = null,
|
||||
val nonWorkingTotal: String? = null,
|
||||
val nonWorkingDays: Int = 0,
|
||||
val nonWorkingDetails: List<NonWorkingDetailDto> = emptyList(),
|
||||
val days: List<WeekDayDto> = emptyList(),
|
||||
)
|
||||
|
||||
@@ -88,6 +91,9 @@ data class WeekDayDto(
|
||||
val workTime: String? = null,
|
||||
val totalWorkTime: String? = null,
|
||||
val netWorkTime: String? = null,
|
||||
val holiday: WeekDayHolidayDto? = null,
|
||||
val vacation: WeekDayVacationDto? = null,
|
||||
val sick: WeekDaySickDto? = null,
|
||||
val status: String? = null,
|
||||
val statusText: String? = null,
|
||||
val workBlocks: List<WorkBlockDto> = emptyList(),
|
||||
@@ -99,3 +105,28 @@ data class WorkBlockDto(
|
||||
val totalWorkTime: String? = null,
|
||||
val netWorkTime: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WeekDayHolidayDto(
|
||||
val hours: Double = 0.0,
|
||||
val description: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WeekDayVacationDto(
|
||||
val hours: Double = 0.0,
|
||||
val halfDay: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WeekDaySickDto(
|
||||
val hours: Double = 0.0,
|
||||
val type: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NonWorkingDetailDto(
|
||||
val date: String? = null,
|
||||
val type: String? = null,
|
||||
val hours: Double = 0.0,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package de.tsschulz.timeclock.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -14,12 +17,14 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import de.tsschulz.timeclock.data.api.WeekDayDto
|
||||
import de.tsschulz.timeclock.data.api.WeekOverviewDto
|
||||
import de.tsschulz.timeclock.data.api.WorkBlockDto
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.tsschulz.timeclock.ui.admin.AdminViewModel
|
||||
import de.tsschulz.timeclock.ui.admin.HolidaysAdminScreen
|
||||
@@ -49,6 +54,7 @@ import de.tsschulz.timeclock.ui.settings.ProfileScreen
|
||||
import de.tsschulz.timeclock.ui.settings.SettingsViewModel
|
||||
import de.tsschulz.timeclock.ui.settings.TimewishScreen
|
||||
import de.tsschulz.timeclock.ui.theme.TcColors
|
||||
import de.tsschulz.timeclock.ui.theme.TcRadius
|
||||
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
||||
import de.tsschulz.timeclock.ui.time.EntriesScreen
|
||||
import de.tsschulz.timeclock.ui.time.StatsScreen
|
||||
@@ -224,34 +230,46 @@ private fun DemoScreen(
|
||||
)
|
||||
}
|
||||
AppRoute.Export -> TcCard { Text("Export ist in der Android-App deaktiviert.", color = TcColors.TextMuted) }
|
||||
AppRoute.Profile -> ProfileScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onSave = { name, stateId, weekWorkdays, dailyHours, titleType ->
|
||||
settingsViewModel.updateProfile(name, stateId, weekWorkdays, dailyHours, titleType)
|
||||
},
|
||||
)
|
||||
AppRoute.Profile -> {
|
||||
LaunchedEffect(route) { settingsViewModel.loadProfile() }
|
||||
ProfileScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onSave = { name, stateId, weekWorkdays, dailyHours, titleType ->
|
||||
settingsViewModel.updateProfile(name, stateId, weekWorkdays, dailyHours, titleType)
|
||||
},
|
||||
)
|
||||
}
|
||||
AppRoute.Password -> PasswordScreen(
|
||||
state = settingsState,
|
||||
onChange = { old, new, confirm -> settingsViewModel.changePassword(old, new, confirm) },
|
||||
)
|
||||
AppRoute.Timewish -> TimewishScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onCreate = { day, type, hours, start, end -> settingsViewModel.createTimewish(day, type, hours, start, end) },
|
||||
onDelete = { settingsViewModel.deleteTimewish(it) },
|
||||
)
|
||||
AppRoute.Permissions -> PermissionsScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onAdd = { settingsViewModel.addWatcher(it) },
|
||||
onDelete = { settingsViewModel.deleteWatcher(it) },
|
||||
)
|
||||
AppRoute.Invite -> InviteScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onSend = { settingsViewModel.sendInvite(it) },
|
||||
)
|
||||
AppRoute.Timewish -> {
|
||||
LaunchedEffect(route) { settingsViewModel.loadTimewishes() }
|
||||
TimewishScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onCreate = { day, type, hours, start, end -> settingsViewModel.createTimewish(day, type, hours, start, end) },
|
||||
onDelete = { settingsViewModel.deleteTimewish(it) },
|
||||
)
|
||||
}
|
||||
AppRoute.Permissions -> {
|
||||
LaunchedEffect(route) { settingsViewModel.loadWatchers() }
|
||||
PermissionsScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onAdd = { settingsViewModel.addWatcher(it) },
|
||||
onDelete = { settingsViewModel.deleteWatcher(it) },
|
||||
)
|
||||
}
|
||||
AppRoute.Invite -> {
|
||||
LaunchedEffect(route) { settingsViewModel.loadInvites() }
|
||||
InviteScreen(
|
||||
state = settingsState,
|
||||
isTablet = isTablet,
|
||||
onSend = { settingsViewModel.sendInvite(it) },
|
||||
)
|
||||
}
|
||||
AppRoute.Holidays -> HolidaysAdminScreen(
|
||||
state = adminState,
|
||||
isTablet = isTablet,
|
||||
@@ -298,55 +316,203 @@ private fun WeekOverviewScreen(
|
||||
|
||||
@Composable
|
||||
private fun WeekTablet(week: WeekOverviewDto) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Xl), modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(TcSpacing.Lg)) {
|
||||
week.days.forEach { day -> WeekDayCard(day) }
|
||||
}
|
||||
TcCard(modifier = Modifier.weight(1f)) {
|
||||
SectionTitle("Wochensumme")
|
||||
DetailRow("Arbeitszeit", week.weekTotal ?: "0:00")
|
||||
DetailRow("Gesamt", week.totalAll ?: week.weekTotal ?: "0:00")
|
||||
val today = week.days.firstOrNull { it.isToday } ?: week.days.firstOrNull()
|
||||
today?.let {
|
||||
SectionTitle("Aktueller Tag")
|
||||
DetailRow("Tag", it.name ?: "—")
|
||||
DetailRow("Datum", it.date.toDisplayDate())
|
||||
DetailRow("Status", it.statusText ?: "—")
|
||||
DetailRow("Arbeitszeit", it.netWorkTime ?: it.totalWorkTime ?: "—")
|
||||
}
|
||||
}
|
||||
}
|
||||
WeekTable(week, compact = false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekPhone(week: WeekOverviewDto) {
|
||||
week.days.forEach { day -> WeekDayCard(day) }
|
||||
WeekTable(week, compact = true)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekTable(week: WeekOverviewDto, compact: Boolean) {
|
||||
TcCard {
|
||||
SectionTitle("Wochensumme")
|
||||
DetailRow("Arbeitszeit", week.weekTotal ?: "0:00")
|
||||
DetailRow("Gesamt", week.totalAll ?: week.weekTotal ?: "0:00")
|
||||
if (!compact) {
|
||||
WeekHeaderRow()
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
||||
week.days.forEach { day -> WeekDayRow(day, compact) }
|
||||
WeekSummaryRows(week, compact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekDayCard(day: WeekDayDto) {
|
||||
TcCard {
|
||||
SectionTitle("${day.name ?: "Tag"} ${day.date.toDisplayDate()}")
|
||||
val blocks = day.workBlocks
|
||||
if (blocks.isNotEmpty()) {
|
||||
blocks.forEachIndexed { index, block ->
|
||||
val label = if (blocks.size > 1) "Arbeitszeit ${index + 1}" else "Arbeitszeit"
|
||||
DetailRow(label, block.workTime ?: day.workTime ?: "—")
|
||||
DetailRow("Netto", block.netWorkTime ?: block.totalWorkTime ?: "—")
|
||||
}
|
||||
} else {
|
||||
DetailRow("Arbeitszeit", day.workTime ?: "—")
|
||||
DetailRow("Netto", day.netWorkTime ?: day.totalWorkTime ?: "—")
|
||||
}
|
||||
DetailRow("Status", day.statusText ?: "—")
|
||||
private fun WeekHeaderRow() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TcColors.Background, RoundedCornerShape(TcRadius.Medium))
|
||||
.padding(horizontal = TcSpacing.Md, vertical = TcSpacing.Sm),
|
||||
horizontalArrangement = Arrangement.spacedBy(TcSpacing.Md),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
WeekCell("Tag", weight = 1.0f, header = true)
|
||||
WeekCell("Datum", weight = 1.0f, header = true)
|
||||
WeekCell("Zeiten", weight = 2.4f, header = true)
|
||||
WeekCell("Arbeitszeit", weight = 1.4f, header = true)
|
||||
WeekCell("Status", weight = 1.2f, header = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekDayRow(day: WeekDayDto, compact: Boolean) {
|
||||
val bg = if (day.isToday) TcColors.ActiveMenu else TcColors.Background
|
||||
if (compact) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(bg, RoundedCornerShape(TcRadius.Medium))
|
||||
.border(1.dp, TcColors.Border, RoundedCornerShape(TcRadius.Medium))
|
||||
.padding(TcSpacing.Md),
|
||||
verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm),
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(day.name ?: "Tag", color = TcColors.Text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(day.date.toDisplayDate(), color = TcColors.TextMuted, fontSize = 14.sp)
|
||||
}
|
||||
WeekDetailBlock("Zeiten", day.timeLines())
|
||||
WeekDetailBlock("Arbeitszeit", day.workTotalLines())
|
||||
day.statusText?.let { WeekStatusBadge(it) }
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(bg, RoundedCornerShape(TcRadius.Medium))
|
||||
.border(1.dp, TcColors.Border, RoundedCornerShape(TcRadius.Medium))
|
||||
.padding(TcSpacing.Md),
|
||||
horizontalArrangement = Arrangement.spacedBy(TcSpacing.Md),
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
WeekCell(day.name ?: "Tag", weight = 1.0f, strong = true)
|
||||
WeekCell(day.date.toDisplayDate(), weight = 1.0f)
|
||||
WeekMultilineCell(day.timeLines(), weight = 2.4f)
|
||||
WeekMultilineCell(day.workTotalLines(), weight = 1.4f)
|
||||
WeekCell(day.statusText ?: "—", weight = 1.2f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekSummaryRows(week: WeekOverviewDto, compact: Boolean) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(TcColors.Card, RoundedCornerShape(TcRadius.Medium))
|
||||
.border(1.dp, TcColors.Border, RoundedCornerShape(TcRadius.Medium))
|
||||
.padding(TcSpacing.Md),
|
||||
verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm),
|
||||
) {
|
||||
WeekSummaryLine("Wochensumme", week.weekTotal ?: "0:00", compact)
|
||||
if (week.nonWorkingDays > 0) {
|
||||
WeekSummaryLine(
|
||||
"Arbeitsfreie Tage (${week.nonWorkingDays})",
|
||||
week.nonWorkingTotal ?: "0:00",
|
||||
compact,
|
||||
week.nonWorkingDetails.joinToString { "${it.date.toDisplayDate()}: ${it.type ?: "frei"} (${it.hours.toHourLabel()}h)" },
|
||||
)
|
||||
}
|
||||
WeekSummaryLine("Gesamtsumme", week.totalAll ?: week.weekTotal ?: "0:00", compact)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekSummaryLine(label: String, value: String, compact: Boolean, detail: String? = null) {
|
||||
if (compact) {
|
||||
Column {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(label, color = TcColors.Text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text(value, color = TcColors.Text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
detail?.let { Text(it, color = TcColors.TextMuted, fontSize = 12.sp) }
|
||||
}
|
||||
} else {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TcSpacing.Md)) {
|
||||
Text(label, color = TcColors.Text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(5.4f))
|
||||
Text(value, color = TcColors.Text, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1.4f))
|
||||
Text(detail.orEmpty(), color = TcColors.TextMuted, fontSize = 12.sp, modifier = Modifier.weight(1.2f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekDetailBlock(label: String, lines: List<String>) {
|
||||
Text(label, color = TcColors.TextMuted, fontSize = 13.sp, fontWeight = FontWeight.Medium)
|
||||
lines.ifEmpty { listOf("—") }.forEach { line ->
|
||||
Text(line, color = TcColors.Text, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekStatusBadge(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
color = TcColors.Text,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier
|
||||
.background(TcColors.Button, RoundedCornerShape(TcRadius.Medium))
|
||||
.border(1.dp, TcColors.ButtonBorder, RoundedCornerShape(TcRadius.Medium))
|
||||
.padding(horizontal = TcSpacing.Sm, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.WeekCell(text: String, weight: Float, header: Boolean = false, strong: Boolean = false) {
|
||||
Text(
|
||||
text = text,
|
||||
color = if (header) TcColors.TextMuted else TcColors.Text,
|
||||
fontSize = if (header) 13.sp else 14.sp,
|
||||
fontWeight = if (header || strong) FontWeight.SemiBold else FontWeight.Normal,
|
||||
modifier = Modifier.weight(weight),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.WeekMultilineCell(lines: List<String>, weight: Float) {
|
||||
Column(modifier = Modifier.weight(weight), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
lines.ifEmpty { listOf("—") }.forEach {
|
||||
Text(it, color = TcColors.Text, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WeekDayDto.timeLines(): List<String> {
|
||||
val blocks = workBlocks
|
||||
return if (blocks.isNotEmpty()) {
|
||||
blocks.flatMapIndexed { index, block ->
|
||||
listOf("${if (blocks.size > 1) "Arbeitszeit ${index + 1}" else "Arbeitszeit"}: ${block.workTime ?: workTime ?: "—"}")
|
||||
}
|
||||
} else if (!workTime.isNullOrBlank()) {
|
||||
listOf("Arbeitszeit: $workTime")
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun WeekDayDto.workTotalLines(): List<String> {
|
||||
val lines = mutableListOf<String>()
|
||||
val blocks = workBlocks
|
||||
if (blocks.isNotEmpty()) {
|
||||
blocks.forEach { block -> lines.addAll(block.totalLines()) }
|
||||
if (blocks.size > 1 && !netWorkTime.isNullOrBlank()) lines.add("Gesamt: $netWorkTime")
|
||||
} else {
|
||||
totalWorkTime?.let { lines.add(it) }
|
||||
netWorkTime?.let { lines.add("= $it") }
|
||||
}
|
||||
holiday?.let { lines.add("+ ${it.hours.toHourLabel()}:00 ${it.description ?: "Feiertag"}") }
|
||||
vacation?.let { lines.add("+ ${it.hours.toHourLabel()}:00 Urlaub") }
|
||||
sick?.let { lines.add("${it.hours.toHourLabel()}:00 (${it.type.toSickLabel()})") }
|
||||
return lines
|
||||
}
|
||||
|
||||
private fun WorkBlockDto.totalLines(): List<String> =
|
||||
buildList {
|
||||
totalWorkTime?.let { add(it) }
|
||||
netWorkTime?.let { add("= $it") }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CalendarDemo(isTablet: Boolean) {
|
||||
if (isTablet) {
|
||||
@@ -408,6 +574,19 @@ private fun String?.toDisplayDate(): String {
|
||||
}.getOrDefault(this)
|
||||
}
|
||||
|
||||
private fun Double.toHourLabel(): String =
|
||||
if (this % 1.0 == 0.0) toInt().toString() else toString()
|
||||
|
||||
private fun String?.toSickLabel(): String =
|
||||
when (this) {
|
||||
"self" -> "Krank"
|
||||
"child" -> "Kind krank"
|
||||
"parents" -> "Eltern krank"
|
||||
"partner" -> "Partner krank"
|
||||
null -> "Krank"
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionTitle(text: String) {
|
||||
Text(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.tsschulz.timeclock.ui.admin
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -34,6 +36,8 @@ import de.tsschulz.timeclock.ui.theme.TcColors
|
||||
import de.tsschulz.timeclock.ui.theme.TcRadius
|
||||
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun HolidaysAdminScreen(
|
||||
@@ -42,7 +46,7 @@ fun HolidaysAdminScreen(
|
||||
onCreate: (String, Double, String, List<String>) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
) {
|
||||
var date by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
|
||||
var date by rememberSaveable { mutableStateOf(LocalDate.now().toGermanDate()) }
|
||||
var hours by rememberSaveable { mutableStateOf("8") }
|
||||
var description by rememberSaveable { mutableStateOf("") }
|
||||
var stateIds by rememberSaveable { mutableStateOf("") }
|
||||
@@ -61,8 +65,11 @@ fun HolidaysAdminScreen(
|
||||
stateIds = stateIds,
|
||||
onStateIds = { stateIds = it },
|
||||
onCreate = {
|
||||
onCreate(date, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
||||
description = ""
|
||||
normalizeHolidayDate(date)?.let { isoDate ->
|
||||
onCreate(isoDate, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
||||
date = LocalDate.parse(isoDate).toGermanDate()
|
||||
description = ""
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(0.9f),
|
||||
)
|
||||
@@ -83,8 +90,11 @@ fun HolidaysAdminScreen(
|
||||
stateIds = stateIds,
|
||||
onStateIds = { stateIds = it },
|
||||
onCreate = {
|
||||
onCreate(date, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
||||
description = ""
|
||||
normalizeHolidayDate(date)?.let { isoDate ->
|
||||
onCreate(isoDate, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
||||
date = LocalDate.parse(isoDate).toGermanDate()
|
||||
description = ""
|
||||
}
|
||||
},
|
||||
)
|
||||
HolidayList("Zukünftige Feiertage", state.futureHolidays, onDelete)
|
||||
@@ -129,15 +139,9 @@ private fun HolidayForm(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FormCard("Feiertag hinzufügen", modifier) {
|
||||
TcTextField("Datum", date, onDate, placeholder = "YYYY-MM-DD")
|
||||
HolidayDateInput(date, onDate)
|
||||
TcTextField("Freie Stunden", hours, onHours, placeholder = "8")
|
||||
TcTextField("Beschreibung", description, onDescription, placeholder = "z.B. Tag der Deutschen Einheit")
|
||||
TcTextField(
|
||||
label = "Bundesland-IDs",
|
||||
value = stateIds,
|
||||
onValueChange = onStateIds,
|
||||
placeholder = "Leer lassen für Bundesfeiertag",
|
||||
)
|
||||
if (state.holidayStates.isNotEmpty()) {
|
||||
Text("Bundesländer", color = TcColors.TextMuted, fontSize = 13.sp)
|
||||
FlowRow(
|
||||
@@ -160,6 +164,36 @@ private fun HolidayForm(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HolidayDateInput(value: String, onValueChange: (String) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val selectedDate = parseHolidayDate(value) ?: LocalDate.now()
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm), verticalAlignment = Alignment.Bottom) {
|
||||
TcTextField(
|
||||
label = "Datum",
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = "TT.MM.JJJJ oder YYYY-MM-DD",
|
||||
)
|
||||
TcButton(
|
||||
text = "Auswählen",
|
||||
variant = ButtonVariant.Default,
|
||||
onClick = {
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _, year, month, day ->
|
||||
onValueChange(LocalDate.of(year, month + 1, day).toGermanDate())
|
||||
},
|
||||
selectedDate.year,
|
||||
selectedDate.monthValue - 1,
|
||||
selectedDate.dayOfMonth,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HolidayList(title: String, holidays: List<HolidayDto>, onDelete: (String) -> Unit) {
|
||||
ListCard(title, holidays) { holiday -> HolidayRow(holiday, onDelete) }
|
||||
@@ -269,6 +303,20 @@ private fun toggleStateId(raw: String, id: String): String {
|
||||
return ids.joinToString(",")
|
||||
}
|
||||
|
||||
private val germanDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY)
|
||||
|
||||
private fun LocalDate.toGermanDate(): String = format(germanDateFormatter)
|
||||
|
||||
private fun parseHolidayDate(raw: String): LocalDate? {
|
||||
val value = raw.trim()
|
||||
if (value.isBlank()) return null
|
||||
return runCatching { LocalDate.parse(value) }
|
||||
.recoverCatching { LocalDate.parse(value, germanDateFormatter) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
private fun normalizeHolidayDate(raw: String): String? = parseHolidayDate(raw)?.toString()
|
||||
|
||||
private fun String?.toDisplayDate(): String {
|
||||
if (this.isNullOrBlank()) return "-"
|
||||
return runCatching {
|
||||
|
||||
@@ -50,6 +50,7 @@ import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun VacationScreen(state: BookingUiState, isTablet: Boolean, onCreate: (Int, String, String?) -> Unit, onDelete: (String) -> Unit) {
|
||||
@@ -58,13 +59,21 @@ fun VacationScreen(state: BookingUiState, isTablet: Boolean, onCreate: (Int, Str
|
||||
var end by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
|
||||
Phase4Frame(loading = state.vacationLoading, error = state.vacationError) {
|
||||
FormCard("Urlaub eintragen", isTablet) {
|
||||
TcTextField("Umfang (0 Zeitraum, 1 Halber Tag)", type, { type = it }, placeholder = "0")
|
||||
FieldLabel("Umfang")
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
||||
TcButton("Zeitraum", variant = if (type == "0") ButtonVariant.Primary else ButtonVariant.Default, onClick = { type = "0" })
|
||||
TcButton("Halber Tag", variant = if (type == "1") ButtonVariant.Primary else ButtonVariant.Default, onClick = { type = "1" })
|
||||
TcButton("Halber Tag", variant = if (type == "1") ButtonVariant.Primary else ButtonVariant.Default, onClick = {
|
||||
type = "1"
|
||||
end = start
|
||||
})
|
||||
}
|
||||
DateField("Urlaubsbeginn", start, {
|
||||
start = it
|
||||
if (type == "1") end = it
|
||||
})
|
||||
if (type != "1") {
|
||||
DateField("Urlaubsende", end, { end = it })
|
||||
}
|
||||
TcTextField("Urlaubsbeginn", start, { start = it }, placeholder = "YYYY-MM-DD")
|
||||
TcTextField("Urlaubsende", end, { end = it }, placeholder = "YYYY-MM-DD")
|
||||
TcButton("Urlaub eintragen", variant = ButtonVariant.Primary, onClick = {
|
||||
val typeValue = type.toIntOrNull() ?: 0
|
||||
onCreate(typeValue, start, if (typeValue == 1) start else end)
|
||||
@@ -86,9 +95,9 @@ fun SickScreen(state: BookingUiState, isTablet: Boolean, onCreate: (String, Stri
|
||||
}
|
||||
Phase4Frame(loading = state.sickLoading, error = state.sickError) {
|
||||
FormCard("Erkrankung eintragen", isTablet) {
|
||||
TcTextField("Erster Krankheitstag", start, { start = it }, placeholder = "YYYY-MM-DD")
|
||||
TcTextField("Letzter Krankheitstag", end, { end = it }, placeholder = "YYYY-MM-DD")
|
||||
TcTextField("Krankheitstyp-ID", typeId, { typeId = it }, placeholder = state.sickTypes.joinToString { "${it.id}=${it.name}" })
|
||||
DateField("Erster Krankheitstag", start, { start = it })
|
||||
DateField("Letzter Krankheitstag", end, { end = it })
|
||||
FieldLabel("Krankheitstyp")
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm), verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
||||
state.sickTypes.forEach { type ->
|
||||
TcButton(
|
||||
@@ -162,11 +171,16 @@ fun TimefixScreen(
|
||||
|
||||
@Composable
|
||||
fun WorkdaysScreen(state: BookingUiState, onYear: (Int) -> Unit) {
|
||||
var year by rememberSaveable { mutableStateOf(state.workdaysYear.toString()) }
|
||||
var selectedYear by rememberSaveable { mutableStateOf(state.workdaysYear) }
|
||||
LaunchedEffect(state.workdaysYear) {
|
||||
selectedYear = state.workdaysYear
|
||||
}
|
||||
Phase4Frame(loading = state.workdaysLoading, error = state.workdaysError) {
|
||||
FormCard("Arbeitstage", isTablet = false) {
|
||||
TcTextField("Jahr", year, { year = it }, placeholder = "2026")
|
||||
TcButton("Jahr laden", variant = ButtonVariant.Primary, onClick = { year.toIntOrNull()?.let(onYear) })
|
||||
YearDropdown(selectedYear) { year ->
|
||||
selectedYear = year
|
||||
onYear(year)
|
||||
}
|
||||
}
|
||||
WorkdaysCard(state.workdays)
|
||||
}
|
||||
@@ -311,6 +325,32 @@ private fun ActionDropdown(value: String, onValueChange: (String) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun YearDropdown(value: Int, onValueChange: (Int) -> Unit) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val years = 2000..2100
|
||||
FieldLabel("Jahr")
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TcButton(
|
||||
text = value.toString(),
|
||||
variant = ButtonVariant.Default,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
years.forEach { year ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(year.toString(), color = TcColors.Text) },
|
||||
onClick = {
|
||||
onValueChange(year)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FieldLabel(label: String) {
|
||||
Text(
|
||||
@@ -432,11 +472,14 @@ private fun CalendarCell(day: CalendarDayDto) {
|
||||
.border(1.dp, TcColors.Border, RoundedCornerShape(TcRadius.Medium))
|
||||
.padding(TcSpacing.Sm),
|
||||
) {
|
||||
Text(day.day.toString(), color = TcColors.Text, fontWeight = FontWeight.SemiBold)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.Bottom) {
|
||||
Text(day.toWeekdayLabel(), color = TcColors.TextMuted, fontSize = 11.sp)
|
||||
Text(day.day.toString(), color = TcColors.Text, fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
day.holiday?.let { Text(it, color = TcColors.Danger, fontSize = 11.sp) }
|
||||
if (day.sick) Text("Krank", color = TcColors.Secondary, fontSize = 11.sp)
|
||||
day.vacation?.let { Text(if (it == "half") "Urlaub 1/2" else "Urlaub", color = TcColors.Primary, fontSize = 11.sp) }
|
||||
day.workedHours?.let { Text("${it}h", color = TcColors.TextMuted, fontSize = 11.sp) }
|
||||
day.workedHours?.let { Text(it.toHourMinuteLabel(), color = TcColors.TextMuted, fontSize = 11.sp) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,4 +515,16 @@ private fun String.toActionLabel(): String =
|
||||
private fun WorklogEntryDto.toWorklogLabel(): String =
|
||||
"${time ?: "—"} - ${(action ?: "—").toActionLabel()}"
|
||||
|
||||
private fun CalendarDayDto.toWeekdayLabel(): String =
|
||||
runCatching {
|
||||
LocalDate.parse(date).format(DateTimeFormatter.ofPattern("EEE", Locale.GERMANY))
|
||||
}.getOrDefault("")
|
||||
|
||||
private fun Double.toHourMinuteLabel(): String {
|
||||
val totalMinutes = (this * 60).roundToInt().coerceAtLeast(0)
|
||||
val hours = totalMinutes / 60
|
||||
val minutes = totalMinutes % 60
|
||||
return "%d:%02d".format(hours, minutes)
|
||||
}
|
||||
|
||||
private val Color333 = androidx.compose.ui.graphics.Color(0xFF333333)
|
||||
|
||||
@@ -40,13 +40,14 @@ val adminSections = userSections + MenuSection(
|
||||
)
|
||||
|
||||
val mockStatusRows = listOf(
|
||||
StatusRow(label = "Heute", isHeading = true),
|
||||
StatusRow(label = "Status", value = "Arbeit läuft"),
|
||||
StatusRow(label = "Beginn", value = "08:12"),
|
||||
StatusRow(label = "Arbeitszeit", value = "04:37:18"),
|
||||
StatusRow(label = "Offen", value = "03:23"),
|
||||
StatusRow(label = "Reguläres Ende", value = "16:42"),
|
||||
StatusRow(label = "Derzeit gearbeitet", value = "04:37:18 h"),
|
||||
StatusRow(label = "Offen", value = "03:23 h"),
|
||||
StatusRow(label = "Normales Arbeitsende", value = "16:42 Uhr"),
|
||||
StatusRow(label = "Überstunden (Woche)", value = "00:18 h"),
|
||||
StatusRow(label = "Wochenarbeitszeit", value = "20:37 h"),
|
||||
StatusRow(label = "Bereinigtes Arbeitsende (heute)", isHeading = true),
|
||||
StatusRow(label = "- Generell", value = "16:42 Uhr"),
|
||||
)
|
||||
|
||||
val mockPrimaryAction = StatusAction("Pause starten", ButtonVariant.Success)
|
||||
val mockSecondaryAction = StatusAction("Gehen", ButtonVariant.Secondary)
|
||||
val mockPrimaryAction = StatusAction("Arbeit beenden", ButtonVariant.Danger)
|
||||
val mockSecondaryAction = StatusAction("Pause beginnen", ButtonVariant.Secondary)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package de.tsschulz.timeclock.ui.settings
|
||||
|
||||
import android.app.DatePickerDialog
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -18,11 +22,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import de.tsschulz.timeclock.data.api.InvitationDto
|
||||
import de.tsschulz.timeclock.data.api.ProfileDto
|
||||
import de.tsschulz.timeclock.data.api.StateDto
|
||||
import de.tsschulz.timeclock.data.api.TimewishDto
|
||||
import de.tsschulz.timeclock.data.api.WatcherDto
|
||||
import de.tsschulz.timeclock.ui.components.TcButton
|
||||
@@ -35,6 +41,8 @@ import de.tsschulz.timeclock.ui.theme.TcColors
|
||||
import de.tsschulz.timeclock.ui.theme.TcRadius
|
||||
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
@@ -47,7 +55,7 @@ fun ProfileScreen(
|
||||
var stateId by rememberSaveable { mutableStateOf("") }
|
||||
var weekWorkdays by rememberSaveable { mutableStateOf("5") }
|
||||
var dailyHours by rememberSaveable { mutableStateOf("8.0") }
|
||||
var preferredTitleType by rememberSaveable { mutableStateOf("0") }
|
||||
var preferredTitleType by rememberSaveable { mutableStateOf(2) }
|
||||
|
||||
LaunchedEffect(profile) {
|
||||
profile?.let {
|
||||
@@ -55,7 +63,7 @@ fun ProfileScreen(
|
||||
stateId = it.stateId.orEmpty()
|
||||
weekWorkdays = (it.weekWorkdays ?: 5).toString()
|
||||
dailyHours = (it.dailyHours ?: 8.0).toString()
|
||||
preferredTitleType = (it.preferredTitleType ?: 0).toString()
|
||||
preferredTitleType = it.preferredTitleType ?: 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,17 +71,22 @@ fun ProfileScreen(
|
||||
ResponsiveSettings(isTablet) {
|
||||
FormCard("Persönliche Daten") {
|
||||
TcTextField("Name", fullName, { fullName = it })
|
||||
TcTextField("Bundesland-ID", stateId, { stateId = it }, placeholder = state.states.joinToString { "${it.id}=${it.name}" })
|
||||
StateDropdown(
|
||||
states = state.states,
|
||||
selectedId = stateId,
|
||||
selectedName = profile?.stateName,
|
||||
onSelect = { stateId = it.orEmpty() },
|
||||
)
|
||||
TcTextField("Arbeitstage pro Woche", weekWorkdays, { weekWorkdays = it }, placeholder = "5")
|
||||
TcTextField("Stunden pro Tag", dailyHours, { dailyHours = it }, placeholder = "8.0")
|
||||
TcTextField("Titeltyp", preferredTitleType, { preferredTitleType = it }, placeholder = "0")
|
||||
TitleTypeDropdown(preferredTitleType, { preferredTitleType = it })
|
||||
TcButton("Speichern", variant = ButtonVariant.Primary, onClick = {
|
||||
onSave(
|
||||
fullName,
|
||||
stateId.ifBlank { null },
|
||||
weekWorkdays.toIntOrNull() ?: 5,
|
||||
dailyHours.toDoubleOrNull() ?: 8.0,
|
||||
preferredTitleType.toIntOrNull() ?: 0,
|
||||
preferredTitleType,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -113,8 +126,8 @@ fun TimewishScreen(
|
||||
onCreate: (Int, Int, Double?, String, String?) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
) {
|
||||
var day by rememberSaveable { mutableStateOf("1") }
|
||||
var wishtype by rememberSaveable { mutableStateOf("1") }
|
||||
var day by rememberSaveable { mutableStateOf(1) }
|
||||
var wishtype by rememberSaveable { mutableStateOf(2) }
|
||||
var hours by rememberSaveable { mutableStateOf("8.0") }
|
||||
var startDate by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
|
||||
var endDate by rememberSaveable { mutableStateOf("") }
|
||||
@@ -122,16 +135,21 @@ fun TimewishScreen(
|
||||
SettingsFrame(state) {
|
||||
ResponsiveSettings(isTablet) {
|
||||
FormCard("Zeitwunsch eintragen") {
|
||||
TcTextField("Wochentag", day, { day = it }, placeholder = "1")
|
||||
TcTextField("Wunschtyp", wishtype, { wishtype = it }, placeholder = "1")
|
||||
TcTextField("Stunden", hours, { hours = it }, placeholder = "8.0")
|
||||
TcTextField("Gueltig ab", startDate, { startDate = it }, placeholder = "YYYY-MM-DD")
|
||||
TcTextField("Gueltig bis", endDate, { endDate = it }, placeholder = "YYYY-MM-DD")
|
||||
WeekdayDropdown(day, { day = it })
|
||||
WishtypeDropdown(wishtype) {
|
||||
wishtype = it
|
||||
hours = if (it == 2) "8.0" else "0"
|
||||
}
|
||||
if (wishtype == 2) {
|
||||
TcTextField("Stunden", hours, { hours = it }, placeholder = "8.0")
|
||||
}
|
||||
SettingsDateField("Gültig ab", startDate, { startDate = it })
|
||||
SettingsDateField("Gültig bis (optional)", endDate, { endDate = it }, allowBlank = true)
|
||||
TcButton("Zeitwunsch speichern", variant = ButtonVariant.Primary, onClick = {
|
||||
onCreate(
|
||||
day.toIntOrNull() ?: 1,
|
||||
wishtype.toIntOrNull() ?: 1,
|
||||
hours.toDoubleOrNull(),
|
||||
day,
|
||||
wishtype,
|
||||
if (wishtype == 2) hours.toDoubleOrNull() else null,
|
||||
startDate,
|
||||
endDate.ifBlank { null },
|
||||
)
|
||||
@@ -191,6 +209,189 @@ fun PermissionsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleTypeDropdown(value: Int, onValueChange: (Int) -> Unit) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
FieldLabel("Anzeige in Seitentitel")
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TcButton(
|
||||
text = titleTypeLabel(value),
|
||||
variant = ButtonVariant.Default,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
(0..7).forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(titleTypeLabel(type), color = TcColors.Text) },
|
||||
onClick = {
|
||||
onValueChange(type)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WeekdayDropdown(value: Int, onValueChange: (Int) -> Unit) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
FieldLabel("Wochentag")
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TcButton(
|
||||
text = weekdayLabel(value),
|
||||
variant = ButtonVariant.Default,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
(1..7).forEach { day ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(weekdayLabel(day), color = TcColors.Text) },
|
||||
onClick = {
|
||||
onValueChange(day)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WishtypeDropdown(value: Int, onValueChange: (Int) -> Unit) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
FieldLabel("Typ")
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TcButton(
|
||||
text = wishtypeLabel(value),
|
||||
variant = ButtonVariant.Default,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
(0..2).forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(wishtypeLabel(type), color = TcColors.Text) },
|
||||
onClick = {
|
||||
onValueChange(type)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsDateField(
|
||||
label: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
allowBlank: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val parsed = runCatching { LocalDate.parse(value) }.getOrDefault(LocalDate.now())
|
||||
FieldLabel(label)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm), verticalAlignment = Alignment.CenterVertically) {
|
||||
TcButton(
|
||||
text = if (allowBlank && value.isBlank()) "Kein Enddatum" else parsed.toGermanDate(),
|
||||
variant = ButtonVariant.Default,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
DatePickerDialog(
|
||||
context,
|
||||
{ _, year, month, day ->
|
||||
onValueChange(LocalDate.of(year, month + 1, day).toString())
|
||||
},
|
||||
parsed.year,
|
||||
parsed.monthValue - 1,
|
||||
parsed.dayOfMonth,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
if (allowBlank) {
|
||||
TcButton("Leeren", variant = ButtonVariant.Default, onClick = { onValueChange("") })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateDropdown(
|
||||
states: List<StateDto>,
|
||||
selectedId: String,
|
||||
selectedName: String?,
|
||||
onSelect: (String?) -> Unit,
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
val selected = states.firstOrNull { it.id == selectedId }
|
||||
FieldLabel("Bundesland")
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TcButton(
|
||||
text = selected?.name ?: selectedName?.takeIf { selectedId.isNotBlank() } ?: "Kein Bundesland ausgewählt",
|
||||
variant = ButtonVariant.Default,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { expanded = true },
|
||||
)
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Kein Bundesland ausgewählt", color = TcColors.Text) },
|
||||
onClick = {
|
||||
onSelect(null)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
states.forEach { state ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(state.name, color = TcColors.Text) },
|
||||
onClick = {
|
||||
onSelect(state.id)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun titleTypeLabel(value: Int): String =
|
||||
when (value) {
|
||||
0 -> "Restzeit für Tag ohne Korrektur"
|
||||
1 -> "Restzeit für Tag, korrigiert nach Wochenarbeitszeit"
|
||||
2 -> "Restzeit für Tag, korrigiert nach Gesamtarbeitszeit"
|
||||
3 -> "Heute gearbeitete Zeit"
|
||||
4 -> "Uhrzeit für Arbeitsende ohne Korrektur"
|
||||
5 -> "Uhrzeit für Arbeitsende, korrigiert nach Wochenarbeitszeit"
|
||||
6 -> "Uhrzeit für Arbeitsende, korrigiert nach Gesamtarbeitszeit"
|
||||
7 -> "Nur App-Name"
|
||||
else -> "Restzeit für Tag, korrigiert nach Gesamtarbeitszeit"
|
||||
}
|
||||
|
||||
private fun weekdayLabel(value: Int): String =
|
||||
when (value) {
|
||||
1 -> "Montag"
|
||||
2 -> "Dienstag"
|
||||
3 -> "Mittwoch"
|
||||
4 -> "Donnerstag"
|
||||
5 -> "Freitag"
|
||||
6 -> "Samstag"
|
||||
7 -> "Sonntag"
|
||||
else -> "Montag"
|
||||
}
|
||||
|
||||
private fun wishtypeLabel(value: Int): String =
|
||||
when (value) {
|
||||
0 -> "Kein Wunsch"
|
||||
1 -> "Frei"
|
||||
2 -> "Arbeit"
|
||||
else -> "Arbeit"
|
||||
}
|
||||
|
||||
private val germanDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY)
|
||||
|
||||
private fun LocalDate.toGermanDate(): String = format(germanDateFormatter)
|
||||
|
||||
@Composable
|
||||
private fun SettingsFrame(state: SettingsUiState, content: @Composable () -> Unit) {
|
||||
if (state.loading) TcLoading("Lade Daten...")
|
||||
@@ -295,6 +496,16 @@ private fun SectionTitle(title: String) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FieldLabel(label: String) {
|
||||
Text(
|
||||
text = label,
|
||||
color = TcColors.Text,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Detail(label: String, value: String) {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
|
||||
@@ -37,9 +37,6 @@ class SettingsViewModel(
|
||||
|
||||
fun loadPhase5() {
|
||||
loadProfile()
|
||||
loadTimewishes()
|
||||
loadInvites()
|
||||
loadWatchers()
|
||||
}
|
||||
|
||||
fun loadProfile() = launchLoad {
|
||||
|
||||
@@ -22,16 +22,13 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
data class TimeUiState(
|
||||
val loading: Boolean = false,
|
||||
val clockInProgress: Boolean = false,
|
||||
val error: String? = null,
|
||||
val dashboard: TimeDashboard? = null,
|
||||
val statusRows: List<StatusRow> = listOf(StatusRow(label = "Heute", isHeading = true), StatusRow("Status", "—")),
|
||||
val statusRows: List<StatusRow> = listOf(StatusRow("Derzeit gearbeitet", "—")),
|
||||
val primaryAction: StatusAction? = null,
|
||||
val secondaryAction: StatusAction? = null,
|
||||
val weekLoading: Boolean = false,
|
||||
@@ -169,46 +166,54 @@ class TimeViewModel(
|
||||
private fun TimeDashboard.toStatusRows(): List<StatusRow> {
|
||||
val stats = stats
|
||||
return buildList {
|
||||
add(StatusRow(label = "Heute", isHeading = true))
|
||||
add(StatusRow("Status", state.toStatusText()))
|
||||
if (runningStartTime != null) add(StatusRow("Beginn", runningStartTime.toDisplayTime()))
|
||||
if (currentPauseStart != null) add(StatusRow("Pause seit", currentPauseStart.toDisplayTime()))
|
||||
add(StatusRow("Arbeitszeit", stats.currentlyWorked ?: "—"))
|
||||
add(StatusRow("Offen", stats.open ?: "—"))
|
||||
add(StatusRow("Woche", stats.weekWorktime ?: "—"))
|
||||
add(StatusRow("Überstunden", stats.overtime ?: "—"))
|
||||
add(StatusRow("Gesamt", stats.totalOvertime ?: "—"))
|
||||
add(StatusRow("Arbeitsende", stats.adjustedEndTodayGeneral ?: stats.regularEnd ?: "—"))
|
||||
add(StatusRow("Derzeit gearbeitet", stats.currentlyWorked.toHoursLabel()))
|
||||
add(StatusRow("Offen", stats.open.toHoursLabel()))
|
||||
add(StatusRow("Normales Arbeitsende", stats.regularEnd.toClockLabel()))
|
||||
addOvertimeRow("Woche", stats.overtime)
|
||||
addOvertimeRow("Gesamt", stats.totalOvertime)
|
||||
add(StatusRow("Wochenarbeitszeit", stats.weekWorktime.toHoursLabel()))
|
||||
add(StatusRow("Arbeitsfreie Stunden", stats.nonWorkingHours.toHoursLabel()))
|
||||
add(StatusRow("Offen für Woche", stats.openForWeek.toHoursLabel()))
|
||||
add(StatusRow("Bereinigtes Arbeitsende (heute)", isHeading = true))
|
||||
add(StatusRow("- Generell", stats.adjustedEndTodayGeneral.toClockLabel()))
|
||||
add(StatusRow("- Woche", stats.adjustedEndTodayWeek.toClockLabel()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun TimeDashboard.primaryAction(): StatusAction? =
|
||||
when (state) {
|
||||
null, "stop work" -> StatusAction("Kommen", ButtonVariant.Success, "start work")
|
||||
"start work", "stop pause" -> StatusAction("Pause starten", ButtonVariant.Default, "start pause")
|
||||
"start pause" -> StatusAction("Pause beenden", ButtonVariant.Success, "stop pause")
|
||||
else -> StatusAction("Kommen", ButtonVariant.Success, "start work")
|
||||
null, "stop work" -> StatusAction("Arbeit beginnen", ButtonVariant.Success, "start work")
|
||||
"start work", "stop pause" -> StatusAction("Arbeit beenden", ButtonVariant.Danger, "stop work")
|
||||
"start pause" -> null
|
||||
else -> StatusAction("Arbeit beginnen", ButtonVariant.Success, "start work")
|
||||
}
|
||||
|
||||
private fun TimeDashboard.secondaryAction(): StatusAction? =
|
||||
when (state) {
|
||||
"start work", "stop pause" -> StatusAction("Gehen", ButtonVariant.Secondary, "stop work")
|
||||
"start work", "stop pause" -> StatusAction("Pause beginnen", ButtonVariant.Secondary, "start pause")
|
||||
"start pause" -> StatusAction("Pause beenden", ButtonVariant.Secondary, "stop pause")
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun String?.toStatusText(): String =
|
||||
when (this) {
|
||||
null, "stop work" -> "Nicht eingestempelt"
|
||||
"start work" -> "Arbeit läuft"
|
||||
"start pause" -> "Pause läuft"
|
||||
"stop pause" -> "Arbeit läuft"
|
||||
else -> this
|
||||
}
|
||||
private fun MutableList<StatusRow>.addOvertimeRow(scope: String, value: String?) {
|
||||
val raw = value?.takeIf { it.isNotBlank() && it != "—" } ?: "—"
|
||||
val isNegative = raw.startsWith("-")
|
||||
val label = if (isNegative) "Fehlzeit ($scope)" else "Überstunden ($scope)"
|
||||
val displayValue = if (isNegative) raw.removePrefix("-").toHoursLabel() else raw.toHoursLabel()
|
||||
add(StatusRow(label, displayValue))
|
||||
}
|
||||
|
||||
private fun String.toDisplayTime(): String =
|
||||
runCatching {
|
||||
OffsetDateTime.parse(this).toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm", Locale.GERMANY))
|
||||
}.getOrDefault(this)
|
||||
private fun String?.toHoursLabel(): String {
|
||||
val value = this?.takeIf { it.isNotBlank() } ?: return "—"
|
||||
if (value == "—" || value.contains("erreicht")) return value
|
||||
return if (value.endsWith(" h")) value else "$value h"
|
||||
}
|
||||
|
||||
private fun String?.toClockLabel(): String {
|
||||
val value = this?.takeIf { it.isNotBlank() } ?: return "—"
|
||||
if (value == "—" || value.contains("erreicht") || value.endsWith(" Uhr")) return value
|
||||
return "$value Uhr"
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val application: Application,
|
||||
|
||||
Reference in New Issue
Block a user