Implemented different kinds of grouping the transactions

This commit is contained in:
dankito 2024-09-06 16:14:12 +02:00
parent 43bd89a047
commit d792384efc
10 changed files with 146 additions and 23 deletions

View File

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.RoundedCornersCard import net.codinux.banking.ui.forms.RoundedCornersCard
import net.codinux.banking.ui.forms.Select import net.codinux.banking.ui.forms.Select
@ -17,11 +18,11 @@ private val uiState = DI.uiState
private val uiSettings = DI.uiSettings private val uiSettings = DI.uiSettings
val labelsWidth = 60.dp private val labelsWidth = 60.dp
val selectBoxesWidth = 154.dp private val selectBoxesWidth = 154.dp
val horizontalPadding = 6.dp private val horizontalPadding = 6.dp
@Composable @Composable
fun FilterBar() { fun FilterBar() {
@ -29,6 +30,8 @@ fun FilterBar() {
val transactionsFilter = uiState.transactionsFilter.collectAsState() val transactionsFilter = uiState.transactionsFilter.collectAsState()
val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
val filterService = DI.accountTransactionsFilterService val filterService = DI.accountTransactionsFilterService
val years by remember(transactions) { derivedStateOf { filterService.getYearForWhichWeHaveTransactions(transactions.value).sorted() + listOf(null as? Int) } } val years by remember(transactions) { derivedStateOf { filterService.getYearForWhichWeHaveTransactions(transactions.value).sorted() + listOf(null as? Int) } }
@ -43,6 +46,19 @@ fun FilterBar() {
Column(Modifier.height(166.dp).width(390.dp)) { Column(Modifier.height(166.dp).width(390.dp)) {
RoundedCornersCard(cornerSize = 4.dp, shadowElevation = 24.dp) { RoundedCornersCard(cornerSize = 4.dp, shadowElevation = 24.dp) {
Column(Modifier.fillMaxWidth().background(Color.White).padding(16.dp)) { Column(Modifier.fillMaxWidth().background(Color.White).padding(16.dp)) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("Umsätze", Modifier.width(labelsWidth))
Select(
label = "Gruppieren",
items = TransactionsGrouping.entries,
selectedItem = transactionsGrouping,
onSelectedItemChanged = { grouping -> uiSettings.transactionsGrouping.value = grouping },
getItemDisplayText = { grouping -> Internationalization.translate(grouping) },
modifier = Modifier.width(selectBoxesWidth).padding(horizontal = horizontalPadding)
)
}
Row(Modifier.fillMaxWidth().padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth().padding(top = 8.dp), verticalAlignment = Alignment.CenterVertically) {
Text("Zeitraum", Modifier.width(labelsWidth)) Text("Zeitraum", Modifier.width(labelsWidth))

View File

@ -1,13 +1,19 @@
package net.codinux.banking.ui.composables.settings package net.codinux.banking.ui.composables.settings
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.BooleanOption import net.codinux.banking.ui.forms.BooleanOption
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.model.TransactionsGrouping
@Composable @Composable
fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) { fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
@ -15,7 +21,7 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
val showBalance by uiSettings.showBalance.collectAsState() val showBalance by uiSettings.showBalance.collectAsState()
val groupTransactions by uiSettings.groupTransactions.collectAsState() val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
val zebraStripes by uiSettings.zebraStripes.collectAsState() val zebraStripes by uiSettings.zebraStripes.collectAsState()
@ -27,13 +33,25 @@ fun UiSettings(modifier: Modifier, textColor: Color = Color.Unspecified) {
Column(modifier) { Column(modifier) {
BooleanOption("Kontostand anzeigen", showBalance, textColor = textColor) { uiSettings.showBalance.value = it } BooleanOption("Kontostand anzeigen", showBalance, textColor = textColor) { uiSettings.showBalance.value = it }
BooleanOption("Umsätze gruppieren", groupTransactions, textColor = textColor) { uiSettings.groupTransactions.value = it }
BooleanOption("Zebra Stripes", zebraStripes, textColor = textColor) { uiSettings.zebraStripes.value = it } BooleanOption("Zebra Stripes", zebraStripes, textColor = textColor) { uiSettings.zebraStripes.value = it }
BooleanOption("Bank Icons anzeigen", showBankIcons, textColor = textColor) { uiSettings.showBankIcons.value = it } BooleanOption("Bank Icons anzeigen", showBankIcons, textColor = textColor) { uiSettings.showBankIcons.value = it }
BooleanOption("Umsätze farbig anzeigen", showColoredAmounts, textColor = textColor) { uiSettings.showColoredAmounts.value = it } BooleanOption("Umsätze farbig anzeigen", showColoredAmounts, textColor = textColor) { uiSettings.showColoredAmounts.value = it }
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("Umsätze gruppieren", color = textColor)
Select(
label = "",
items = TransactionsGrouping.entries,
selectedItem = transactionsGrouping,
onSelectedItemChanged = { grouping -> uiSettings.transactionsGrouping.value = grouping },
getItemDisplayText = { grouping -> Internationalization.translate(grouping) },
textColor = textColor,
modifier = Modifier.width(175.dp).padding(horizontal = 6.dp)
)
}
} }
} }

View File

@ -12,12 +12,13 @@ import androidx.compose.ui.graphics.Color
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 kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
import net.codinux.banking.dataaccess.entities.UserAccountEntity import net.codinux.banking.dataaccess.entities.UserAccountEntity
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.forms.RoundedCornersCard import net.codinux.banking.ui.forms.RoundedCornersCard
import net.codinux.banking.ui.model.AccountTransactionViewModel import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.TransactionsGrouping
import net.codinux.banking.ui.service.TransactionsGroupingService
private val calculator = DI.calculator private val calculator = DI.calculator
@ -27,21 +28,23 @@ private val formatUtil = DI.formatUtil
fun GroupedTransactionsListItems( fun GroupedTransactionsListItems(
modifier: Modifier, modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>, transactionsToDisplay: List<AccountTransactionViewModel>,
userAccountsId: Map<Long, UserAccountEntity> userAccountsId: Map<Long, UserAccountEntity>,
transactionsGrouping: TransactionsGrouping
) { ) {
val groupingService = remember { TransactionsGroupingService() }
val groupedByMonth by remember(transactionsToDisplay) { val groupedByDate by remember(transactionsToDisplay, transactionsGrouping) {
derivedStateOf { transactionsToDisplay.groupBy { LocalDate(it.valueDate.year, it.valueDate.monthNumber, 1) } } derivedStateOf { transactionsToDisplay.groupBy { groupingService.getKeyForGroup(it, transactionsGrouping) } }
} }
val showColoredAmounts by DI.uiSettings.showColoredAmounts.collectAsState() val showColoredAmounts by DI.uiSettings.showColoredAmounts.collectAsState()
LazyColumn(modifier, contentPadding = PaddingValues(bottom = 12.dp)) { // padding bottom = add the space the FAB sticks into the content area (= 26 - the 16 we add at the bottom of the expenses line) LazyColumn(modifier, contentPadding = PaddingValues(bottom = 12.dp)) { // padding bottom = add the space the FAB sticks into the content area (= 26 - the 16 we add at the bottom of the expenses line)
items(groupedByMonth.keys.sortedDescending()) { month -> items(groupedByDate.keys.sortedDescending()) { groupingDate ->
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
Text( Text(
text = DI.formatUtil.formatMonth(month), text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp), modifier = Modifier.padding(top = 8.dp, bottom = 2.dp),
@ -49,8 +52,7 @@ fun GroupedTransactionsListItems(
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
val monthTransactions = val monthTransactions = groupedByDate[groupingDate].orEmpty().sortedByDescending { it.valueDate }
groupedByMonth[month].orEmpty().sortedByDescending { it.valueDate }
RoundedCornersCard { RoundedCornersCard {
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.model.TransactionsGrouping
import net.codinux.banking.ui.settings.UiSettings import net.codinux.banking.ui.settings.UiSettings
import net.codinux.banking.ui.state.UiState import net.codinux.banking.ui.state.UiState
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
@ -39,7 +40,7 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
val showBalance by uiSettings.showBalance.collectAsState() val showBalance by uiSettings.showBalance.collectAsState()
val groupTransactions by uiSettings.groupTransactions.collectAsState() val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
val showColoredAmounts by DI.uiSettings.showColoredAmounts.collectAsState() val showColoredAmounts by DI.uiSettings.showColoredAmounts.collectAsState()
@ -57,8 +58,8 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
} }
} }
if (groupTransactions) { if (transactionsGrouping != TransactionsGrouping.None) {
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, userAccountsId) GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, userAccountsId, transactionsGrouping)
} else { } else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) { LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
itemsIndexed(transactionsToDisplay) { index, transaction -> itemsIndexed(transactionsToDisplay) { index, transaction ->

View File

@ -1,6 +1,7 @@
package net.codinux.banking.ui.config package net.codinux.banking.ui.config
import net.codinux.banking.client.model.tan.ActionRequiringTan import net.codinux.banking.client.model.tan.ActionRequiringTan
import net.codinux.banking.ui.model.TransactionsGrouping
object Internationalization { object Internationalization {
@ -21,4 +22,12 @@ object Internationalization {
ActionRequiringTan.ChangeTanMedium -> "um das TAN Medium zu ändern" ActionRequiringTan.ChangeTanMedium -> "um das TAN Medium zu ändern"
} }
fun translate(transactionsGrouping: TransactionsGrouping): String = when (transactionsGrouping) {
TransactionsGrouping.Quarter -> "Quartal"
TransactionsGrouping.Month -> "Monat"
TransactionsGrouping.Day -> "Tag"
TransactionsGrouping.Week -> "Woche"
TransactionsGrouping.None -> "Nicht gruppieren"
}
} }

View File

@ -4,6 +4,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import net.codinux.banking.ui.config.Colors
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -14,6 +17,7 @@ fun <T> Select(
onSelectedItemChanged: (T) -> Unit, onSelectedItemChanged: (T) -> Unit,
getItemDisplayText: (T) -> String, getItemDisplayText: (T) -> String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
textColor: Color? = null,
leadingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null,
dropDownItemContent: @Composable ((T) -> Unit)? = null dropDownItemContent: @Composable ((T) -> Unit)? = null
) { ) {
@ -24,11 +28,14 @@ fun <T> Select(
value = getItemDisplayText(selectedItem), value = getItemDisplayText(selectedItem),
onValueChange = { }, onValueChange = { },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
label = { Text(label) }, textStyle = if (textColor != null) TextStyle(textColor) else LocalTextStyle.current,
label = { Text(label, color = textColor ?: Color.Unspecified) },
readOnly = true, readOnly = true,
maxLines = 1, maxLines = 1,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(showDropDownMenu) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(showDropDownMenu) },
leadingIcon = leadingIcon leadingIcon = leadingIcon,
colors = if (textColor == null) TextFieldDefaults.outlinedTextFieldColors()
else TextFieldDefaults.outlinedTextFieldColors(textColor = textColor, unfocusedBorderColor = textColor, unfocusedLabelColor = textColor, placeholderColor = textColor, focusedBorderColor = Colors.CodinuxSecondaryColor)
) )
// due to a bug (still not fixed since 2021) in ExposedDropdownMenu its popup has a maximum width of 800 pixel / 320dp which is too less to fit // due to a bug (still not fixed since 2021) in ExposedDropdownMenu its popup has a maximum width of 800 pixel / 320dp which is too less to fit

View File

@ -0,0 +1,9 @@
package net.codinux.banking.ui.model
enum class TransactionsGrouping {
None,
Day,
Week,
Month,
Quarter
}

View File

@ -1,16 +1,40 @@
package net.codinux.banking.ui.service package net.codinux.banking.ui.service
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import kotlinx.datetime.LocalDate import kotlinx.datetime.*
import kotlinx.datetime.Month
import net.codinux.banking.client.model.Amount import net.codinux.banking.client.model.Amount
import net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.model.TransactionsGrouping
class FormatUtil { class FormatUtil {
fun formatDate(date: LocalDate): String = // TODO: find a better way fun formatDate(date: LocalDate): String = // TODO: find a better way
"${minDigits(date.dayOfMonth, 2)}.${minDigits(date.monthNumber, 2)}.${date.year.toString().substring(2)}" "${minDigits(date.dayOfMonth, 2)}.${minDigits(date.monthNumber, 2)}.${date.year.toString().substring(2)}"
fun formatGroupingDate(date: LocalDate, transactionsGrouping: TransactionsGrouping): String = when (transactionsGrouping) {
TransactionsGrouping.Day -> formatDate(date)
TransactionsGrouping.Week -> formatWeek(date)
TransactionsGrouping.Month -> formatMonth(date)
TransactionsGrouping.Quarter -> {
val quarter = when (date.monthNumber) {
1 -> "1"
4 -> "2"
7 -> "3"
else -> "4"
}
"${quarter}. Quartal ${date.year}"
}
TransactionsGrouping.None -> "" // illegal state
}
fun formatWeek(date: LocalDate): String {
// not fully correct, just for a proof of concept
val calenderWeek = date.dayOfYear / 7 + 1
val endOfWeek = date.plus(6, DateTimeUnit.DAY)
return "KW ${minDigits(calenderWeek, 2)}, ${minDigits(date.dayOfMonth, 2)}.${minDigits(date.monthNumber, 2)} - ${formatDate(endOfWeek)}"
}
fun formatMonth(date: LocalDate): String = // TODO: find a better way fun formatMonth(date: LocalDate): String = // TODO: find a better way
"${getMonthName(date.month)} ${date.year}" "${getMonthName(date.month)} ${date.year}"

View File

@ -0,0 +1,36 @@
package net.codinux.banking.ui.service
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Month
import net.codinux.banking.fints.extensions.minusDays
import net.codinux.banking.ui.model.AccountTransactionViewModel
import net.codinux.banking.ui.model.TransactionsGrouping
class TransactionsGroupingService {
fun getQuarter(date: LocalDate): Int = when (date.month) {
Month.JANUARY, Month.FEBRUARY, Month.MARCH -> 1
Month.APRIL, Month.MAY, Month.JUNE -> 4
Month.JULY, Month.AUGUST, Month.SEPTEMBER -> 7
else -> 10
}
fun getStartOfWeek(date: LocalDate): LocalDate = when (date.dayOfWeek) {
DayOfWeek.TUESDAY -> date.minusDays(1)
DayOfWeek.WEDNESDAY -> date.minusDays(2)
DayOfWeek.THURSDAY -> date.minusDays(3)
DayOfWeek.FRIDAY -> date.minusDays(4)
DayOfWeek.SATURDAY -> date.minusDays(5)
DayOfWeek.SUNDAY -> date.minusDays(6)
else -> date
}
fun getKeyForGroup(transaction: AccountTransactionViewModel, grouping: TransactionsGrouping) = when (grouping) {
TransactionsGrouping.Quarter -> LocalDate(transaction.valueDate.year, getQuarter(transaction.valueDate), 1)
TransactionsGrouping.Month -> LocalDate(transaction.valueDate.year, transaction.valueDate.monthNumber, 1)
TransactionsGrouping.Week -> getStartOfWeek(transaction.valueDate)
else -> transaction.valueDate
}
}

View File

@ -2,12 +2,13 @@ package net.codinux.banking.ui.settings
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import net.codinux.banking.ui.model.TransactionsGrouping
class UiSettings : ViewModel() { class UiSettings : ViewModel() {
val showBalance = MutableStateFlow(true) val showBalance = MutableStateFlow(true)
val groupTransactions = MutableStateFlow(true) val transactionsGrouping = MutableStateFlow(TransactionsGrouping.Month)
val zebraStripes = MutableStateFlow(true) val zebraStripes = MutableStateFlow(true)