Coverage Summary for Class: CsvBackupScreenKt (com.georgeci.moneysurfer.feature.settings.csv)

Class Method, % Branch, % Line, % Instruction, %
CsvBackupScreenKt 0% (0/16) 0% (0/76) 0% (0/117) 0% (0/1601)
CsvBackupScreenKt$CsvBackupScreen$1$1 0% (0/1) 0% (0/2) 0% (0/3) 0% (0/33)
Total 0% (0/17) 0% (0/78) 0% (0/120) 0% (0/1634)


 package com.georgeci.moneysurfer.feature.settings.csv
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.SnackbarDuration
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.georgeci.moneysurfer.domain.csv.CsvImportReport
 import com.georgeci.moneysurfer.domain.csv.CsvRowError
 import com.georgeci.moneysurfer.domain.csv.CsvRowIssue
 import com.georgeci.moneysurfer.feature.settings.backup.BackupPickerFormat
 import com.georgeci.moneysurfer.feature.settings.backup.rememberBackupPickerLauncher
 import com.georgeci.moneysurfer.feature.settings.components.SettingsSubScreenScaffold
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsChevron
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsGroup
 import com.georgeci.moneysurfer.uikit.components.settings.SurferSettingsRow
 import com.georgeci.moneysurfer.uikit.components.settings.SurferStatusHeroCard
 import com.georgeci.moneysurfer.uikit.components.settings.SurferStatusHeroTone
 import com.georgeci.moneysurfer.uikit.icons.SurferIcons
 import com.georgeci.moneysurfer.utils.HandleSideEffect
 import moneysurfer.feature.settings.generated.resources.Res
 import moneysurfer.feature.settings.generated.resources.settings_csv_export_success
 import moneysurfer.feature.settings.generated.resources.settings_csv_export_supporting
 import moneysurfer.feature.settings.generated.resources.settings_csv_hero_supporting
 import moneysurfer.feature.settings.generated.resources.settings_csv_hero_title
 import moneysurfer.feature.settings.generated.resources.settings_csv_import_result
 import moneysurfer.feature.settings.generated.resources.settings_csv_import_supporting
 import moneysurfer.feature.settings.generated.resources.settings_csv_import_title
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_account_workspace_mismatch
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_category_workspace_mismatch
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_columns
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_currency_mismatch
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_invalid_value
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_unknown_account
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_unknown_category
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_unknown_workspace
 import moneysurfer.feature.settings.generated.resources.settings_csv_issue_unpaired_transfer
 import moneysurfer.feature.settings.generated.resources.settings_csv_last_import_summary
 import moneysurfer.feature.settings.generated.resources.settings_csv_notice_file_too_large
 import moneysurfer.feature.settings.generated.resources.settings_csv_notice_generic
 import moneysurfer.feature.settings.generated.resources.settings_csv_notice_invalid_file
 import moneysurfer.feature.settings.generated.resources.settings_csv_row_error_format
 import moneysurfer.feature.settings.generated.resources.settings_csv_section_export
 import moneysurfer.feature.settings.generated.resources.settings_csv_section_import
 import moneysurfer.feature.settings.generated.resources.settings_csv_section_last_import
 import moneysurfer.feature.settings.generated.resources.settings_csv_title
 import moneysurfer.feature.settings.generated.resources.settings_export_csv
 import org.jetbrains.compose.resources.pluralStringResource
 import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.viewmodel.koinViewModel
 
 @Composable
 fun CsvBackupScreen(
     onNavigateBack: () -> Unit,
     viewModel: CsvBackupViewModel = koinViewModel(),
 ) {
     val state by viewModel.collectAsStateWithLifecycle()
     val snackbarHostState = remember { SnackbarHostState() }
     var pendingNotice by remember { mutableStateOf<CsvBackupNotice?>(null) }
 
     val launcher = rememberBackupPickerLauncher(
         format = BackupPickerFormat.Csv,
         onSavePicked = { sink -> viewModel.onEvent(CsvBackupEvent.OnSaveSinkChosen(sink)) },
         onOpenPicked = { source -> viewModel.onEvent(CsvBackupEvent.OnOpenSourceChosen(source)) },
     )
 
     // Resolve through Compose's standard format-args path so positional
     // placeholders survive translator reordering. We snapshot at composition;
     // the LaunchedEffect below feeds the snackbar from the resolved value.
     val pendingNoticeText: String? = pendingNotice?.let { noticeText(it) }
     LaunchedEffect(pendingNoticeText) {
         val text = pendingNoticeText ?: return@LaunchedEffect
         snackbarHostState.showSnackbar(message = text, duration = SnackbarDuration.Short)
         pendingNotice = null
     }
 
     viewModel.HandleSideEffect { effect ->
         when (effect) {
             CsvBackupEffect.NavigateBack -> onNavigateBack()
             is CsvBackupEffect.RequestSaveFile -> launcher.launchSave(effect.suggestedName)
             CsvBackupEffect.RequestOpenFile -> launcher.launchOpen()
             // A notice is how this screen learns an operation ended. Only the
             // export leaves the picker holding a staged file, and the progress
             // scrim keeps the two flows from overlapping, so the notice that
             // lands here is the one belonging to that export.
             is CsvBackupEffect.Notify -> {
                 pendingNotice = effect.notice
                 launcher.onSaveCompleted(effect.notice is CsvBackupNotice.ExportSuccess)
             }
         }
     }
 
     CsvBackupContent(
         state = state,
         snackbarHostState = snackbarHostState,
         onEvent = viewModel::onEvent,
     )
 }
 
 @Composable
 private fun noticeText(notice: CsvBackupNotice): String = when (notice) {
     is CsvBackupNotice.ExportSuccess ->
         pluralStringResource(Res.plurals.settings_csv_export_success, notice.count, notice.count)
     is CsvBackupNotice.ImportFinished ->
         stringResource(
             Res.string.settings_csv_import_result,
             notice.imported,
             notice.skipped,
             notice.failed,
         )
     CsvBackupNotice.InvalidFile -> stringResource(Res.string.settings_csv_notice_invalid_file)
     CsvBackupNotice.FileTooLarge -> stringResource(Res.string.settings_csv_notice_file_too_large)
     CsvBackupNotice.Generic -> stringResource(Res.string.settings_csv_notice_generic)
 }
 
 @Composable
 private fun CsvBackupContent(
     state: CsvBackupState,
     snackbarHostState: SnackbarHostState,
     onEvent: (CsvBackupEvent) -> Unit,
 ) {
     SettingsSubScreenScaffold(
         title = stringResource(Res.string.settings_csv_title),
         snackbarHostState = snackbarHostState,
         onBack = { onEvent(CsvBackupEvent.OnBackClick) },
         showProgressOverlay = state.phase != CsvBackupPhase.Idle,
     ) { padding ->
         Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
             SurferStatusHeroCard(
                 title = stringResource(Res.string.settings_csv_hero_title),
                 supporting = stringResource(Res.string.settings_csv_hero_supporting),
                 icon = SurferIcons.Download,
                 tone = SurferStatusHeroTone.Primary,
             )
         }
 
         SurferSettingsGroup(title = stringResource(Res.string.settings_csv_section_export)) {
             SurferSettingsRow(
                 icon = SurferIcons.Download,
                 title = stringResource(Res.string.settings_export_csv),
                 supportingText = stringResource(Res.string.settings_csv_export_supporting),
                 onClick = { onEvent(CsvBackupEvent.OnExportClick) },
                 trailing = { SurferSettingsChevron() },
             )
         }
 
         SurferSettingsGroup(title = stringResource(Res.string.settings_csv_section_import)) {
             SurferSettingsRow(
                 icon = SurferIcons.Sync,
                 title = stringResource(Res.string.settings_csv_import_title),
                 supportingText = stringResource(Res.string.settings_csv_import_supporting),
                 onClick = { onEvent(CsvBackupEvent.OnImportClick) },
                 trailing = { SurferSettingsChevron() },
             )
         }
 
         val report = state.lastImportReport
         if (report != null) {
             LastImportGroup(report)
         }
 
         Spacer(Modifier.height(padding.calculateBottomPadding() + 32.dp))
     }
 }
 
 @Composable
 private fun LastImportGroup(report: CsvImportReport) {
     SurferSettingsGroup(title = stringResource(Res.string.settings_csv_section_last_import)) {
         SurferSettingsRow(
             icon = SurferIcons.Info,
             title = stringResource(
                 Res.string.settings_csv_last_import_summary,
                 report.imported,
                 report.skippedDuplicates,
                 report.errors.size,
             ),
             multiline = true,
         )
         for (error in report.errors) {
             SurferSettingsRow(
                 icon = SurferIcons.Info,
                 title = rowErrorText(error),
                 danger = true,
                 multiline = true,
             )
         }
     }
 }
 
 @Composable
 private fun rowErrorText(error: CsvRowError): String =
     stringResource(Res.string.settings_csv_row_error_format, error.rowNumber, issueText(error.issue))
 
 @Composable
 private fun issueText(issue: CsvRowIssue): String = when (issue) {
     is CsvRowIssue.ColumnCountMismatch ->
         stringResource(Res.string.settings_csv_issue_columns, issue.expected, issue.actual)
     is CsvRowIssue.InvalidValue ->
         stringResource(Res.string.settings_csv_issue_invalid_value, issue.column)
     is CsvRowIssue.UnknownWorkspace ->
         stringResource(Res.string.settings_csv_issue_unknown_workspace, issue.workspaceId)
     is CsvRowIssue.UnknownAccount ->
         stringResource(Res.string.settings_csv_issue_unknown_account, issue.accountId)
     is CsvRowIssue.UnknownCategory ->
         stringResource(Res.string.settings_csv_issue_unknown_category, issue.categoryId)
     is CsvRowIssue.AccountWorkspaceMismatch ->
         stringResource(
             Res.string.settings_csv_issue_account_workspace_mismatch,
             issue.accountId,
             issue.workspaceId,
         )
     is CsvRowIssue.CategoryWorkspaceMismatch ->
         stringResource(
             Res.string.settings_csv_issue_category_workspace_mismatch,
             issue.categoryId,
             issue.workspaceId,
         )
     is CsvRowIssue.CurrencyMismatch ->
         stringResource(
             Res.string.settings_csv_issue_currency_mismatch,
             issue.currency,
             issue.accountCurrency,
         )
     is CsvRowIssue.UnpairedTransfer ->
         stringResource(Res.string.settings_csv_issue_unpaired_transfer, issue.transferId)
 }