Refactor TimefixService to improve query handling and enhance error management in MainActivity. Update AndroidManifest to specify application class and increment Gradle version. Enhance BookingViewModel for better state management and loading indicators. Update UI components for improved user experience in booking screens.

This commit is contained in:
Torsten Schulz (local)
2026-05-14 22:51:48 +02:00
parent 5b6adab4cd
commit 333f90c792
11 changed files with 376 additions and 78 deletions

View File

@@ -33,15 +33,15 @@ class TimefixService {
});
// Hole auch die Timefixes für diese Einträge
const entryIds = entries.map(e => e.id);
const entryIds = entries.map(e => e.id).filter(Boolean);
if (entryIds.length === 0) {
return [];
}
const timefixesForEntries = await sequelize.query(
`SELECT worklog_id, fix_type, fix_date_time FROM timefix WHERE worklog_id IN (?)`,
`SELECT worklog_id, fix_type, fix_date_time FROM timefix WHERE worklog_id IN (:entryIds)`,
{
replacements: [entryIds],
replacements: { entryIds },
type: sequelize.QueryTypes.SELECT
}
);

View File

@@ -1,5 +1,5 @@
plugins {
id("com.android.application") version "9.1.1" apply false
id("com.android.application") version "9.2.1" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.2.20" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" apply false
}

View File

@@ -5,6 +5,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Stechuhr"
android:name=".TimeClockApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TimeClock">

View File

@@ -2,6 +2,8 @@ package de.tsschulz.timeclock
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.view.WindowCompat
@@ -18,8 +20,10 @@ class MainActivity : ComponentActivity() {
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.i(TAG, "MainActivity.onCreate")
window.statusBarColor = Color.rgb(240, 255, 236)
window.navigationBarColor = Color.WHITE
try {
setContent {
val authViewModel: AuthViewModel = viewModel(factory = AuthViewModel.Factory(application))
val timeViewModel: TimeViewModel = viewModel(factory = TimeViewModel.Factory(application))
@@ -36,10 +40,25 @@ class MainActivity : ComponentActivity() {
)
}
}
} catch (e: Throwable) {
Log.e(TAG, "Failed to create Compose content", e)
setContentView(
TextView(this).apply {
text = "Die App konnte nicht gestartet werden.\n${e.message.orEmpty()}"
setTextColor(Color.BLACK)
setBackgroundColor(Color.WHITE)
setPadding(32, 32, 32, 32)
},
)
}
window.decorView.post {
val controller = WindowCompat.getInsetsController(window, window.decorView)
controller.isAppearanceLightStatusBars = true
controller.isAppearanceLightNavigationBars = true
}
}
private companion object {
const val TAG = "TimeClockStartup"
}
}

View File

@@ -0,0 +1,20 @@
package de.tsschulz.timeclock
import android.app.Application
import android.util.Log
class TimeClockApplication : Application() {
override fun onCreate() {
super.onCreate()
Log.i(TAG, "Application started")
val previousHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Log.e(TAG, "Uncaught exception on ${thread.name}", throwable)
previousHandler?.uncaughtException(thread, throwable)
}
}
private companion object {
const val TAG = "TimeClockStartup"
}
}

View File

@@ -56,8 +56,11 @@ data class TimefixDto(
@Serializable
data class WorklogEntryDto(
val id: String,
@Serializable(with = FlexibleStringSerializer::class)
val time: String? = null,
@Serializable(with = FlexibleStringSerializer::class)
val action: String? = null,
@Serializable(with = FlexibleStringSerializer::class)
val tstamp: String? = null,
)

View File

@@ -0,0 +1,35 @@
package de.tsschulz.timeclock.data.api
import kotlinx.serialization.KSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
object FlexibleStringSerializer : KSerializer<String?> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("FlexibleString", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): String? {
val jsonDecoder = decoder as? JsonDecoder ?: return decoder.decodeString()
return when (val element = jsonDecoder.decodeJsonElement()) {
JsonNull -> null
is JsonPrimitive -> element.contentOrNull
else -> null
}
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: String?) {
if (value == null) {
encoder.encodeNull()
} else {
encoder.encodeString(value)
}
}
}

View File

