Coverage Summary for Class: TransactionFilterSectionsKt (com.georgeci.moneysurfer.feature.transaction.filters)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| TransactionFilterSectionsKt |
100%
(1/1)
|
100%
(16/16)
|
50%
(38/76)
|
100%
(157/157)
|
98.7%
(2337/2367)
|
package com.georgeci.moneysurfer.feature.transaction.filters
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Checkbox
import androidx.compose.material3.FilterChip
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.domain.model.Account
import com.georgeci.moneysurfer.domain.model.Category
import com.georgeci.moneysurfer.domain.primitives.AccountId
import com.georgeci.moneysurfer.domain.primitives.CategoryId
import com.georgeci.moneysurfer.feature.transaction.filter.TransactionFilters
import com.georgeci.moneysurfer.feature.transaction.filter.TransactionSort
import com.georgeci.moneysurfer.feature.transaction.filter.TransactionTypeFilter
import com.georgeci.moneysurfer.uikit.components.base.SurferSegmentedControl
import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsSwitch
import com.georgeci.moneysurfer.uikit.theme.AppTheme
import moneysurfer.feature.transaction.generated.resources.Res
import moneysurfer.feature.transaction.generated.resources.transaction_filters_amount_max
import moneysurfer.feature.transaction.generated.resources.transaction_filters_amount_min
import moneysurfer.feature.transaction.generated.resources.transaction_filters_categories_all
import moneysurfer.feature.transaction.generated.resources.transaction_filters_categories_counter
import moneysurfer.feature.transaction.generated.resources.transaction_filters_planned_only
import moneysurfer.feature.transaction.generated.resources.transaction_filters_planned_only_hint
import moneysurfer.feature.transaction.generated.resources.transaction_filters_recurring_only
import moneysurfer.feature.transaction.generated.resources.transaction_filters_recurring_only_hint
import moneysurfer.feature.transaction.generated.resources.transaction_filters_section_accounts
import moneysurfer.feature.transaction.generated.resources.transaction_filters_section_amount
import moneysurfer.feature.transaction.generated.resources.transaction_filters_section_categories
import moneysurfer.feature.transaction.generated.resources.transaction_filters_section_other
import moneysurfer.feature.transaction.generated.resources.transaction_filters_section_sort
import moneysurfer.feature.transaction.generated.resources.transaction_filters_section_type
import moneysurfer.feature.transaction.generated.resources.transaction_filters_sort_newest
import moneysurfer.feature.transaction.generated.resources.transaction_filters_sort_oldest
import moneysurfer.feature.transaction.generated.resources.transaction_filters_type_all
import moneysurfer.feature.transaction.generated.resources.transaction_filters_type_expenses
import moneysurfer.feature.transaction.generated.resources.transaction_filters_type_income
import moneysurfer.feature.transaction.generated.resources.transaction_filters_type_transfer
import org.jetbrains.compose.resources.stringResource
@Composable
internal fun FilterSectionHeader(text: String) {
Text(
text = text.uppercase(),
style = AppTheme.typography.labelSmall,
color = AppTheme.materialColors.onSurfaceVariant,
)
}
@Composable
internal fun TypeSection(
type: TransactionTypeFilter,
onEvent: (TransactionFiltersEvent) -> Unit,
) {
val labels = mapOf(
TransactionTypeFilter.All to stringResource(Res.string.transaction_filters_type_all),
TransactionTypeFilter.Expenses to stringResource(Res.string.transaction_filters_type_expenses),
TransactionTypeFilter.Income to stringResource(Res.string.transaction_filters_type_income),
TransactionTypeFilter.Transfer to stringResource(Res.string.transaction_filters_type_transfer),
)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
FilterSectionHeader(stringResource(Res.string.transaction_filters_section_type))
SurferSegmentedControl(
options = TransactionTypeFilter.entries,
selected = type,
label = { labels.getValue(it) },
onSelect = { onEvent(TransactionFiltersEvent.OnTypeSelected(it)) },
optionTestTag = { TransactionFiltersTestTags.TypePrefix + it.name.lowercase() },
)
}
}
/**
* Multi-select over the accounts. An empty selection means "every account", the same convention
* the category chips use — there is no separate "all" tile to keep in sync with the checkboxes.
*/
@Composable
internal fun AccountsSection(
accounts: List<Account>,
selected: Set<AccountId>,
onEvent: (TransactionFiltersEvent) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
FilterSectionHeader(stringResource(Res.string.transaction_filters_section_accounts))
accounts.forEach { account ->
AccountTile(
name = account.name,
checked = account.id in selected,
onToggle = { onEvent(TransactionFiltersEvent.OnAccountToggled(account.id)) },
)
}
}
}
@Composable
private fun AccountTile(
name: String,
checked: Boolean,
onToggle: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(AppTheme.shapes.large)
.background(AppTheme.materialColors.surfaceContainerLow)
.clickable(onClick = onToggle)
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = name,
style = AppTheme.typography.bodyLarge,
color = AppTheme.materialColors.onSurface,
modifier = Modifier.weight(1f),
)
Checkbox(checked = checked, onCheckedChange = { onToggle() })
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
internal fun CategoriesSection(
categories: List<Category>,
selected: Set<CategoryId>,
onEvent: (TransactionFiltersEvent) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
FilterSectionHeader(stringResource(Res.string.transaction_filters_section_categories))
Text(
text = stringResource(
Res.string.transaction_filters_categories_counter,
selected.size,
categories.size,
),
style = AppTheme.typography.labelSmall,
color = AppTheme.materialColors.onSurfaceVariant,
textAlign = TextAlign.End,
modifier = Modifier.weight(1f),
)
}
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterChip(
selected = selected.isEmpty(),
onClick = { onEvent(TransactionFiltersEvent.OnAllCategoriesClick) },
label = { Text(stringResource(Res.string.transaction_filters_categories_all)) },
)
categories.forEach { category ->
FilterChip(
selected = category.id in selected,
onClick = { onEvent(TransactionFiltersEvent.OnCategoryToggled(category.id)) },
label = { Text(category.name) },
)
}
}
}
}
@Composable
internal fun AmountSection(
minAmount: String,
maxAmount: String,
onEvent: (TransactionFiltersEvent) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
FilterSectionHeader(stringResource(Res.string.transaction_filters_section_amount))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
AmountField(
label = stringResource(Res.string.transaction_filters_amount_min),
value = minAmount,
onChange = { onEvent(TransactionFiltersEvent.OnMinAmountChanged(it)) },
modifier = Modifier.weight(1f),
)
AmountField(
label = stringResource(Res.string.transaction_filters_amount_max),
value = maxAmount,
onChange = { onEvent(TransactionFiltersEvent.OnMaxAmountChanged(it)) },
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun AmountField(
label: String,
value: String,
onChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
OutlinedTextField(
value = value,
onValueChange = onChange,
label = { Text(label) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
modifier = modifier,
)
}
@Composable
internal fun OtherSection(
filters: TransactionFilters,
onEvent: (TransactionFiltersEvent) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
FilterSectionHeader(stringResource(Res.string.transaction_filters_section_other))
ToggleRow(
title = stringResource(Res.string.transaction_filters_recurring_only),
hint = stringResource(Res.string.transaction_filters_recurring_only_hint),
checked = filters.recurringOnly,
onChange = { onEvent(TransactionFiltersEvent.OnRecurringOnlyChanged(it)) },
)
ToggleRow(
title = stringResource(Res.string.transaction_filters_planned_only),
hint = stringResource(Res.string.transaction_filters_planned_only_hint),
checked = filters.plannedOnly,
onChange = { onEvent(TransactionFiltersEvent.OnPlannedOnlyChanged(it)) },
)
}
}
@Composable
private fun ToggleRow(
title: String,
hint: String,
checked: Boolean,
onChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onChange(!checked) }
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = AppTheme.typography.bodyLarge,
color = AppTheme.materialColors.onSurface,
)
Text(
text = hint,
style = AppTheme.typography.bodySmall,
color = AppTheme.materialColors.onSurfaceVariant,
)
}
SurferSettingsSwitch(checked = checked, onCheckedChange = onChange)
}
}
@Composable
internal fun SortSection(
sort: TransactionSort,
onEvent: (TransactionFiltersEvent) -> Unit,
) {
val labels = mapOf(
TransactionSort.Newest to stringResource(Res.string.transaction_filters_sort_newest),
TransactionSort.Oldest to stringResource(Res.string.transaction_filters_sort_oldest),
)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
FilterSectionHeader(stringResource(Res.string.transaction_filters_section_sort))
SurferSegmentedControl(
options = TransactionSort.entries,
selected = sort,
label = { labels.getValue(it) },
onSelect = { onEvent(TransactionFiltersEvent.OnSortSelected(it)) },
)
}
}