feat: Add excludeFromBilling option for diary dates and update related functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

This commit is contained in:
Torsten Schulz (local)
2026-05-30 16:10:34 +02:00
parent 25f3802d66
commit 9d9481ac76
12 changed files with 208 additions and 25 deletions

View File

@@ -1588,6 +1588,7 @@ private fun DiaryListScreen(
var newDiaryDateStr by rememberSaveable { mutableStateOf("") }
var newDiaryStart by rememberSaveable { mutableStateOf("") }
var newDiaryEnd by rememberSaveable { mutableStateOf("") }
var newDiaryExcludeFromBilling by rememberSaveable { mutableStateOf(false) }
val newDateScope = rememberCoroutineScope()
var newDateScheduleGroups by remember { mutableStateOf<List<TrainingGroupDto>>(emptyList()) }
var newDateScheduleLoading by remember { mutableStateOf(false) }
@@ -1735,6 +1736,7 @@ private fun DiaryListScreen(
newDiaryDateStr = kotlin.runCatching { java.time.LocalDate.now().toString() }.getOrElse { "" }
newDiaryStart = diaryTimeForFormField(tmpl?.trainingStart).ifBlank { "17:30" }
newDiaryEnd = diaryTimeForFormField(tmpl?.trainingEnd).ifBlank { "19:30" }
newDiaryExcludeFromBilling = false
showNewDateDialog = true
},
modifier = Modifier.heightIn(min = TouchMinHeight),
@@ -1774,6 +1776,7 @@ private fun DiaryListScreen(
slot.date,
diaryTimeFieldToApi(slot.trainingStart),
diaryTimeFieldToApi(slot.trainingEnd),
excludeFromBilling = false,
)
quickCreateBusy = false
if (id != null) {
@@ -1922,6 +1925,20 @@ private fun DiaryListScreen(
enabled = !diaryState.isLoading,
modifier = Modifier.fillMaxWidth(),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("Nicht abrechnen")
Switch(
checked = newDiaryExcludeFromBilling,
onCheckedChange = { newDiaryExcludeFromBilling = it },
enabled = !diaryState.isLoading,
)
}
diaryState.error?.let { err ->
Text(
err,
@@ -1941,6 +1958,7 @@ private fun DiaryListScreen(
newDiaryDateStr.trim(),
diaryTimeFieldToApi(newDiaryStart),
diaryTimeFieldToApi(newDiaryEnd),
newDiaryExcludeFromBilling,
)
if (id != null) {
showNewDateDialog = false
@@ -2775,10 +2793,11 @@ private fun DiaryDetailScreen(
initialDate = entry.date.take(10),
initialStart = entry.trainingStart.orEmpty(),
initialEnd = entry.trainingEnd.orEmpty(),
initialExcludeFromBilling = entry.excludeFromBilling,
submitLabel = tr("common.save", "Speichern"),
onSubmit = { _, start, end ->
onSubmit = { _, start, end, excludeFromBilling ->
dependencies.applicationScope.launch {
dependencies.diaryManager.updateTimes(clubId, entry.id, start, end)
dependencies.diaryManager.updateTimes(clubId, entry.id, start, end, excludeFromBilling)
showEdit = false
}
},
@@ -6878,14 +6897,16 @@ private fun DeleteAccountDialog(
@Composable
private fun DiaryEditForm(
submitLabel: String,
onSubmit: (date: String, trainingStart: String?, trainingEnd: String?) -> Unit,
onSubmit: (date: String, trainingStart: String?, trainingEnd: String?, excludeFromBilling: Boolean) -> Unit,
initialDate: String = "",
initialStart: String = "",
initialEnd: String = "",
initialExcludeFromBilling: Boolean = false,
) {
var date by rememberSaveable { mutableStateOf(initialDate) }
var start by rememberSaveable { mutableStateOf(initialStart) }
var end by rememberSaveable { mutableStateOf(initialEnd) }
var excludeFromBilling by rememberSaveable { mutableStateOf(initialExcludeFromBilling) }
var error by rememberSaveable { mutableStateOf<String?>(null) }
Card(modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), elevation = 2.dp) {
@@ -6895,6 +6916,17 @@ private fun DiaryEditForm(
OutlinedTextField(value = start, onValueChange = { start = it }, label = { Text("Start") }, modifier = Modifier.weight(1f), singleLine = true)
OutlinedTextField(value = end, onValueChange = { end = it }, label = { Text("Ende") }, modifier = Modifier.weight(1f), singleLine = true)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("Nicht abrechnen")
Switch(
checked = excludeFromBilling,
onCheckedChange = { excludeFromBilling = it },
)
}
ErrorText(error)
Button(onClick = {
if (!date.matches(Regex("\\d{4}-\\d{2}-\\d{2}"))) {
@@ -6906,7 +6938,7 @@ private fun DiaryEditForm(
return@Button
}
error = null
onSubmit(date, start.takeIf { it.isNotBlank() }, end.takeIf { it.isNotBlank() })
onSubmit(date, start.takeIf { it.isNotBlank() }, end.takeIf { it.isNotBlank() }, excludeFromBilling)
}) {
Text(submitLabel)
}

View File

@@ -197,6 +197,65 @@ private fun GlobalOrdersScreen(dependencies: AppDependencies, onBack: () -> Unit
}
}
fun quickSaveFlags(orderId: Int, nextStatus: String? = null, nextPaidConfirmed: Boolean? = null) {
val snapshot = rows.firstOrNull { it.order.id == orderId } ?: return
val patchedStatus = nextStatus ?: snapshot.draftStatus
val patchedPaidConfirmed = nextPaidConfirmed ?: snapshot.draftPaidConfirmed
val memberId = snapshot.order.memberId ?: return
val orderClubId = snapshot.order.clubId ?: return
rows = rows.map {
if (it.order.id == orderId) {
it.copy(
draftStatus = patchedStatus,
draftPaidConfirmed = patchedPaidConfirmed,
)
} else {
it
}
}
scope.launch {
savingIds = savingIds + orderId
runCatching {
dependencies.memberOrdersApi.update(
clubId = orderClubId,
memberId = memberId,
orderId = orderId,
body = MemberOrderPatchBody(
// Nur Status-Flags sofort speichern; restliche Felder bleiben Draft bis "Speichern".
item = snapshot.order.item,
status = patchedStatus,
cost = normalizeAmount(snapshot.order.cost),
paidAmount = normalizeAmount(snapshot.order.paidAmount),
budget = normalizeAmount(snapshot.order.budget),
paidConfirmed = patchedPaidConfirmed,
),
).order
}.onSuccess { updated ->
if (updated != null) {
rows = rows.map { row ->
if (row.order.id == orderId) {
row.copy(
order = updated,
draftStatus = updated.status,
draftPaidConfirmed = updated.paidConfirmed,
)
} else {
row
}
}
}
}.onFailure { e ->
rows = rows.map {
if (it.order.id == orderId) snapshot else it
}
err = e.message
}
savingIds = savingIds - orderId
}
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -338,8 +397,9 @@ private fun GlobalOrdersScreen(dependencies: AppDependencies, onBack: () -> Unit
orderStatuses.forEach { (v, k) ->
TextButton(
onClick = {
rows = rows.map { if (it.order.id == o.id) it.copy(draftStatus = v) else it }
quickSaveFlags(orderId = o.id, nextStatus = v)
},
enabled = !savingIds.contains(o.id),
) {
Text(
tr(k, v),
@@ -373,8 +433,9 @@ private fun GlobalOrdersScreen(dependencies: AppDependencies, onBack: () -> Unit
Switch(
checked = row.draftPaidConfirmed,
onCheckedChange = { v ->
rows = rows.map { if (it.order.id == o.id) it.copy(draftPaidConfirmed = v) else it }
quickSaveFlags(orderId = o.id, nextPaidConfirmed = v)
},
enabled = !savingIds.contains(o.id),
)
Text(tr("orders.paidConfirmed", "Bezahlt bestätigt"), modifier = Modifier.padding(start = 8.dp))
}