@@ -94,7 +94,6 @@ fun TimeClockApp(
val sections = if (user.isAdmin) adminSections else userSections
LaunchedEffect(user.id) { timeViewModel.start() }
LaunchedEffect(user.id) { bookingViewModel.loadPhase4() }
LaunchedEffect(user.id) { settingsViewModel.loadPhase5() }
LaunchedEffect(user.id, user.isAdmin) { adminViewModel.loadPhase6(user.isAdmin) }
@@ -169,27 +168,42 @@ private fun DemoScreen(
onWeekOffset = onWeekOffset,
isTablet = isTablet,
)
AppRoute.Timefix -> TimefixScreen(
AppRoute.Timefix -> {
LaunchedEffect(route) { bookingViewModel.loadTimefix() }
TimefixScreen(
state = bookingState,
isTablet = isTablet,
onDate = { bookingViewModel.setTimefixDate(it) },
onCreate = { id, date, time, action -> bookingViewModel.createTimefix(id, date, time, action) },
onDelete = { bookingViewModel.deleteTimefix(it) },
)
AppRoute.Vacation -> VacationScreen(
}
AppRoute.Vacation -> {
LaunchedEffect(route) { bookingViewModel.loadVacations() }
VacationScreen(
state = bookingState,
isTablet = isTablet,
onCreate = { type, start, end -> bookingViewModel.createVacation(type, start, end) },
onDelete = { bookingViewModel.deleteVacation(it) },
)
AppRoute.Sick -> SickScreen(
}
AppRoute.Sick -> {
LaunchedEffect(route) { bookingViewModel.loadSick() }
SickScreen(
state = bookingState,
isTablet = isTablet,
onCreate = { type, start, end -> bookingViewModel.createSick(type, start, end) },
onDelete = { bookingViewModel.deleteSick(it) },
)
AppRoute.Workdays -> WorkdaysScreen(state = bookingState, onYear = { bookingViewModel.loadWorkdays(it) })
AppRoute.Calendar -> CalendarScreen(state = bookingState, onMonth = { bookingViewModel.changeCalendarMonth(it) })
}
AppRoute.Workdays -> {
LaunchedEffect(route) { bookingViewModel.loadWorkdays(bookingState.workdaysYear) }
WorkdaysScreen(state = bookingState, onYear = { bookingViewModel.loadWorkdays(it) })
}
AppRoute.Calendar -> {
LaunchedEffect(route) { bookingViewModel.loadCalendar(bookingState.calendarYear, bookingState.calendarMonth) }
CalendarScreen(state = bookingState, onMonth = { bookingViewModel.changeCalendarMonth(it) })
}
AppRoute.Entries -> {
LaunchedEffect(Unit) { timeViewModel.loadEntries() }
EntriesScreen(

View File

@@ -1,7 +1,10 @@
package de.tsschulz.timeclock.ui.booking
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -13,13 +16,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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
@@ -49,9 +56,13 @@ fun VacationScreen(state: BookingUiState, isTablet: Boolean, onCreate: (Int, Str
var type by rememberSaveable { mutableStateOf("0") }
var start by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
var end by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
Phase4Frame(state) {
Phase4Frame(loading = state.vacationLoading, error = state.vacationError) {
FormCard("Urlaub eintragen", isTablet) {
TcTextField("Umfang (0 Zeitraum, 1 Halber Tag)", type, { type = it }, placeholder = "0")
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" })
}
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 = {
@@ -70,11 +81,23 @@ fun SickScreen(state: BookingUiState, isTablet: Boolean, onCreate: (String, Stri
var start by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
var end by rememberSaveable { mutableStateOf(LocalDate.now().toString()) }
var typeId by rememberSaveable { mutableStateOf(state.sickTypes.firstOrNull()?.id?.toString().orEmpty()) }
Phase4Frame(state) {
LaunchedEffect(state.sickTypes) {
if (typeId.isBlank()) typeId = state.sickTypes.firstOrNull()?.id.orEmpty()
}
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}" })
FlowRow(horizontalArrangement = Arrangement.spacedBy(TcSpacing.Sm), verticalArrangement = Arrangement.spacedBy(TcSpacing.Sm)) {
state.sickTypes.forEach { type ->
TcButton(
text = type.name,
variant = if (type.id == typeId) ButtonVariant.Primary else ButtonVariant.Default,
onClick = { typeId = type.id },
)
}
}
TcButton("Erkrankung eintragen", variant = ButtonVariant.Primary, onClick = {
if (typeId.isNotBlank()) onCreate(typeId, start, end.ifBlank { start })
})
@@ -85,6 +108,7 @@ fun SickScreen(state: BookingUiState, isTablet: Boolean, onCreate: (String, Stri
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun TimefixScreen(
state: BookingUiState,
@@ -94,16 +118,38 @@ fun TimefixScreen(
onDelete: (String) -> Unit,
) {
var worklogId by rememberSaveable { mutableStateOf("") }
var newDate by rememberSaveable { mutableStateOf(state.timefixDate) }
var time by rememberSaveable { mutableStateOf("08:00") }
var action by rememberSaveable { mutableStateOf("start work") }
Phase4Frame(state) {
LaunchedEffect(state.worklogEntries) {
val selectedStillExists = state.worklogEntries.any { it.id == worklogId }
if (worklogId.isBlank() || !selectedStillExists) {
val first = state.worklogEntries.firstOrNull()
worklogId = first?.id.orEmpty()
first?.time?.let { time = it }
first?.action?.let { action = it }
}
}
LaunchedEffect(state.timefixDate) {
if (newDate.isBlank()) newDate = state.timefixDate
}
Phase4Frame(loading = state.timefixLoading, error = state.timefixError) {
FormCard("Zeitkorrektur", isTablet) {
TcTextField("Datum", state.timefixDate, onDate, placeholder = "YYYY-MM-DD")
TcTextField("Worklog-ID", worklogId, { worklogId = it }, placeholder = state.worklogEntries.firstOrNull()?.id?.toString() ?: "")
TcTextField("Neue Uhrzeit", time, { time = it }, placeholder = "HH:MM")
TcTextField("Aktion", action, { action = it }, placeholder = "start work")
DateField("Datum des Original-Eintrags", state.timefixDate, onDate)
WorklogDropdown(
entries = state.worklogEntries,
selectedId = worklogId,
onSelect = { entry ->
worklogId = entry.id
entry.time?.let { time = it }
entry.action?.let { action = it }
},
)
DateField("Neues Datum", newDate, { newDate = it })
TimeField("Neue Uhrzeit", time, { time = it })
ActionDropdown(action, { action = it })
TcButton("Korrektur erstellen", variant = ButtonVariant.Primary, onClick = {
if (worklogId.isNotBlank()) onCreate(worklogId, state.timefixDate, time, action)
if (worklogId.isNotBlank()) onCreate(worklogId, newDate, time, action)
})
if (state.worklogEntries.isEmpty()) {
Text("Für dieses Datum sind keine Worklog-Einträge vorhanden.", color = TcColors.TextMuted, fontSize = 13.sp)
@@ -117,7 +163,7 @@ fun TimefixScreen(
@Composable
fun WorkdaysScreen(state: BookingUiState, onYear: (Int) -> Unit) {
var year by rememberSaveable { mutableStateOf(state.workdaysYear.toString()) }
Phase4Frame(state) {
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) })
@@ -128,7 +174,7 @@ fun WorkdaysScreen(state: BookingUiState, onYear: (Int) -> Unit) {
@Composable
fun CalendarScreen(state: BookingUiState, onMonth: (Int) -> Unit) {
Phase4Frame(state) {
Phase4Frame(loading = state.calendarLoading, error = state.calendarError) {
TcCard {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
TcButton("", variant = ButtonVariant.Secondary, onClick = { onMonth(-1) })
@@ -147,9 +193,9 @@ fun CalendarScreen(state: BookingUiState, onMonth: (Int) -> Unit) {
}
@Composable
private fun Phase4Frame(state: BookingUiState, content: @Composable () -> Unit) {
if (state.loading) TcLoading("Lade Daten...")
state.error?.let { TcError(it) }
private fun Phase4Frame(loading: Boolean, error: String?, content: @Composable () -> Unit) {
if (loading) TcLoading("Lade Daten...")
error?.let { TcError(it) }
content()
}
@@ -161,6 +207,121 @@ private fun FormCard(title: String, isTablet: Boolean, content: @Composable Colu
}
}
@Composable
private fun DateField(label: String, value: String, onValueChange: (String) -> Unit) {
val context = LocalContext.current
val parsed = runCatching { LocalDate.parse(value) }.getOrDefault(LocalDate.now())
FieldLabel(label)
TcButton(
text = parsed.format(DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY)),
variant = ButtonVariant.Default,
modifier = Modifier.fillMaxWidth(),
onClick = {
DatePickerDialog(
context,
{ _, year, month, day ->
onValueChange(LocalDate.of(year, month + 1, day).toString())
},
parsed.year,
parsed.monthValue - 1,
parsed.dayOfMonth,
).show()
},
)
}
@Composable
private fun TimeField(label: String, value: String, onValueChange: (String) -> Unit) {
val context = LocalContext.current
val parts = value.split(":")
val hour = parts.getOrNull(0)?.toIntOrNull() ?: 8
val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0
FieldLabel(label)
TcButton(
text = "%02d:%02d".format(hour, minute),
variant = ButtonVariant.Default,
modifier = Modifier.fillMaxWidth(),
onClick = {
TimePickerDialog(
context,
{ _, selectedHour, selectedMinute ->
onValueChange("%02d:%02d".format(selectedHour, selectedMinute))
},
hour,
minute,
true,
).show()
},
)
}
@Composable
private fun WorklogDropdown(
entries: List<WorklogEntryDto>,
selectedId: String,
onSelect: (WorklogEntryDto) -> Unit,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
val selected = entries.firstOrNull { it.id == selectedId }
FieldLabel("Eintrag")
Box(modifier = Modifier.fillMaxWidth()) {
TcButton(
text = selected?.toWorklogLabel() ?: if (entries.isEmpty()) "Keine Einträge für dieses Datum" else "Bitte wählen",
variant = ButtonVariant.Default,
modifier = Modifier.fillMaxWidth(),
onClick = { if (entries.isNotEmpty()) expanded = true },
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
entries.forEach { entry ->
DropdownMenuItem(
text = { Text(entry.toWorklogLabel(), color = TcColors.Text) },
onClick = {
onSelect(entry)
expanded = false
},
)
}
}
}
}
@Composable
private fun ActionDropdown(value: String, onValueChange: (String) -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
val actions = listOf("start work", "stop work", "start pause", "stop pause")
FieldLabel("Neuer Eintrags-Typ")
Box(modifier = Modifier.fillMaxWidth()) {
TcButton(
text = value.toActionLabel(),
variant = ButtonVariant.Default,
modifier = Modifier.fillMaxWidth(),
onClick = { expanded = true },
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
actions.forEach { action ->
DropdownMenuItem(
text = { Text(action.toActionLabel(), color = TcColors.Text) },
onClick = {
onValueChange(action)
expanded = false
},
)
}
}
}
}
@Composable
private fun FieldLabel(label: String) {
Text(
text = label,
color = Color333,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
modifier = Modifier.padding(bottom = TcSpacing.Sm),
)
}
@Composable
private fun <T> ListCard(title: String, items: List<T>, row: @Composable (T) -> Unit) {
TcCard {
@@ -191,14 +352,14 @@ private fun SickRow(item: SickEntryDto, onDelete: (String) -> Unit) = DataRow(
@Composable
private fun TimefixRow(item: TimefixDto, onDelete: (String) -> Unit) = DataRow(
title = item.newAction ?: "Zeitkorrektur",
title = item.newAction?.toActionLabel() ?: "Zeitkorrektur",
details = "${item.originalDate.toDisplayDate()} ${item.originalTime ?: "—"}${item.newDate.toDisplayDate()} ${item.newTime ?: "—"}",
onDelete = { onDelete(item.id) },
)
@Composable
private fun WorklogRow(item: WorklogEntryDto) = DataRow(
title = "#${item.id} ${item.action ?: "—"}",
title = item.action?.toActionLabel() ?: "Eintrag",
details = "${item.time ?: "—"} (${item.tstamp ?: "—"})",
)
@@ -298,3 +459,17 @@ private fun String?.toDisplayDate(): String {
LocalDate.parse(this.substring(0, 10)).format(DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY))
}.getOrDefault(this)
}
private fun String.toActionLabel(): String =
when (this) {
"start work" -> "Arbeit beginnen"
"stop work" -> "Arbeit beenden"
"start pause" -> "Pause beginnen"
"stop pause" -> "Pause beenden"
else -> this
}
private fun WorklogEntryDto.toWorklogLabel(): String =
"${time ?: "—"} - ${(action ?: "—").toActionLabel()}"
private val Color333 = androidx.compose.ui.graphics.Color(0xFF333333)

View File

@@ -25,6 +25,16 @@ import java.time.YearMonth
data class BookingUiState(
val loading: Boolean = false,
val error: String? = null,
val vacationLoading: Boolean = false,
val vacationError: String? = null,
val sickLoading: Boolean = false,
val sickError: String? = null,
val timefixLoading: Boolean = false,
val timefixError: String? = null,
val workdaysLoading: Boolean = false,
val workdaysError: String? = null,
val calendarLoading: Boolean = false,
val calendarError: String? = null,
val vacations: List<VacationDto> = emptyList(),
val sickEntries: List<SickEntryDto> = emptyList(),
val sickTypes: List<SickTypeDto> = emptyList(),
@@ -46,13 +56,14 @@ class BookingViewModel(
fun loadPhase4() {
loadVacations()
loadSick()
loadTimefix()
loadWorkdays(_uiState.value.workdaysYear)
loadCalendar(_uiState.value.calendarYear, _uiState.value.calendarMonth)
}
fun loadVacations() = launchLoad { copy(vacations = repository.getVacations()) }
fun loadVacations() = launchSection(
start = { copy(vacationLoading = true, vacationError = null) },
success = { vacations -> copy(vacationLoading = false, vacationError = null, vacations = vacations) },
failure = { message -> copy(vacationLoading = false, vacationError = message) },
block = { repository.getVacations() },
)
fun createVacation(type: Int, start: String, end: String?) = launchMutation {
repository.createVacation(type, start, end)
loadVacations()
@@ -62,9 +73,12 @@ class BookingViewModel(
loadVacations()
}
fun loadSick() = launchLoad {
copy(sickEntries = repository.getSickEntries(), sickTypes = repository.getSickTypes())
}
fun loadSick() = launchSection(
start = { copy(sickLoading = true, sickError = null) },
success = { result -> copy(sickLoading = false, sickError = null, sickEntries = result.first, sickTypes = result.second) },
failure = { message -> copy(sickLoading = false, sickError = message) },
block = { repository.getSickEntries() to repository.getSickTypes() },
)
fun createSick(typeId: String, start: String, end: String?) = launchMutation {
repository.createSick(typeId, start, end)
loadSick()
@@ -79,9 +93,12 @@ class BookingViewModel(
loadTimefix(date)
}
fun loadTimefix(date: String = _uiState.value.timefixDate) = launchLoad {
copy(timefixes = repository.getTimefixes(), worklogEntries = repository.getWorklogEntries(date))
}
fun loadTimefix(date: String = _uiState.value.timefixDate) = launchSection(
start = { copy(timefixLoading = true, timefixError = null) },
success = { result -> copy(timefixLoading = false, timefixError = null, timefixes = result.first, worklogEntries = result.second) },
failure = { message -> copy(timefixLoading = false, timefixError = message) },
block = { repository.getTimefixes() to repository.getWorklogEntries(date) },
)
fun createTimefix(worklogId: String, date: String, time: String, action: String) = launchMutation {
repository.createTimefix(worklogId, date, time, action)
loadTimefix(date)
@@ -91,25 +108,39 @@ class BookingViewModel(
loadTimefix()
}
fun loadWorkdays(year: Int) = launchLoad {
copy(workdaysYear = year, workdays = repository.getWorkdays(year))
}
fun loadWorkdays(year: Int) = launchSection(
start = { copy(workdaysLoading = true, workdaysError = null, workdaysYear = year) },
success = { workdays -> copy(workdaysLoading = false, workdaysError = null, workdays = workdays) },
failure = { message -> copy(workdaysLoading = false, workdaysError = message) },
block = { repository.getWorkdays(year) },
)
fun changeCalendarMonth(delta: Int) {
val current = YearMonth.of(_uiState.value.calendarYear, _uiState.value.calendarMonth).plusMonths(delta.toLong())
loadCalendar(current.year, current.monthValue)
}
fun loadCalendar(year: Int, month: Int) = launchLoad {
copy(calendarYear = year, calendarMonth = month, calendar = repository.getCalendar(year, month))
}
fun loadCalendar(year: Int, month: Int) = launchSection(
start = { copy(calendarLoading = true, calendarError = null, calendarYear = year, calendarMonth = month) },
success = { calendar -> copy(calendarLoading = false, calendarError = null, calendar = calendar) },
failure = { message -> copy(calendarLoading = false, calendarError = message) },
block = { repository.getCalendar(year, month) },
)
private fun launchLoad(reducer: suspend BookingUiState.() -> BookingUiState) {
private fun <T> launchSection(
start: BookingUiState.() -> BookingUiState,
success: BookingUiState.(T) -> BookingUiState,
failure: BookingUiState.(String) -> BookingUiState,
block: suspend () -> T,
) {
viewModelScope.launch {
_uiState.update { it.copy(loading = true, error = null) }
runCatching { _uiState.value.reducer() }
.onSuccess { next -> _uiState.value = next.copy(loading = false, error = null) }
.onFailure { e -> _uiState.update { it.copy(loading = false, error = e.message ?: "Daten konnten nicht geladen werden") } }
_uiState.update { it.start().copy(error = null) }
runCatching { block() }
.onSuccess { value -> _uiState.update { it.success(value) } }
.onFailure { e ->
val message = e.message ?: "Daten konnten nicht geladen werden"
_uiState.update { it.failure(message) }
}
}
}

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME