Coverage Summary for Class: TransactionFiltersScreenKt (com.georgeci.moneysurfer.feature.transaction.filters)

Class Class, % Method, % Branch, % Line, % Instruction, %
TransactionFiltersScreenKt 100% (1/1) 71.4% (10/14) 27.3% (12/44) 87.4% (76/87) 67.4% (727/1079)


 package com.georgeci.moneysurfer.feature.transaction.filters
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.domain.primitives.AccountId
 import com.georgeci.moneysurfer.feature.transaction.filter.TransactionFilters
 import com.georgeci.moneysurfer.uikit.components.SurferButton
 import com.georgeci.moneysurfer.uikit.components.SurferButtonStyle
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbar
 import com.georgeci.moneysurfer.uikit.modifier.surferContentContainer
 import com.georgeci.moneysurfer.uikit.modifier.surferSafeInsets
 import com.georgeci.moneysurfer.uikit.modifier.surferTestTagAsId
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import com.georgeci.moneysurfer.utils.HandleSideEffect
 import moneysurfer.feature.transaction.generated.resources.Res
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_apply
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_cancel
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_reset
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_results
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_results_capped
 import moneysurfer.feature.transaction.generated.resources.transaction_filters_title
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 import org.koin.core.parameter.parametersOf
 
 /** Apply carries a counted label, so it needs more room than the bare "Cancel" beside it. */
 private const val APPLY_WEIGHT = 1.4f
 
 object TransactionFiltersTestTags {
     const val Root = "transactionFilters:root"
     const val Apply = "transactionFilters:apply"
     const val Cancel = "transactionFilters:cancel"
 
     /**
      * Transaction-type option, suffixed with the lowercased
      * [com.georgeci.moneysurfer.domain.model.TransactionTypeFilter]. The Apply label carries a
      * live result count, so it can never be a stable text selector.
      */
     const val TypePrefix = "transactionFilters:type:"
 }
 
 @Composable
 fun TransactionFiltersScreen(
     accountId: AccountId?,
     anchorEpochDay: Long?,
     onNavigateBack: () -> Unit,
     // Key on the anchor as well: reopening the screen on a different paged period must build a
     // fresh ViewModel so its result count reflects that period, not the one it first opened on.
     viewModel: TransactionFiltersViewModel = koinViewModel(
         key = "${accountId?.value.orEmpty()}:$anchorEpochDay",
     ) { parametersOf(accountId, anchorEpochDay) },
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             TransactionFiltersEffect.NavigateBack -> onNavigateBack()
         }
     }
 
     TransactionFiltersContent(state = state, onEvent = viewModel::onEvent)
 }
 
 /**
  * The filter draft, drawn from a state it is handed.
  *
  * Public for the desktop UI tests in `:composeApp` `jvmTest`, which mount it with a state of their
  * own — see docs/testing/testing-strategy.md.
  */
 @Composable
 fun TransactionFiltersContent(
     state: TransactionFiltersState,
     onEvent: (TransactionFiltersEvent) -> Unit,
 ) {
     Scaffold(
         modifier = Modifier
             .surferSafeInsets()
             .testTag(TransactionFiltersTestTags.Root)
             .surferTestTagAsId(),
         containerColor = AppTheme.materialColors.surface,
         topBar = {
             SurferToolbar(
                 title = stringResource(Res.string.transaction_filters_title),
                 onBack = { onEvent(TransactionFiltersEvent.OnCancelClick) },
                 actions = {
                     TextButton(
                         onClick = { onEvent(TransactionFiltersEvent.OnResetClick) },
                         enabled = state.canReset,
                     ) {
                         Text(stringResource(Res.string.transaction_filters_reset))
                     }
                 },
             )
         },
         bottomBar = { FiltersFooter(state = state, onEvent = onEvent) },
     ) { padding ->
         Column(
             modifier = Modifier
                 .fillMaxSize()
                 .surferContentContainer()
                 .padding(padding)
                 .verticalScroll(rememberScrollState())
                 .padding(horizontal = 16.dp),
             verticalArrangement = Arrangement.spacedBy(20.dp),
         ) {
             Spacer(Modifier.height(4.dp))
             DateRangeSection(dateRange = state.draft.dateRange, onEvent = onEvent)
             TypeSection(type = state.draft.type, onEvent = onEvent)
             if (state.showAccounts) {
                 AccountsSection(
                     accounts = state.accounts,
                     selected = state.draft.accountIds,
                     onEvent = onEvent,
                 )
             }
             CategoriesSection(
                 categories = state.categories,
                 selected = state.draft.categoryIds,
                 onEvent = onEvent,
             )
             AmountSection(
                 minAmount = state.draft.minAmount,
                 maxAmount = state.draft.maxAmount,
                 onEvent = onEvent,
             )
             OtherSection(filters = state.draft, onEvent = onEvent)
             SortSection(sort = state.draft.sort, onEvent = onEvent)
             Spacer(Modifier.height(8.dp))
         }
     }
 }
 
 /**
  * Sticky footer. `Apply` carries the live result count — a real count over the same windowed
  * query the list runs, so the number the user commits to is the number they land on.
  */
 @Composable
 private fun FiltersFooter(
     state: TransactionFiltersState,
     onEvent: (TransactionFiltersEvent) -> Unit,
 ) {
     val results = if (state.resultCountCapped) {
         stringResource(Res.string.transaction_filters_results_capped, state.resultCount)
     } else {
         stringResource(Res.string.transaction_filters_results, state.resultCount)
     }
     Surface(color = AppTheme.materialColors.surface) {
         // The surface itself bleeds to the window edges; its contents take the same cap as the
         // form above, so the divider terminates that column instead of overrunning it.
         Column(modifier = Modifier.surferContentContainer()) {
             HorizontalDivider(color = AppTheme.materialColors.outlineVariant)
             Row(
                 modifier = Modifier
                     .fillMaxWidth()
                     .padding(horizontal = 16.dp, vertical = 12.dp),
                 horizontalArrangement = Arrangement.spacedBy(12.dp),
             ) {
                 SurferButton(
                     text = stringResource(Res.string.transaction_filters_cancel),
                     onClick = { onEvent(TransactionFiltersEvent.OnCancelClick) },
                     style = SurferButtonStyle.Outlined,
                     modifier = Modifier
                         .weight(1f)
                         .testTag(TransactionFiltersTestTags.Cancel),
                 )
                 SurferButton(
                     text = stringResource(Res.string.transaction_filters_apply, results),
                     onClick = { onEvent(TransactionFiltersEvent.OnApplyClick) },
                     modifier = Modifier
                         .weight(APPLY_WEIGHT)
                         .testTag(TransactionFiltersTestTags.Apply),
                 )
             }
         }
     }
 }
 
 @Preview
 @Composable
 private fun TransactionFiltersPreview() {
     AppTheme {
         TransactionFiltersContent(
             state = TransactionFiltersState(
                 draft = TransactionFilters.Empty,
                 showAccounts = true,
                 resultCount = 47,
             ),
             onEvent = {},
         )
     }
 }