Coverage Summary for Class: TransactionsSearchAndChipsKt (com.georgeci.moneysurfer.feature.transaction.list)

Class Method, % Branch, % Line, % Instruction, %
TransactionsSearchAndChipsKt 92.9% (13/14) 60% (36/60) 90.1% (91/101) 90.8% (1033/1138)
TransactionsSearchAndChipsKt$WhenMappings
Total 92.9% (13/14) 60% (36/60) 90.1% (91/101) 90.8% (1033/1138)


 package com.georgeci.moneysurfer.feature.transaction.list
 
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.material3.Badge
 import androidx.compose.material3.BadgedBox
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.dp
 import com.georgeci.moneysurfer.feature.transaction.filter.TransactionDatePreset
 import com.georgeci.moneysurfer.feature.transaction.filter.TransactionDateRange
 import com.georgeci.moneysurfer.feature.transaction.filter.TransactionSort
 import com.georgeci.moneysurfer.feature.transaction.filter.TransactionTypeFilter
 import com.georgeci.moneysurfer.uikit.components.SurferSearchField
 import com.georgeci.moneysurfer.uikit.components.base.SurferSelectableChip
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import moneysurfer.feature.transaction.generated.resources.Res
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_date_last_month
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_date_this_month
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_date_this_week
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_date_this_year
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_date_today
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_date_yesterday
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_account
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_category
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_date
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_date_custom
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_selected_count
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_sort
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_type
 import moneysurfer.feature.transaction.generated.resources.transactions_list_chip_value
 import moneysurfer.feature.transaction.generated.resources.transactions_list_filter_expenses
 import moneysurfer.feature.transaction.generated.resources.transactions_list_filter_income
 import moneysurfer.feature.transaction.generated.resources.transactions_list_filter_transfer
 import moneysurfer.feature.transaction.generated.resources.transactions_list_filters_badge
 import moneysurfer.feature.transaction.generated.resources.transactions_list_filters_content_description
 import moneysurfer.feature.transaction.generated.resources.transactions_list_search_placeholder
 import moneysurfer.feature.transaction.generated.resources.transactions_list_sort_newest
 import moneysurfer.feature.transaction.generated.resources.transactions_list_sort_oldest
 import org.jetbrains.compose.resources.stringResource
 
 /**
  * The design's search field with the filter button inside it. The badge counts applied filters
  * only — the search text is right there in the field, so counting it too would say the same thing
  * twice.
  */
 @Composable
 internal fun SearchRow(
     query: String,
     activeFilterCount: Int,
     onEvent: (TransactionsByAccountEvent) -> Unit,
 ) {
     val filtersLabel = stringResource(Res.string.transactions_list_filters_content_description)
     SurferSearchField(
         value = query,
         onValueChange = { onEvent(TransactionsByAccountEvent.OnSearchQueryChanged(it)) },
         placeholder = stringResource(Res.string.transactions_list_search_placeholder),
         containerColor = AppTheme.materialColors.surfaceContainerLow,
         trailing = {
             BadgedBox(
                 badge = {
                     if (activeFilterCount > 0) {
                         Badge {
                             Text(
                                 stringResource(
                                     Res.string.transactions_list_filters_badge,
                                     activeFilterCount,
                                 ),
                             )
                         }
                     }
                 },
             ) {
                 IconButton(onClick = { onEvent(TransactionsByAccountEvent.OnOpenFiltersClick) }) {
                     Icon(imageVector = SurferIcons.Filter, contentDescription = filtersLabel)
                 }
             }
         },
     )
 }
 
 /**
  * `Type` / `Account` / `Category` / `Sort`, plus a date chip once an explicit range replaces the
  * pager — without it the range would be applied with nothing on screen saying so.
  *
  * Every chip opens the filter screen rather than cycling its own value: the chips are shortcuts
  * into it, so there is one place where a filter is changed and one place it can be read from.
  */
 @Composable
 internal fun FilterChipRail(
     chips: TransactionFilterChipsUi,
     onOpenFilters: () -> Unit,
 ) {
     Row(
         modifier = Modifier
             .fillMaxWidth()
             .horizontalScroll(rememberScrollState())
             .padding(horizontal = 16.dp, vertical = 4.dp),
         horizontalArrangement = Arrangement.spacedBy(8.dp),
     ) {
         chips.dateRangeValue()?.let { value ->
             RailChip(
                 label = stringResource(Res.string.transactions_list_chip_date),
                 value = value,
                 onClick = onOpenFilters,
                 modifier = Modifier.testTag(TransactionsListTestTags.FilterDate),
             )
         }
         RailChip(
             label = stringResource(Res.string.transactions_list_chip_type),
             value = chips.type.chipValue(),
             onClick = onOpenFilters,
             modifier = Modifier.testTag(TransactionsListTestTags.FilterType),
         )
         RailChip(
             label = stringResource(Res.string.transactions_list_chip_account),
             value = selectionValue(chips.accountName, chips.accountCount),
             onClick = onOpenFilters,
             modifier = Modifier.testTag(TransactionsListTestTags.FilterAccount),
         )
         RailChip(
             label = stringResource(Res.string.transactions_list_chip_category),
             value = selectionValue(chips.categoryName, chips.categoryCount),
             onClick = onOpenFilters,
             modifier = Modifier.testTag(TransactionsListTestTags.FilterCategory),
         )
         RailChip(
             label = stringResource(Res.string.transactions_list_chip_sort),
             value = chips.sort.chipValue(),
             // The sort chip always spells out its value, so "is it filled" has to come from
             // whether it differs from the default rather than from having a value at all.
             highlighted = chips.sort != TransactionSort.Newest,
             onClick = onOpenFilters,
             modifier = Modifier.testTag(TransactionsListTestTags.FilterSort),
         )
     }
 }
 
 /**
  * A chip reads `Type` when unset and `Type · Expenses` once it carries a value. No leading check —
  * the fill alone marks it set, and [highlighted] lets the sort chip (which always shows a value)
  * still read as unset while it holds the default.
  */
 @Composable
 private fun RailChip(
     label: String,
     value: String?,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     highlighted: Boolean = value != null,
 ) {
     val text = if (value != null) {
         stringResource(Res.string.transactions_list_chip_value, label, value)
     } else {
         label
     }
     SurferSelectableChip(
         label = text,
         selected = highlighted,
         onClick = onClick,
         modifier = modifier,
         showCheck = false,
     )
 }
 
 /** One pick shows its name, several show how many — null leaves the chip in its bare state. */
 @Composable
 private fun selectionValue(name: String?, count: Int): String? = when {
     count == 0 -> null
     name != null -> name
     else -> stringResource(Res.string.transactions_list_chip_selected_count, count)
 }
 
 @Composable
 private fun TransactionFilterChipsUi.dateRangeValue(): String? = when (val range = dateRange) {
     TransactionDateRange.FollowPeriod -> null
     is TransactionDateRange.Custom -> stringResource(Res.string.transactions_list_chip_date_custom)
     is TransactionDateRange.Preset -> range.preset.chipValue()
 }
 
 @Composable
 private fun TransactionDatePreset.chipValue(): String = stringResource(
     when (this) {
         TransactionDatePreset.Today -> Res.string.transaction_filters_date_today
         TransactionDatePreset.Yesterday -> Res.string.transaction_filters_date_yesterday
         TransactionDatePreset.ThisWeek -> Res.string.transaction_filters_date_this_week
         TransactionDatePreset.ThisMonth -> Res.string.transaction_filters_date_this_month
         TransactionDatePreset.LastMonth -> Res.string.transaction_filters_date_last_month
         TransactionDatePreset.ThisYear -> Res.string.transaction_filters_date_this_year
     },
 )
 
 @Composable
 private fun TransactionTypeFilter.chipValue(): String? = when (this) {
     TransactionTypeFilter.All -> null
     TransactionTypeFilter.Expenses -> stringResource(Res.string.transactions_list_filter_expenses)
     TransactionTypeFilter.Income -> stringResource(Res.string.transactions_list_filter_income)
     TransactionTypeFilter.Transfer -> stringResource(Res.string.transactions_list_filter_transfer)
 }
 
 @Composable
 private fun TransactionSort.chipValue(): String = stringResource(
     when (this) {
         TransactionSort.Newest -> Res.string.transactions_list_sort_newest
         TransactionSort.Oldest -> Res.string.transactions_list_sort_oldest
     },
 )