Coverage Summary for Class: TransactionDetailsScreenKt (com.georgeci.moneysurfer.feature.transaction.details)

Class Method, % Branch, % Line, % Instruction, %
TransactionDetailsScreenKt 50% (4/8) 25% (17/68) 73.7% (70/95) 53.3% (715/1341)
TransactionDetailsScreenKt$TransactionDetailsScreen$1$1 0% (0/1) 0% (0/1)
TransactionDetailsScreenKt$TransactionDetailsScreen$2$1 0% (0/1) 0% (0/1)
Total 40% (4/10) 25% (17/68) 72.2% (70/97) 53.3% (715/1341)


 package com.georgeci.moneysurfer.feature.transaction.details
 
 import androidx.compose.foundation.layout.Box
 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.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.FilledTonalButton
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Text
 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.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.domain.primitives.TransactionId
 import com.georgeci.moneysurfer.uikit.components.base.SurferPaneScaffold
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbarAction
 import com.georgeci.moneysurfer.uikit.components.transaction.SurferDeleteTransactionDialog
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.uikit.modifier.surferContentContainer
 import com.georgeci.moneysurfer.uikit.modifier.surferTestTagAsId
 import com.georgeci.moneysurfer.uikit.semantics.SurferSemantics
 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_details_delete_content_description
 import moneysurfer.feature.transaction.generated.resources.transaction_details_duplicate
 import moneysurfer.feature.transaction.generated.resources.transaction_details_edit_content_description
 import moneysurfer.feature.transaction.generated.resources.transaction_details_title
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 import org.koin.core.parameter.parametersOf
 
 /**
  * Stable selectors for the transaction details screen — see docs/testing/testing-strategy.md.
  *
  * The dialog's confirm button is not here: the dialog moved to uikit when the transaction lists
  * gained swipe-to-delete, and its tag went with it as
  * [SurferDeleteTransactionDialogTestTags][com.georgeci.moneysurfer.uikit.components.transaction.SurferDeleteTransactionDialogTestTags].
  * The id the flows tap is unchanged.
  */
 object TransactionDetailsTestTags {
     const val Root = "transactionDetails:root"
     const val Edit = "transactionDetails:edit"
     const val Delete = "transactionDetails:delete"
 }
 
 @Composable
 fun TransactionDetailsScreen(
     transactionId: TransactionId,
     onNavigateBack: () -> Unit,
     onNavigateToEdit: (TransactionId) -> Unit = {},
     onNavigateToDuplicate: (TransactionId) -> Unit = {},
     viewModel: TransactionDetailsViewModel = koinViewModel(
         key = transactionId.value,
     ) { parametersOf(transactionId) },
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             TransactionDetailsEffect.NavigateBack -> onNavigateBack()
             is TransactionDetailsEffect.NavigateToEdit -> onNavigateToEdit(effect.transactionId)
             is TransactionDetailsEffect.NavigateToDuplicate -> onNavigateToDuplicate(effect.transactionId)
         }
     }
 
     when (val current = state) {
         is TransactionDetailsState.Loading -> TransactionDetailsLoading(onEvent = viewModel::onEvent)
         is TransactionDetailsState.Content -> TransactionDetailsContent(
             state = current,
             onEvent = viewModel::onEvent,
         )
     }
 }
 
 @Composable
 private fun TransactionDetailsLoading(onEvent: (TransactionDetailsEvent) -> Unit) {
     SurferPaneScaffold(
         title = stringResource(Res.string.transaction_details_title),
         onBack = { onEvent(TransactionDetailsEvent.OnBackClick) },
     ) { padding ->
         Box(modifier = Modifier.fillMaxSize().padding(padding))
     }
 }
 
 /**
  * The details of one transaction, 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 TransactionDetailsContent(
     state: TransactionDetailsState.Content,
     onEvent: (TransactionDetailsEvent) -> Unit,
 ) {
     if (state.showDeleteConfirmation) {
         SurferDeleteTransactionDialog(
             titleOrNull = state.note.ifBlank { null },
             onConfirm = { onEvent(TransactionDetailsEvent.OnDeleteConfirmed) },
             onDismiss = { onEvent(TransactionDetailsEvent.OnDeleteDismissed) },
             isTransfer = state.isTransfer,
             isSplit = state.isSplit,
         )
     }
 
     SurferPaneScaffold(
         title = stringResource(Res.string.transaction_details_title),
         modifier = Modifier
             .testTag(TransactionDetailsTestTags.Root)
             .surferTestTagAsId(),
         onBack = { onEvent(TransactionDetailsEvent.OnBackClick) },
         actions = { DetailsActions(onEvent = onEvent) },
     ) { padding ->
         Column(
             modifier = Modifier
                 .fillMaxSize()
                 .surferContentContainer()
                 .padding(top = padding.calculateTopPadding()),
         ) {
             Column(
                 modifier = Modifier
                     .fillMaxWidth()
                     .verticalScroll(rememberScrollState())
                     .weight(1f),
             ) {
                 val heroVisual = heroVisualFor(state)
                 HeroCard(
                     header = heroHeaderFor(state),
                     headerColor = heroVisual.tint,
                     visual = heroVisual,
                     formattedAmount = state.formattedAmount,
                     note = state.note,
                     formattedDate = state.formattedDate,
                     isPlanned = state.isPlanned,
                 )
 
                 state.split?.let { SplitCard(split = it) }
 
                 DetailsCard(state = state)
 
                 Spacer(Modifier.height(padding.calculateBottomPadding()))
             }
 
             if (state.canDuplicate) {
                 DuplicateButton(onEvent = onEvent)
             }
         }
     }
 }
 
 @Composable
 private fun DetailsActions(onEvent: (TransactionDetailsEvent) -> Unit) {
     SurferToolbarAction(
         icon = SurferIcons.Edit,
         contentDescription = stringResource(Res.string.transaction_details_edit_content_description),
         onClick = { onEvent(TransactionDetailsEvent.OnEditClick) },
         modifier = Modifier.testTag(TransactionDetailsTestTags.Edit),
     )
     SurferToolbarAction(
         icon = SurferIcons.Delete,
         contentDescription = stringResource(Res.string.transaction_details_delete_content_description),
         onClick = { onEvent(TransactionDetailsEvent.OnDeleteClick) },
         modifier = Modifier.testTag(TransactionDetailsTestTags.Delete),
         tint = AppTheme.materialColors.error,
     )
 }
 
 @Composable
 private fun DuplicateButton(onEvent: (TransactionDetailsEvent) -> Unit) {
     Row(
         modifier = Modifier
             .fillMaxWidth()
             .padding(horizontal = 16.dp)
             .padding(bottom = AppTheme.spacing.large),
     ) {
         FilledTonalButton(
             onClick = { onEvent(TransactionDetailsEvent.OnDuplicateClick) },
             modifier = Modifier.fillMaxWidth(),
             shape = RoundedCornerShape(20.dp),
         ) {
             Icon(
                 imageVector = SurferIcons.Copy,
                 contentDescription = SurferSemantics.Decorative,
                 modifier = Modifier.size(18.dp),
             )
             Spacer(Modifier.width(AppTheme.spacing.small))
             Text(stringResource(Res.string.transaction_details_duplicate))
         }
     }
 }