Coverage Summary for Class: GoalContributionScreenKt (com.georgeci.moneysurfer.feature.goal.contribution)

Class Method, % Branch, % Line, % Instruction, %
GoalContributionScreenKt 0% (0/15) 0% (0/38) 0% (0/73) 0.4% (4/1049)
GoalContributionScreenKt$WhenMappings
Total 0% (0/15) 0% (0/38) 0% (0/73) 0.4% (4/1049)


 package com.georgeci.moneysurfer.feature.goal.contribution
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material3.OutlinedTextField
 import androidx.compose.material3.Scaffold
 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.text.input.KeyboardType
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.domain.primitives.GoalId
 import com.georgeci.moneysurfer.feature.goal.GoalDateField
 import com.georgeci.moneysurfer.feature.goal.GoalDatePickerDialog
 import com.georgeci.moneysurfer.feature.goal.formatGoalDate
 import com.georgeci.moneysurfer.navigation.GoalContributionMode
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbar
 import com.georgeci.moneysurfer.uikit.components.base.SurferToolbarButtonAction
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 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.preview.SurferComponentPreview
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import com.georgeci.moneysurfer.utils.HandleSideEffect
 import kotlinx.datetime.LocalDate
 import moneysurfer.feature.goal.generated.resources.Res
 import moneysurfer.feature.goal.generated.resources.goal_contribution_amount_label
 import moneysurfer.feature.goal.generated.resources.goal_contribution_date_label
 import moneysurfer.feature.goal.generated.resources.goal_contribution_note_label
 import moneysurfer.feature.goal.generated.resources.goal_contribution_save
 import moneysurfer.feature.goal.generated.resources.goal_contribution_saved_label
 import moneysurfer.feature.goal.generated.resources.goal_contribution_title_add
 import moneysurfer.feature.goal.generated.resources.goal_contribution_title_withdraw
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 import org.koin.core.parameter.parametersOf
 
 /** Stable selectors for the goal add/withdraw screen — see docs/testing/testing-strategy.md. */
 object GoalContributionTestTags {
     const val Root = "goalContribution:root"
     const val AmountField = "goalContribution:amount"
     const val SaveButton = "goalContribution:save"
 }
 
 @Composable
 fun GoalContributionScreen(
     goalId: GoalId,
     mode: GoalContributionMode,
     onNavigateBack: () -> Unit,
     viewModel: GoalContributionViewModel = koinViewModel(
         key = "${goalId.value}-${mode.name}",
     ) { parametersOf(goalId, mode) },
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             GoalContributionEffect.NavigateBack -> onNavigateBack()
         }
     }
 
     GoalContributionContent(state = state, onEvent = viewModel::onEvent)
 }
 
 @Composable
 private fun GoalContributionContent(
     state: GoalContributionState,
     onEvent: (GoalContributionEvent) -> Unit,
 ) {
     Scaffold(
         modifier = Modifier
             .surferSafeInsets()
             .testTag(GoalContributionTestTags.Root)
             .surferTestTagAsId(),
         containerColor = AppTheme.materialColors.surface,
         topBar = {
             SurferToolbar(
                 title = stringResource(
                     when (state.mode) {
                         GoalContributionMode.ADD -> Res.string.goal_contribution_title_add
                         GoalContributionMode.WITHDRAW -> Res.string.goal_contribution_title_withdraw
                     },
                 ),
                 onBack = { onEvent(GoalContributionEvent.OnBackClick) },
                 actions = {
                     SurferToolbarButtonAction(
                         icon = SurferIcons.Check,
                         text = stringResource(Res.string.goal_contribution_save),
                         enabled = state.canSave,
                         onClick = { onEvent(GoalContributionEvent.OnSaveClick) },
                         modifier = Modifier.testTag(GoalContributionTestTags.SaveButton),
                     )
                 },
             )
         },
     ) { padding ->
         Column(
             modifier = Modifier
                 .fillMaxSize()
                 .surferContentContainer()
                 .padding(padding)
                 .padding(AppTheme.spacing.default),
             verticalArrangement = Arrangement.spacedBy(AppTheme.spacing.default),
         ) {
             Text(
                 text = stringResource(Res.string.goal_contribution_saved_label, state.savedLabel),
                 style = AppTheme.typography.bodyMedium,
                 color = AppTheme.materialColors.onSurfaceVariant,
             )
 
             OutlinedTextField(
                 value = state.amount,
                 onValueChange = { onEvent(GoalContributionEvent.OnAmountChanged(it)) },
                 label = { Text(stringResource(Res.string.goal_contribution_amount_label)) },
                 isError = state.error != null,
                 supportingText = state.error?.let { error -> { Text(stringResource(error)) } },
                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
                 singleLine = true,
                 modifier = Modifier
                     .fillMaxWidth()
                     .testTag(GoalContributionTestTags.AmountField),
             )
 
             GoalDateField(
                 label = stringResource(Res.string.goal_contribution_date_label),
                 value = state.occurredOn?.let(::formatGoalDate).orEmpty(),
                 onClick = { onEvent(GoalContributionEvent.OnDatePickerOpen) },
             )
 
             OutlinedTextField(
                 value = state.note,
                 onValueChange = { onEvent(GoalContributionEvent.OnNoteChanged(it)) },
                 label = { Text(stringResource(Res.string.goal_contribution_note_label)) },
                 singleLine = true,
                 modifier = Modifier.fillMaxWidth(),
             )
         }
     }
 
     if (state.showDatePicker) {
         GoalDatePickerDialog(
             initialDate = state.occurredOn,
             onDismiss = { onEvent(GoalContributionEvent.OnDatePickerDismiss) },
             onConfirm = {
                 onEvent(GoalContributionEvent.OnDateChanged(it))
                 onEvent(GoalContributionEvent.OnDatePickerDismiss)
             },
         )
     }
 }
 
 @Preview
 @Composable
 private fun GoalContributionScreenPreview() {
     SurferComponentPreview {
         GoalContributionContent(
             state = GoalContributionState(
                 mode = GoalContributionMode.ADD,
                 goalTitle = "Emergency fund",
                 savedLabel = "€8,915",
                 amount = "250",
                 occurredOn = LocalDate(2026, 7, 12),
             ),
             onEvent = {},
         )
     }
 }