Percent Split
This commit is contained in:
@@ -324,15 +324,27 @@ class EditBillActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private suspend fun saveBill(): Long = withContext(Dispatchers.IO) {
|
private suspend fun saveBill(): Long = withContext(Dispatchers.IO) {
|
||||||
val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS)
|
val groupedBillIds = intent.getLongArrayExtra(PARAM_GROUPED_BILL_IDS)
|
||||||
val isCustomSplit = viewModel.isCustomSplit
|
val splitMode = viewModel.splitMode
|
||||||
|
|
||||||
if (isCustomSplit) {
|
if (splitMode != SplitMode.EVEN) {
|
||||||
val splits: Map<Long, Double> = viewModel.owersCustomSplit.filter { (id, amountStr) ->
|
val splits: Map<Long, Double> = if (splitMode == SplitMode.CUSTOM) {
|
||||||
viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull()
|
viewModel.owersCustomSplit.filter { (id, amountStr) ->
|
||||||
?: 0.0) > 0
|
viewModel.owersSelection[id] == true && (amountStr.replace(',', '.').toDoubleOrNull()
|
||||||
}.mapValues {
|
?: 0.0) > 0
|
||||||
val uiAmount = it.value.replace(',', '.').toDoubleOrNull() ?: 0.0
|
}.mapValues {
|
||||||
SupportUtil.round2(uiAmount / viewModel.selectedCurrencyRate)
|
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
|
if (splits.isEmpty()) return@withContext 0L
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.helcel.cowspent.android.bill_edit
|
package net.helcel.cowspent.android.bill_edit
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.material.icons.filled.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
@@ -357,34 +359,54 @@ fun OwerSelectionSection(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
if (viewModel.isCustomSplit) {
|
if (viewModel.splitMode != SplitMode.EVEN) {
|
||||||
val diff = viewModel.getDiffSplit()
|
val diff = viewModel.getDiffSplit()
|
||||||
if (abs(diff) > 0.01) {
|
if (abs(diff) > 0.01) {
|
||||||
|
val unit = if (viewModel.splitMode == SplitMode.PERCENT) "%" else ""
|
||||||
val diffText =
|
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)
|
SupportUtil.normalNumberFormat.format(-diff)
|
||||||
}"
|
}$unit"
|
||||||
Text(
|
Text(
|
||||||
diffText,
|
diffText,
|
||||||
color = MaterialTheme.colors.error,
|
color = MaterialTheme.colors.error,
|
||||||
fontSize = 12.sp,
|
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,
|
val splitOptions = listOf(SplitMode.EVEN, SplitMode.CUSTOM, SplitMode.PERCENT)
|
||||||
enabled = canEdit,
|
Row(
|
||||||
onCheckedChange = {
|
modifier = Modifier
|
||||||
viewModel.isCustomSplit = !it
|
.padding(start = 8.dp)
|
||||||
viewModel.updateSplits()
|
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.05f), MaterialTheme.shapes.small)
|
||||||
},
|
) {
|
||||||
colors = SwitchDefaults.colors(
|
splitOptions.forEach { mode ->
|
||||||
uncheckedThumbColor = MaterialTheme.colors.onSurface,
|
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 ->
|
viewModel.members.forEach { member ->
|
||||||
@@ -410,12 +432,22 @@ fun OwerSelectionSection(
|
|||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(member.name, modifier = Modifier.weight(1f))
|
Text(member.name, modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
if (isSelected || viewModel.isCustomSplit) {
|
if (isSelected || viewModel.splitMode != SplitMode.EVEN) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val value = if (viewModel.splitMode == SplitMode.PERCENT) {
|
||||||
|
viewModel.owersPercentSplit[member.id] ?: ""
|
||||||
|
} else {
|
||||||
|
viewModel.owersCustomSplit[member.id] ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = viewModel.owersCustomSplit[member.id] ?: "",
|
value = value,
|
||||||
onValueChange = {
|
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 != "")
|
viewModel.owersSelection[member.id] = (it != "")
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -423,7 +455,7 @@ fun OwerSelectionSection(
|
|||||||
.height(46.dp),
|
.height(46.dp),
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = viewModel.isCustomSplit && canEdit,
|
enabled = viewModel.splitMode != SplitMode.EVEN && canEdit,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
cursorBrush = SolidColor(MaterialTheme.colors.onSurface),
|
cursorBrush = SolidColor(MaterialTheme.colors.onSurface),
|
||||||
textStyle = LocalTextStyle.current.copy(
|
textStyle = LocalTextStyle.current.copy(
|
||||||
@@ -432,13 +464,13 @@ fun OwerSelectionSection(
|
|||||||
),
|
),
|
||||||
) { innerTextField ->
|
) { innerTextField ->
|
||||||
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
||||||
value = viewModel.owersCustomSplit[member.id] ?: "",
|
value = value,
|
||||||
visualTransformation = VisualTransformation.None,
|
visualTransformation = VisualTransformation.None,
|
||||||
innerTextField = innerTextField,
|
innerTextField = innerTextField,
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
enabled = viewModel.isCustomSplit && canEdit,
|
enabled = viewModel.splitMode != SplitMode.EVEN && canEdit,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp),
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp),
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
cursorColor = MaterialTheme.colors.onSurface,
|
cursorColor = MaterialTheme.colors.onSurface,
|
||||||
backgroundColor = Color.Transparent,
|
backgroundColor = Color.Transparent,
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import net.helcel.cowspent.model.DBMember
|
|||||||
import net.helcel.cowspent.util.SupportUtil
|
import net.helcel.cowspent.util.SupportUtil
|
||||||
import net.helcel.cowspent.util.evalMath
|
import net.helcel.cowspent.util.evalMath
|
||||||
|
|
||||||
|
enum class SplitMode {
|
||||||
|
EVEN, CUSTOM, PERCENT
|
||||||
|
}
|
||||||
|
|
||||||
class EditBillViewModel : ViewModel() {
|
class EditBillViewModel : ViewModel() {
|
||||||
var what by mutableStateOf("")
|
var what by mutableStateOf("")
|
||||||
var amount by mutableStateOf("")
|
var amount by mutableStateOf("")
|
||||||
@@ -34,8 +38,9 @@ class EditBillViewModel : ViewModel() {
|
|||||||
var members by mutableStateOf<List<DBMember>>(emptyList())
|
var members by mutableStateOf<List<DBMember>>(emptyList())
|
||||||
|
|
||||||
var owersSelection = mutableStateMapOf<Long, Boolean>()
|
var owersSelection = mutableStateMapOf<Long, Boolean>()
|
||||||
var isCustomSplit by mutableStateOf(false)
|
var splitMode by mutableStateOf(SplitMode.EVEN)
|
||||||
var owersCustomSplit = mutableStateMapOf<Long, String>()
|
var owersCustomSplit = mutableStateMapOf<Long, String>()
|
||||||
|
var owersPercentSplit = mutableStateMapOf<Long, String>()
|
||||||
|
|
||||||
var dialogState by mutableStateOf<DialogState?>(null)
|
var dialogState by mutableStateOf<DialogState?>(null)
|
||||||
|
|
||||||
@@ -54,7 +59,7 @@ class EditBillViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateSplits() {
|
fun updateSplits() {
|
||||||
if (!isCustomSplit) {
|
if (splitMode == SplitMode.EVEN) {
|
||||||
val even = getEvenSplit()
|
val even = getEvenSplit()
|
||||||
val evenStr = if (even == 0.0) "" else SupportUtil.round2(even).toString()
|
val evenStr = if (even == 0.0) "" else SupportUtil.round2(even).toString()
|
||||||
members.forEach { m ->
|
members.forEach { m ->
|
||||||
@@ -64,18 +69,36 @@ class EditBillViewModel : ViewModel() {
|
|||||||
owersCustomSplit.remove(m.id)
|
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) {
|
fun toggleMember(id: Long, selected: Boolean) {
|
||||||
owersSelection[id] = selected
|
owersSelection[id] = selected
|
||||||
if (isCustomSplit) {
|
if (splitMode != SplitMode.EVEN) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
if (owersCustomSplit[id].isNullOrEmpty()) {
|
if (splitMode == SplitMode.CUSTOM && owersCustomSplit[id].isNullOrEmpty()) {
|
||||||
owersCustomSplit[id] = "0"
|
owersCustomSplit[id] = "0"
|
||||||
|
} else if (splitMode == SplitMode.PERCENT && owersPercentSplit[id].isNullOrEmpty()) {
|
||||||
|
owersPercentSplit[id] = "0"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
owersCustomSplit.remove(id)
|
owersCustomSplit.remove(id)
|
||||||
|
owersPercentSplit.remove(id)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateSplits()
|
updateSplits()
|
||||||
@@ -83,10 +106,17 @@ class EditBillViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getDiffSplit(): Double {
|
fun getDiffSplit(): Double {
|
||||||
val customTotal = owersCustomSplit.entries
|
return if (splitMode == SplitMode.PERCENT) {
|
||||||
.filter { owersSelection[it.key] == true }
|
val customTotal = owersPercentSplit.entries
|
||||||
.sumOf { it.value.replace(',', '.').toDoubleOrNull() ?: 0.0 }
|
.filter { owersSelection[it.key] == true }
|
||||||
return amountAsDouble - customTotal
|
.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<Long> {
|
fun getOwersIds(): List<Long> {
|
||||||
@@ -204,9 +234,10 @@ class EditBillViewModel : ViewModel() {
|
|||||||
|
|
||||||
owersSelection.clear()
|
owersSelection.clear()
|
||||||
owersCustomSplit.clear()
|
owersCustomSplit.clear()
|
||||||
|
owersPercentSplit.clear()
|
||||||
|
|
||||||
if (customSplits != null) {
|
if (customSplits != null) {
|
||||||
isCustomSplit = true
|
splitMode = SplitMode.CUSTOM
|
||||||
for (member in members) {
|
for (member in members) {
|
||||||
val selected = customSplits.containsKey(member.id)
|
val selected = customSplits.containsKey(member.id)
|
||||||
owersSelection[member.id] = selected
|
owersSelection[member.id] = selected
|
||||||
@@ -222,6 +253,7 @@ class EditBillViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Even split logic
|
// Even split logic
|
||||||
|
splitMode = SplitMode.EVEN
|
||||||
val billOwerIds = bill.billOwersIds
|
val billOwerIds = bill.billOwersIds
|
||||||
val selectedCount = billOwerIds.size
|
val selectedCount = billOwerIds.size
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user