From 42f67dfbe469cd40eac81ec19e262ab426415b01 Mon Sep 17 00:00:00 2001 From: choelzl Date: Wed, 2 Sep 2026 00:17:42 +0200 Subject: [PATCH] Percent Split --- .../android/bill_edit/EditBillActivity.kt | 28 +++++-- .../android/bill_edit/EditBillScreen.kt | 80 +++++++++++++------ .../android/bill_edit/EditBillViewModel.kt | 50 +++++++++--- 3 files changed, 117 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt index e29c411..9a3027b 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -324,15 +324,27 @@ class EditBillActivity : AppCompatActivity() { private suspend fun saveBill(): Long = withContext(Dispatchers.IO) { val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS) - val isCustomSplit = viewModel.isCustomSplit + val splitMode = viewModel.splitMode - if (isCustomSplit) { - val splits: Map = viewModel.owersCustomSplit.filter { (id, amountStr) -> - viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull() - ?: 0.0) > 0 - }.mapValues { - val uiAmount = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 - SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate) + if (splitMode != SplitMode.EVEN) { + val splits: Map = if (splitMode == SplitMode.CUSTOM) { + viewModel.owersCustomSplit.filter { (id, amountStr) -> + viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull() + ?: 0.0) > 0 + }.mapValues { + val uiAmount = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 + SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate) + } + } else { // PERCENT + val totalAmount = viewModel.amountAsDouble + viewModel.owersPercentSplit.filter { (id, percentStr) -> + viewModel.owersSelection[id] == true && (percentStr.replace(',', '.').toDoubleOrNull() + ?: 0.0) > 0 + }.mapValues { + val percent = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 + val uiAmount = totalAmount * percent / 100.0 + SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate) + } } if (splits.isEmpty()) return@withContext 0L diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt index 42fddcc..eb255f6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt @@ -1,6 +1,7 @@ package net.helcel.cowspent.android.bill_edit import android.annotation.SuppressLint +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -16,6 +17,7 @@ import androidx.compose.material.icons.filled.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor @@ -357,34 +359,54 @@ fun OwerSelectionSection( ) Spacer(Modifier.weight(1f)) - if (viewModel.isCustomSplit) { + if (viewModel.splitMode != SplitMode.EVEN) { val diff = viewModel.getDiffSplit() if (abs(diff) > 0.01) { + val unit = if (viewModel.splitMode == SplitMode.PERCENT) "%" else "" val diffText = - if (diff > 0) "Missing: ${SupportUtil.normalNumberFormat.format(diff)}" else "Excess: ${ + if (diff > 0) "Missing: ${SupportUtil.normalNumberFormat.format(diff)}$unit" else "Excess: ${ SupportUtil.normalNumberFormat.format(-diff) - }" + }$unit" Text( diffText, color = MaterialTheme.colors.error, fontSize = 12.sp, - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(horizontal = 4.dp) ) } - } else { - Text("Even Split", fontSize = 12.sp) } - Switch( - checked = !viewModel.isCustomSplit, - enabled = canEdit, - onCheckedChange = { - viewModel.isCustomSplit = !it - viewModel.updateSplits() - }, - colors = SwitchDefaults.colors( - uncheckedThumbColor = MaterialTheme.colors.onSurface, - ) - ) + + val splitOptions = listOf(SplitMode.EVEN, SplitMode.CUSTOM, SplitMode.PERCENT) + Row( + modifier = Modifier + .padding(start = 8.dp) + .background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f), MaterialTheme.shapes.small) + ) { + splitOptions.forEach { mode -> + val isSelected = viewModel.splitMode == mode + Box( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .background(if (isSelected) MaterialTheme.colors.primary else Color.Transparent) + .clickable(enabled = canEdit) { + viewModel.splitMode = mode + viewModel.updateSplits() + } + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = when (mode) { + SplitMode.EVEN -> "=" + SplitMode.CUSTOM -> "#" + SplitMode.PERCENT -> "%" + }, + color = if (isSelected) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onSurface, + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + } + } } viewModel.members.forEach { member -> @@ -410,12 +432,22 @@ fun OwerSelectionSection( Spacer(modifier = Modifier.width(8.dp)) Text(member.name, modifier = Modifier.weight(1f)) - if (isSelected || viewModel.isCustomSplit) { + if (isSelected || viewModel.splitMode != SplitMode.EVEN) { val interactionSource = remember { MutableInteractionSource() } + val value = if (viewModel.splitMode == SplitMode.PERCENT) { + viewModel.owersPercentSplit[member.id] ?: "" + } else { + viewModel.owersCustomSplit[member.id] ?: "" + } + BasicTextField( - value = viewModel.owersCustomSplit[member.id] ?: "", + value = value, onValueChange = { - viewModel.owersCustomSplit[member.id] = it + if (viewModel.splitMode == SplitMode.PERCENT) { + viewModel.owersPercentSplit[member.id] = it + } else { + viewModel.owersCustomSplit[member.id] = it + } viewModel.owersSelection[member.id] = (it != "") }, modifier = Modifier @@ -423,7 +455,7 @@ fun OwerSelectionSection( .height(46.dp), interactionSource = interactionSource, singleLine = true, - enabled = viewModel.isCustomSplit && canEdit, + enabled = viewModel.splitMode != SplitMode.EVEN && canEdit, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), cursorBrush = SolidColor(MaterialTheme.colors.onSurface), textStyle = LocalTextStyle.current.copy( @@ -432,13 +464,13 @@ fun OwerSelectionSection( ), ) { innerTextField -> TextFieldDefaults.OutlinedTextFieldDecorationBox( - value = viewModel.owersCustomSplit[member.id] ?: "", + value = value, visualTransformation = VisualTransformation.None, innerTextField = innerTextField, singleLine = true, - enabled = viewModel.isCustomSplit && canEdit, + enabled = viewModel.splitMode != SplitMode.EVEN && canEdit, interactionSource = interactionSource, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), colors = TextFieldDefaults.textFieldColors( cursorColor = MaterialTheme.colors.onSurface, backgroundColor = Color.Transparent, diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt index 976c746..95844fa 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillViewModel.kt @@ -16,6 +16,10 @@ import net.helcel.cowspent.model.DBMember import net.helcel.cowspent.util.SupportUtil import net.helcel.cowspent.util.evalMath +enum class SplitMode { + EVEN, CUSTOM, PERCENT +} + class EditBillViewModel : ViewModel() { var what by mutableStateOf("") var amount by mutableStateOf("") @@ -34,8 +38,9 @@ class EditBillViewModel : ViewModel() { var members by mutableStateOf>(emptyList()) var owersSelection = mutableStateMapOf() - var isCustomSplit by mutableStateOf(false) + var splitMode by mutableStateOf(SplitMode.EVEN) var owersCustomSplit = mutableStateMapOf() + var owersPercentSplit = mutableStateMapOf() var dialogState by mutableStateOf(null) @@ -54,7 +59,7 @@ class EditBillViewModel : ViewModel() { } fun updateSplits() { - if (!isCustomSplit) { + if (splitMode == SplitMode.EVEN) { val even = getEvenSplit() val evenStr = if (even == 0.0) "" else SupportUtil.round2(even).toString() members.forEach { m -> @@ -64,18 +69,36 @@ class EditBillViewModel : ViewModel() { owersCustomSplit.remove(m.id) } } + } else if (splitMode == SplitMode.PERCENT) { + val selectedCount = owersSelection.count { it.value } + if (selectedCount > 0) { + val evenPercent = 100.0 / selectedCount + val evenPercentStr = SupportUtil.round2(evenPercent).toString() + members.forEach { m -> + if (owersSelection[m.id] == true) { + if (owersPercentSplit[m.id].isNullOrEmpty()) { + owersPercentSplit[m.id] = evenPercentStr + } + } else { + owersPercentSplit.remove(m.id) + } + } + } } } fun toggleMember(id: Long, selected: Boolean) { owersSelection[id] = selected - if (isCustomSplit) { + if (splitMode != SplitMode.EVEN) { if (selected) { - if (owersCustomSplit[id].isNullOrEmpty()) { + if (splitMode == SplitMode.CUSTOM && owersCustomSplit[id].isNullOrEmpty()) { owersCustomSplit[id] = "0" + } else if (splitMode == SplitMode.PERCENT && owersPercentSplit[id].isNullOrEmpty()) { + owersPercentSplit[id] = "0" } } else { owersCustomSplit.remove(id) + owersPercentSplit.remove(id) } } else { updateSplits() @@ -83,10 +106,17 @@ class EditBillViewModel : ViewModel() { } fun getDiffSplit(): Double { - val customTotal = owersCustomSplit.entries - .filter { owersSelection[it.key] == true } - .sumOf { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 } - return amountAsDouble - customTotal + return if (splitMode == SplitMode.PERCENT) { + val customTotal = owersPercentSplit.entries + .filter { owersSelection[it.key] == true } + .sumOf { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 } + 100.0 - customTotal + } else { + val customTotal = owersCustomSplit.entries + .filter { owersSelection[it.key] == true } + .sumOf { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 } + amountAsDouble - customTotal + } } fun getOwersIds(): List { @@ -204,9 +234,10 @@ class EditBillViewModel : ViewModel() { owersSelection.clear() owersCustomSplit.clear() + owersPercentSplit.clear() if (customSplits != null) { - isCustomSplit = true + splitMode = SplitMode.CUSTOM for (member in members) { val selected = customSplits.containsKey(member.id) owersSelection[member.id] = selected @@ -222,6 +253,7 @@ class EditBillViewModel : ViewModel() { } } else { // Even split logic + splitMode = SplitMode.EVEN val billOwerIds = bill.billOwersIds val selectedCount = billOwerIds.size