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"
|
applicationId = "de.tsschulz.timeclock"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 3
|
versionCode = 4
|
||||||
versionName = "0.8.0-alpha2"
|
versionName = "0.8.0-alpha3"
|
||||||
buildConfigField("String", "API_BASE_URL", "\"${apiBaseUrl.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
|
buildConfigField("String", "API_BASE_URL", "\"${apiBaseUrl.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -24,7 +24,7 @@ data class SickEntryDto(
|
|||||||
val id: String,
|
val id: String,
|
||||||
val startDate: String? = null,
|
val startDate: String? = null,
|
||||||
val endDate: String? = null,
|
val endDate: String? = null,
|
||||||
val sickTypeId: Int? = null,
|
val sickTypeId: String? = null,
|
||||||
val sickTypeName: String? = null,
|
val sickTypeName: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ data class WeekOverviewDto(
|
|||||||
val weekEnd: String? = null,
|
val weekEnd: String? = null,
|
||||||
val weekTotal: String? = null,
|
val weekTotal: String? = null,
|
||||||
val totalAll: String? = null,
|
val totalAll: String? = null,
|
||||||
|
val nonWorkingTotal: String? = null,
|
||||||
|
val nonWorkingDays: Int = 0,
|
||||||
|
val nonWorkingDetails: List<NonWorkingDetailDto> = emptyList(),
|
||||||
val days: List<WeekDayDto> = emptyList(),
|
val days: List<WeekDayDto> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,6 +91,9 @@ data class WeekDayDto(
|
|||||||
val workTime: String? = null,
|
val workTime: String? = null,
|
||||||
val totalWorkTime: String? = null,
|
val totalWorkTime: String? = null,
|
||||||
val netWorkTime: String? = null,
|
val netWorkTime: String? = null,
|
||||||
|
val holiday: WeekDayHolidayDto? = null,
|
||||||
|
val vacation: WeekDayVacationDto? = null,
|
||||||
|
val sick: WeekDaySickDto? = null,
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
val statusText: String? = null,
|
val statusText: String? = null,
|
||||||
val workBlocks: List<WorkBlockDto> = emptyList(),
|
val workBlocks: List<WorkBlockDto> = emptyList(),
|
||||||
@@ -99,3 +105,28 @@ data class WorkBlockDto(
|
|||||||
val totalWorkTime: String? = null,
|
val totalWorkTime: String? = null,
|
||||||
val netWorkTime: 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
|
package de.tsschulz.timeclock.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -14,12 +17,14 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import de.tsschulz.timeclock.data.api.WeekDayDto
|
import de.tsschulz.timeclock.data.api.WeekDayDto
|
||||||
import de.tsschulz.timeclock.data.api.WeekOverviewDto
|
import de.tsschulz.timeclock.data.api.WeekOverviewDto
|
||||||
|
import de.tsschulz.timeclock.data.api.WorkBlockDto
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.tsschulz.timeclock.ui.admin.AdminViewModel
|
import de.tsschulz.timeclock.ui.admin.AdminViewModel
|
||||||
import de.tsschulz.timeclock.ui.admin.HolidaysAdminScreen
|
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.SettingsViewModel
|
||||||
import de.tsschulz.timeclock.ui.settings.TimewishScreen
|
import de.tsschulz.timeclock.ui.settings.TimewishScreen
|
||||||
import de.tsschulz.timeclock.ui.theme.TcColors
|
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.theme.TcSpacing
|
||||||
import de.tsschulz.timeclock.ui.time.EntriesScreen
|
import de.tsschulz.timeclock.ui.time.EntriesScreen
|
||||||
import de.tsschulz.timeclock.ui.time.StatsScreen
|
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.Export -> TcCard { Text("Export ist in der Android-App deaktiviert.", color = TcColors.TextMuted) }
|
||||||
AppRoute.Profile -> ProfileScreen(
|
AppRoute.Profile -> {
|
||||||
|
LaunchedEffect(route) { settingsViewModel.loadProfile() }
|
||||||
|
ProfileScreen(
|
||||||
state = settingsState,
|
state = settingsState,
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onSave = { name, stateId, weekWorkdays, dailyHours, titleType ->
|
onSave = { name, stateId, weekWorkdays, dailyHours, titleType ->
|
||||||
settingsViewModel.updateProfile(name, stateId, weekWorkdays, dailyHours, titleType)
|
settingsViewModel.updateProfile(name, stateId, weekWorkdays, dailyHours, titleType)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
AppRoute.Password -> PasswordScreen(
|
AppRoute.Password -> PasswordScreen(
|
||||||
state = settingsState,
|
state = settingsState,
|
||||||
onChange = { old, new, confirm -> settingsViewModel.changePassword(old, new, confirm) },
|
onChange = { old, new, confirm -> settingsViewModel.changePassword(old, new, confirm) },
|
||||||
)
|
)
|
||||||
AppRoute.Timewish -> TimewishScreen(
|
AppRoute.Timewish -> {
|
||||||
|
LaunchedEffect(route) { settingsViewModel.loadTimewishes() }
|
||||||
|
TimewishScreen(
|
||||||
state = settingsState,
|
state = settingsState,
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onCreate = { day, type, hours, start, end -> settingsViewModel.createTimewish(day, type, hours, start, end) },
|
onCreate = { day, type, hours, start, end -> settingsViewModel.createTimewish(day, type, hours, start, end) },
|
||||||
onDelete = { settingsViewModel.deleteTimewish(it) },
|
onDelete = { settingsViewModel.deleteTimewish(it) },
|
||||||
)
|
)
|
||||||
AppRoute.Permissions -> PermissionsScreen(
|
}
|
||||||
|
AppRoute.Permissions -> {
|
||||||
|
LaunchedEffect(route) { settingsViewModel.loadWatchers() }
|
||||||
|
PermissionsScreen(
|
||||||
state = settingsState,
|
state = settingsState,
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onAdd = { settingsViewModel.addWatcher(it) },
|
onAdd = { settingsViewModel.addWatcher(it) },
|
||||||
onDelete = { settingsViewModel.deleteWatcher(it) },
|
onDelete = { settingsViewModel.deleteWatcher(it) },
|
||||||
)
|
)
|
||||||
AppRoute.Invite -> InviteScreen(
|
}
|
||||||
|
AppRoute.Invite -> {
|
||||||
|
LaunchedEffect(route) { settingsViewModel.loadInvites() }
|
||||||
|
InviteScreen(
|
||||||
state = settingsState,
|
state = settingsState,
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
onSend = { settingsViewModel.sendInvite(it) },
|
onSend = { settingsViewModel.sendInvite(it) },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
AppRoute.Holidays -> HolidaysAdminScreen(
|
AppRoute.Holidays -> HolidaysAdminScreen(
|
||||||
state = adminState,
|
state = adminState,
|
||||||
isTablet = isTablet,
|
isTablet = isTablet,
|
||||||
@@ -298,55 +316,203 @@ private fun WeekOverviewScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WeekTablet(week: WeekOverviewDto) {
|
private fun WeekTablet(week: WeekOverviewDto) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Xl), modifier = Modifier.fillMaxWidth()) {
|
WeekTable(week, compact = false)
|
||||||
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 ?: "—")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WeekPhone(week: WeekOverviewDto) {
|
private fun WeekPhone(week: WeekOverviewDto) {
|
||||||
week.days.forEach { day -> WeekDayCard(day) }
|
WeekTable(week, compact = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WeekTable(week: WeekOverviewDto, compact: Boolean) {
|
||||||
TcCard {
|
TcCard {
|
||||||
SectionTitle("Wochensumme")
|
if (!compact) {
|
||||||
DetailRow("Arbeitszeit", week.weekTotal ?: "0:00")
|
WeekHeaderRow()
|
||||||
DetailRow("Gesamt", week.totalAll ?: week.weekTotal ?: "0:00")
|
}
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
||||||
|
week.days.forEach { day -> WeekDayRow(day, compact) }
|
||||||
|
WeekSummaryRows(week, compact)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun WeekDayCard(day: WeekDayDto) {
|
private fun WeekHeaderRow() {
|
||||||
TcCard {
|
Row(
|
||||||
SectionTitle("${day.name ?: "Tag"} ${day.date.toDisplayDate()}")
|
modifier = Modifier
|
||||||
val blocks = day.workBlocks
|
.fillMaxWidth()
|
||||||
if (blocks.isNotEmpty()) {
|
.background(TcColors.Background, RoundedCornerShape(TcRadius.Medium))
|
||||||
blocks.forEachIndexed { index, block ->
|
.padding(horizontal = TcSpacing.Md, vertical = TcSpacing.Sm),
|
||||||
val label = if (blocks.size > 1) "Arbeitszeit ${index + 1}" else "Arbeitszeit"
|
horizontalArrangement = Arrangement.spacedBy(TcSpacing.Md),
|
||||||
DetailRow(label, block.workTime ?: day.workTime ?: "—")
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
DetailRow("Netto", block.netWorkTime ?: block.totalWorkTime ?: "—")
|
) {
|
||||||
|
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 {
|
} else {
|
||||||
DetailRow("Arbeitszeit", day.workTime ?: "—")
|
Row(
|
||||||
DetailRow("Netto", day.netWorkTime ?: day.totalWorkTime ?: "—")
|
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)
|
||||||
}
|
}
|
||||||
DetailRow("Status", day.statusText ?: "—")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Composable
|
||||||
private fun CalendarDemo(isTablet: Boolean) {
|
private fun CalendarDemo(isTablet: Boolean) {
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
@@ -408,6 +574,19 @@ private fun String?.toDisplayDate(): String {
|
|||||||
}.getOrDefault(this)
|
}.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
|
@Composable
|
||||||
private fun SectionTitle(text: String) {
|
private fun SectionTitle(text: String) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.tsschulz.timeclock.ui.admin
|
package de.tsschulz.timeclock.ui.admin
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.TcRadius
|
||||||
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HolidaysAdminScreen(
|
fun HolidaysAdminScreen(
|
||||||
@@ -42,7 +46,7 @@ fun HolidaysAdminScreen(
|
|||||||
onCreate: (String, Double, String, List<String>) -> Unit,
|
onCreate: (String, Double, String, List<String>) -> Unit,
|
||||||
onDelete: (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 hours by rememberSaveable { mutableStateOf("8") }
|
||||||
var description by rememberSaveable { mutableStateOf("") }
|
var description by rememberSaveable { mutableStateOf("") }
|
||||||
var stateIds by rememberSaveable { mutableStateOf("") }
|
var stateIds by rememberSaveable { mutableStateOf("") }
|
||||||
@@ -61,8 +65,11 @@ fun HolidaysAdminScreen(
|
|||||||
stateIds = stateIds,
|
stateIds = stateIds,
|
||||||
onStateIds = { stateIds = it },
|
onStateIds = { stateIds = it },
|
||||||
onCreate = {
|
onCreate = {
|
||||||
onCreate(date, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
normalizeHolidayDate(date)?.let { isoDate ->
|
||||||
|
onCreate(isoDate, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
||||||
|
date = LocalDate.parse(isoDate).toGermanDate()
|
||||||
description = ""
|
description = ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(0.9f),
|
modifier = Modifier.weight(0.9f),
|
||||||
)
|
)
|
||||||
@@ -83,8 +90,11 @@ fun HolidaysAdminScreen(
|
|||||||
stateIds = stateIds,
|
stateIds = stateIds,
|
||||||
onStateIds = { stateIds = it },
|
onStateIds = { stateIds = it },
|
||||||
onCreate = {
|
onCreate = {
|
||||||
onCreate(date, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
normalizeHolidayDate(date)?.let { isoDate ->
|
||||||
|
onCreate(isoDate, hours.toDoubleOrNull() ?: 8.0, description, parseStateIds(stateIds))
|
||||||
|
date = LocalDate.parse(isoDate).toGermanDate()
|
||||||
description = ""
|
description = ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
HolidayList("Zukünftige Feiertage", state.futureHolidays, onDelete)
|
HolidayList("Zukünftige Feiertage", state.futureHolidays, onDelete)
|
||||||
@@ -129,15 +139,9 @@ private fun HolidayForm(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
FormCard("Feiertag hinzufügen", 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("Freie Stunden", hours, onHours, placeholder = "8")
|
||||||
TcTextField("Beschreibung", description, onDescription, placeholder = "z.B. Tag der Deutschen Einheit")
|
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()) {
|
if (state.holidayStates.isNotEmpty()) {
|
||||||
Text("Bundesländer", color = TcColors.TextMuted, fontSize = 13.sp)
|
Text("Bundesländer", color = TcColors.TextMuted, fontSize = 13.sp)
|
||||||
FlowRow(
|
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
|
@Composable
|
||||||
private fun HolidayList(title: String, holidays: List<HolidayDto>, onDelete: (String) -> Unit) {
|
private fun HolidayList(title: String, holidays: List<HolidayDto>, onDelete: (String) -> Unit) {
|
||||||
ListCard(title, holidays) { holiday -> HolidayRow(holiday, onDelete) }
|
ListCard(title, holidays) { holiday -> HolidayRow(holiday, onDelete) }
|
||||||
@@ -269,6 +303,20 @@ private fun toggleStateId(raw: String, id: String): String {
|
|||||||
return ids.joinToString(",")
|
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 {
|
private fun String?.toDisplayDate(): String {
|
||||||
if (this.isNullOrBlank()) return "-"
|
if (this.isNullOrBlank()) return "-"
|
||||||
return runCatching {
|
return runCatching {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import java.time.LocalDate
|
|||||||
import java.time.YearMonth
|
import java.time.YearMonth
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun VacationScreen(state: BookingUiState, isTablet: Boolean, onCreate: (Int, String, String?) -> Unit, onDelete: (String) -> Unit) {
|
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()) }
|
var end by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
|
||||||
Phase4Frame(loading = state.vacationLoading, error = state.vacationError) {
|
Phase4Frame(loading = state.vacationLoading, error = state.vacationError) {
|
||||||
FormCard("Urlaub eintragen", isTablet) {
|
FormCard("Urlaub eintragen", isTablet) {
|
||||||
TcTextField("Umfang (0 Zeitraum, 1 Halber Tag)", type, { type = it }, placeholder = "0")
|
FieldLabel("Umfang")
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
||||||
TcButton("Zeitraum", variant = if (type == "0") ButtonVariant.Primary else ButtonVariant.Default, onClick = { type = "0" })
|
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 = {
|
TcButton("Urlaub eintragen", variant = ButtonVariant.Primary, onClick = {
|
||||||
val typeValue = type.toIntOrNull() ?: 0
|
val typeValue = type.toIntOrNull() ?: 0
|
||||||
onCreate(typeValue, start, if (typeValue == 1) start else end)
|
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) {
|
Phase4Frame(loading = state.sickLoading, error = state.sickError) {
|
||||||
FormCard("Erkrankung eintragen", isTablet) {
|
FormCard("Erkrankung eintragen", isTablet) {
|
||||||
TcTextField("Erster Krankheitstag", start, { start = it }, placeholder = "YYYY-MM-DD")
|
DateField("Erster Krankheitstag", start, { start = it })
|
||||||
TcTextField("Letzter Krankheitstag", end, { end = it }, placeholder = "YYYY-MM-DD")
|
DateField("Letzter Krankheitstag", end, { end = it })
|
||||||
TcTextField("Krankheitstyp-ID", typeId, { typeId = it }, placeholder = state.sickTypes.joinToString { "${it.id}=${it.name}" })
|
FieldLabel("Krankheitstyp")
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm), verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm), verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
|
||||||
state.sickTypes.forEach { type ->
|
state.sickTypes.forEach { type ->
|
||||||
TcButton(
|
TcButton(
|
||||||
@@ -162,11 +171,16 @@ fun TimefixScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WorkdaysScreen(state: BookingUiState, onYear: (Int) -> Unit) {
|
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) {
|
Phase4Frame(loading = state.workdaysLoading, error = state.workdaysError) {
|
||||||
FormCard("Arbeitstage", isTablet = false) {
|
FormCard("Arbeitstage", isTablet = false) {
|
||||||
TcTextField("Jahr", year, { year = it }, placeholder = "2026")
|
YearDropdown(selectedYear) { year ->
|
||||||
TcButton("Jahr laden", variant = ButtonVariant.Primary, onClick = { year.toIntOrNull()?.let(onYear) })
|
selectedYear = year
|
||||||
|
onYear(year)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WorkdaysCard(state.workdays)
|
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
|
@Composable
|
||||||
private fun FieldLabel(label: String) {
|
private fun FieldLabel(label: String) {
|
||||||
Text(
|
Text(
|
||||||
@@ -432,11 +472,14 @@ private fun CalendarCell(day: CalendarDayDto) {
|
|||||||
.border(1.dp, TcColors.Border, RoundedCornerShape(TcRadius.Medium))
|
.border(1.dp, TcColors.Border, RoundedCornerShape(TcRadius.Medium))
|
||||||
.padding(TcSpacing.Sm),
|
.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) }
|
day.holiday?.let { Text(it, color = TcColors.Danger, fontSize = 11.sp) }
|
||||||
if (day.sick) Text("Krank", color = TcColors.Secondary, 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.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 =
|
private fun WorklogEntryDto.toWorklogLabel(): String =
|
||||||
"${time ?: "—"} - ${(action ?: "—").toActionLabel()}"
|
"${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)
|
private val Color333 = androidx.compose.ui.graphics.Color(0xFF333333)
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ val adminSections = userSections + MenuSection(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val mockStatusRows = listOf(
|
val mockStatusRows = listOf(
|
||||||
StatusRow(label = "Heute", isHeading = true),
|
StatusRow(label = "Derzeit gearbeitet", value = "04:37:18 h"),
|
||||||
StatusRow(label = "Status", value = "Arbeit läuft"),
|
StatusRow(label = "Offen", value = "03:23 h"),
|
||||||
StatusRow(label = "Beginn", value = "08:12"),
|
StatusRow(label = "Normales Arbeitsende", value = "16:42 Uhr"),
|
||||||
StatusRow(label = "Arbeitszeit", value = "04:37:18"),
|
StatusRow(label = "Überstunden (Woche)", value = "00:18 h"),
|
||||||
StatusRow(label = "Offen", value = "03:23"),
|
StatusRow(label = "Wochenarbeitszeit", value = "20:37 h"),
|
||||||
StatusRow(label = "Reguläres Ende", value = "16:42"),
|
StatusRow(label = "Bereinigtes Arbeitsende (heute)", isHeading = true),
|
||||||
|
StatusRow(label = "- Generell", value = "16:42 Uhr"),
|
||||||
)
|
)
|
||||||
|
|
||||||
val mockPrimaryAction = StatusAction("Pause starten", ButtonVariant.Success)
|
val mockPrimaryAction = StatusAction("Arbeit beenden", ButtonVariant.Danger)
|
||||||
val mockSecondaryAction = StatusAction("Gehen", ButtonVariant.Secondary)
|
val mockSecondaryAction = StatusAction("Pause beginnen", ButtonVariant.Secondary)
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package de.tsschulz.timeclock.ui.settings
|
package de.tsschulz.timeclock.ui.settings
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -18,11 +22,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import de.tsschulz.timeclock.data.api.InvitationDto
|
import de.tsschulz.timeclock.data.api.InvitationDto
|
||||||
import de.tsschulz.timeclock.data.api.ProfileDto
|
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.TimewishDto
|
||||||
import de.tsschulz.timeclock.data.api.WatcherDto
|
import de.tsschulz.timeclock.data.api.WatcherDto
|
||||||
import de.tsschulz.timeclock.ui.components.TcButton
|
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.TcRadius
|
||||||
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
import de.tsschulz.timeclock.ui.theme.TcSpacing
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
@@ -47,7 +55,7 @@ fun ProfileScreen(
|
|||||||
var stateId by rememberSaveable { mutableStateOf("") }
|
var stateId by rememberSaveable { mutableStateOf("") }
|
||||||
var weekWorkdays by rememberSaveable { mutableStateOf("5") }
|
var weekWorkdays by rememberSaveable { mutableStateOf("5") }
|
||||||
var dailyHours by rememberSaveable { mutableStateOf("8.0") }
|
var dailyHours by rememberSaveable { mutableStateOf("8.0") }
|
||||||
var preferredTitleType by rememberSaveable { mutableStateOf("0") }
|
var preferredTitleType by rememberSaveable { mutableStateOf(2) }
|
||||||
|
|
||||||
LaunchedEffect(profile) {
|
LaunchedEffect(profile) {
|
||||||
profile?.let {
|
profile?.let {
|
||||||
@@ -55,7 +63,7 @@ fun ProfileScreen(
|
|||||||
stateId = it.stateId.orEmpty()
|
stateId = it.stateId.orEmpty()
|
||||||
weekWorkdays = (it.weekWorkdays ?: 5).toString()
|
weekWorkdays = (it.weekWorkdays ?: 5).toString()
|
||||||
dailyHours = (it.dailyHours ?: 8.0).toString()
|
dailyHours = (it.dailyHours ?: 8.0).toString()
|
||||||
preferredTitleType = (it.preferredTitleType ?: 0).toString()
|
preferredTitleType = it.preferredTitleType ?: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,17 +71,22 @@ fun ProfileScreen(
|
|||||||
ResponsiveSettings(isTablet) {
|
ResponsiveSettings(isTablet) {
|
||||||
FormCard("Persönliche Daten") {
|
FormCard("Persönliche Daten") {
|
||||||
TcTextField("Name", fullName, { fullName = it })
|
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("Arbeitstage pro Woche", weekWorkdays, { weekWorkdays = it }, placeholder = "5")
|
||||||
TcTextField("Stunden pro Tag", dailyHours, { dailyHours = it }, placeholder = "8.0")
|
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 = {
|
TcButton("Speichern", variant = ButtonVariant.Primary, onClick = {
|
||||||
onSave(
|
onSave(
|
||||||
fullName,
|
fullName,
|
||||||
stateId.ifBlank { null },
|
stateId.ifBlank { null },
|
||||||
weekWorkdays.toIntOrNull() ?: 5,
|
weekWorkdays.toIntOrNull() ?: 5,
|
||||||
dailyHours.toDoubleOrNull() ?: 8.0,
|
dailyHours.toDoubleOrNull() ?: 8.0,
|
||||||
preferredTitleType.toIntOrNull() ?: 0,
|
preferredTitleType,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -113,8 +126,8 @@ fun TimewishScreen(
|
|||||||
onCreate: (Int, Int, Double?, String, String?) -> Unit,
|
onCreate: (Int, Int, Double?, String, String?) -> Unit,
|
||||||
onDelete: (String) -> Unit,
|
onDelete: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var day by rememberSaveable { mutableStateOf("1") }
|
var day by rememberSaveable { mutableStateOf(1) }
|
||||||
var wishtype by rememberSaveable { mutableStateOf("1") }
|
var wishtype by rememberSaveable { mutableStateOf(2) }
|
||||||
var hours by rememberSaveable { mutableStateOf("8.0") }
|
var hours by rememberSaveable { mutableStateOf("8.0") }
|
||||||
var startDate by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
|
var startDate by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
|
||||||
var endDate by rememberSaveable { mutableStateOf("") }
|
var endDate by rememberSaveable { mutableStateOf("") }
|
||||||
@@ -122,16 +135,21 @@ fun TimewishScreen(
|
|||||||
SettingsFrame(state) {
|
SettingsFrame(state) {
|
||||||
ResponsiveSettings(isTablet) {
|
ResponsiveSettings(isTablet) {
|
||||||
FormCard("Zeitwunsch eintragen") {
|
FormCard("Zeitwunsch eintragen") {
|
||||||
TcTextField("Wochentag", day, { day = it }, placeholder = "1")
|
WeekdayDropdown(day, { day = it })
|
||||||
TcTextField("Wunschtyp", wishtype, { wishtype = it }, placeholder = "1")
|
WishtypeDropdown(wishtype) {
|
||||||
|
wishtype = it
|
||||||
|
hours = if (it == 2) "8.0" else "0"
|
||||||
|
}
|
||||||
|
if (wishtype == 2) {
|
||||||
TcTextField("Stunden", hours, { hours = it }, placeholder = "8.0")
|
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")
|
SettingsDateField("Gültig ab", startDate, { startDate = it })
|
||||||
|
SettingsDateField("Gültig bis (optional)", endDate, { endDate = it }, allowBlank = true)
|
||||||
TcButton("Zeitwunsch speichern", variant = ButtonVariant.Primary, onClick = {
|
TcButton("Zeitwunsch speichern", variant = ButtonVariant.Primary, onClick = {
|
||||||
onCreate(
|
onCreate(
|
||||||
day.toIntOrNull() ?: 1,
|
day,
|
||||||
wishtype.toIntOrNull() ?: 1,
|
wishtype,
|
||||||
hours.toDoubleOrNull(),
|
if (wishtype == 2) hours.toDoubleOrNull() else null,
|
||||||
startDate,
|
startDate,
|
||||||
endDate.ifBlank { null },
|
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
|
@Composable
|
||||||
private fun SettingsFrame(state: SettingsUiState, content: @Composable () -> Unit) {
|
private fun SettingsFrame(state: SettingsUiState, content: @Composable () -> Unit) {
|
||||||
if (state.loading) TcLoading("Lade Daten...")
|
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
|
@Composable
|
||||||
private fun Detail(label: String, value: String) {
|
private fun Detail(label: String, value: String) {
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
fun loadPhase5() {
|
fun loadPhase5() {
|
||||||
loadProfile()
|
loadProfile()
|
||||||
loadTimewishes()
|
|
||||||
loadInvites()
|
|
||||||
loadWatchers()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadProfile() = launchLoad {
|
fun loadProfile() = launchLoad {
|
||||||
|
|||||||
@@ -22,16 +22,13 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
data class TimeUiState(
|
data class TimeUiState(
|
||||||
val loading: Boolean = false,
|
val loading: Boolean = false,
|
||||||
val clockInProgress: Boolean = false,
|
val clockInProgress: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val dashboard: TimeDashboard? = 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 primaryAction: StatusAction? = null,
|
||||||
val secondaryAction: StatusAction? = null,
|
val secondaryAction: StatusAction? = null,
|
||||||
val weekLoading: Boolean = false,
|
val weekLoading: Boolean = false,
|
||||||
@@ -169,46 +166,54 @@ class TimeViewModel(
|
|||||||
private fun TimeDashboard.toStatusRows(): List<StatusRow> {
|
private fun TimeDashboard.toStatusRows(): List<StatusRow> {
|
||||||
val stats = stats
|
val stats = stats
|
||||||
return buildList {
|
return buildList {
|
||||||
add(StatusRow(label = "Heute", isHeading = true))
|
add(StatusRow("Derzeit gearbeitet", stats.currentlyWorked.toHoursLabel()))
|
||||||
add(StatusRow("Status", state.toStatusText()))
|
add(StatusRow("Offen", stats.open.toHoursLabel()))
|
||||||
if (runningStartTime != null) add(StatusRow("Beginn", runningStartTime.toDisplayTime()))
|
add(StatusRow("Normales Arbeitsende", stats.regularEnd.toClockLabel()))
|
||||||
if (currentPauseStart != null) add(StatusRow("Pause seit", currentPauseStart.toDisplayTime()))
|
addOvertimeRow("Woche", stats.overtime)
|
||||||
add(StatusRow("Arbeitszeit", stats.currentlyWorked ?: "—"))
|
addOvertimeRow("Gesamt", stats.totalOvertime)
|
||||||
add(StatusRow("Offen", stats.open ?: "—"))
|
add(StatusRow("Wochenarbeitszeit", stats.weekWorktime.toHoursLabel()))
|
||||||
add(StatusRow("Woche", stats.weekWorktime ?: "—"))
|
add(StatusRow("Arbeitsfreie Stunden", stats.nonWorkingHours.toHoursLabel()))
|
||||||
add(StatusRow("Überstunden", stats.overtime ?: "—"))
|
add(StatusRow("Offen für Woche", stats.openForWeek.toHoursLabel()))
|
||||||
add(StatusRow("Gesamt", stats.totalOvertime ?: "—"))
|
add(StatusRow("Bereinigtes Arbeitsende (heute)", isHeading = true))
|
||||||
add(StatusRow("Arbeitsende", stats.adjustedEndTodayGeneral ?: stats.regularEnd ?: "—"))
|
add(StatusRow("- Generell", stats.adjustedEndTodayGeneral.toClockLabel()))
|
||||||
|
add(StatusRow("- Woche", stats.adjustedEndTodayWeek.toClockLabel()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TimeDashboard.primaryAction(): StatusAction? =
|
private fun TimeDashboard.primaryAction(): StatusAction? =
|
||||||
when (state) {
|
when (state) {
|
||||||
null, "stop work" -> StatusAction("Kommen", ButtonVariant.Success, "start work")
|
null, "stop work" -> StatusAction("Arbeit beginnen", ButtonVariant.Success, "start work")
|
||||||
"start work", "stop pause" -> StatusAction("Pause starten", ButtonVariant.Default, "start pause")
|
"start work", "stop pause" -> StatusAction("Arbeit beenden", ButtonVariant.Danger, "stop work")
|
||||||
"start pause" -> StatusAction("Pause beenden", ButtonVariant.Success, "stop pause")
|
"start pause" -> null
|
||||||
else -> StatusAction("Kommen", ButtonVariant.Success, "start work")
|
else -> StatusAction("Arbeit beginnen", ButtonVariant.Success, "start work")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TimeDashboard.secondaryAction(): StatusAction? =
|
private fun TimeDashboard.secondaryAction(): StatusAction? =
|
||||||
when (state) {
|
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
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String?.toStatusText(): String =
|
private fun MutableList<StatusRow>.addOvertimeRow(scope: String, value: String?) {
|
||||||
when (this) {
|
val raw = value?.takeIf { it.isNotBlank() && it != "—" } ?: "—"
|
||||||
null, "stop work" -> "Nicht eingestempelt"
|
val isNegative = raw.startsWith("-")
|
||||||
"start work" -> "Arbeit läuft"
|
val label = if (isNegative) "Fehlzeit ($scope)" else "Überstunden ($scope)"
|
||||||
"start pause" -> "Pause läuft"
|
val displayValue = if (isNegative) raw.removePrefix("-").toHoursLabel() else raw.toHoursLabel()
|
||||||
"stop pause" -> "Arbeit läuft"
|
add(StatusRow(label, displayValue))
|
||||||
else -> this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.toDisplayTime(): String =
|
private fun String?.toHoursLabel(): String {
|
||||||
runCatching {
|
val value = this?.takeIf { it.isNotBlank() } ?: return "—"
|
||||||
OffsetDateTime.parse(this).toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm", Locale.GERMANY))
|
if (value == "—" || value.contains("erreicht")) return value
|
||||||
}.getOrDefault(this)
|
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(
|
class Factory(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
|
|||||||
Reference in New Issue
Block a user