From cc1b02f94b6e1df63caf18b504a9cbc1a1f0b92a Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sun, 16 Apr 2023 01:55:48 +0300 Subject: [PATCH] Implement Manual CSV import --- .../java/com/ivy/wallet/ui/csv/CSVEvent.kt | 1 + .../java/com/ivy/wallet/ui/csv/CSVScreen.kt | 37 +++++++++-- .../java/com/ivy/wallet/ui/csv/CSVState.kt | 9 +++ .../com/ivy/wallet/ui/csv/CSVViewModel.kt | 64 +++++++++++++++++-- .../ui/csvimport/flow/ImportResultUI.kt | 54 ++++++++++------ 5 files changed, 134 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/ui/csv/CSVEvent.kt b/app/src/main/java/com/ivy/wallet/ui/csv/CSVEvent.kt index f07e5c8ae3..e210373cc3 100644 --- a/app/src/main/java/com/ivy/wallet/ui/csv/CSVEvent.kt +++ b/app/src/main/java/com/ivy/wallet/ui/csv/CSVEvent.kt @@ -24,5 +24,6 @@ sealed interface CSVEvent { data class MapDescription(val index: Int, val name: String) : CSVEvent object Continue : CSVEvent + object ResetState : CSVEvent } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/csv/CSVScreen.kt b/app/src/main/java/com/ivy/wallet/ui/csv/CSVScreen.kt index f9294752a8..c523abff79 100644 --- a/app/src/main/java/com/ivy/wallet/ui/csv/CSVScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/csv/CSVScreen.kt @@ -24,17 +24,36 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.insets.systemBarsPadding import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.colorAs +import com.ivy.frp.view.navigation.navigation +import com.ivy.wallet.ui.csvimport.flow.ImportProcessing +import com.ivy.wallet.ui.csvimport.flow.ImportResultUI import com.ivy.wallet.ui.ivyWalletCtx import com.ivy.wallet.utils.thenIf +import kotlin.math.abs @Composable fun CSVScreen() { val viewModel: CSVViewModel = viewModel() - UI(state = viewModel.uiState(), onEvent = viewModel::onEvent) + val state = viewModel.uiState() + val nav = navigation() + when (val ui = state.uiState) { + UIState.Idle -> ImportUI(state = state, onEvent = viewModel::onEvent) + is UIState.Processing -> ImportProcessing(progressPercent = ui.percent) + is UIState.Result -> ImportResultUI( + result = ui.importResult, + isManualCsvImport = true, + onTryAgain = { + viewModel.onEvent(CSVEvent.ResetState) + }, + onFinish = { + nav.back() + } + ) + } } @Composable -private fun UI( +private fun ImportUI( state: CSVState, onEvent: (CSVEvent) -> Unit, ) { @@ -301,6 +320,7 @@ private fun AmountMetadata( multiplier: Int, onMetaChange: (Int) -> Unit, ) { + Text(text = "Multiplier", style = UI.typo.nB2) Row(verticalAlignment = Alignment.CenterVertically) { Button(onClick = { onMetaChange( @@ -316,8 +336,8 @@ private fun AmountMetadata( Spacer8(horizontal = true) Text( text = when { - multiplier < 0 -> "/$multiplier" - multiplier > 1 -> "*$multiplier" + multiplier < 0 -> "/${abs(multiplier)}" + multiplier > 1 -> "*${abs(multiplier)}" else -> "None" }, style = UI.typo.nB2, @@ -458,9 +478,12 @@ fun LazyListScope.transferFields( status = transferFields.toAmountStatus, onMapTo = { index, name -> onEvent(CSVEvent.MapToAmount(index, name)) }, metadataContent = { multiplier -> - AmountMetadata(multiplier = multiplier, onMetaChange = { - onEvent(CSVEvent.ToAmountMetaChange(it)) - }) + AmountMetadata( + multiplier = multiplier, + onMetaChange = { + onEvent(CSVEvent.ToAmountMetaChange(it)) + } + ) } ) } diff --git a/app/src/main/java/com/ivy/wallet/ui/csv/CSVState.kt b/app/src/main/java/com/ivy/wallet/ui/csv/CSVState.kt index ed4a886210..76a67a0178 100644 --- a/app/src/main/java/com/ivy/wallet/ui/csv/CSVState.kt +++ b/app/src/main/java/com/ivy/wallet/ui/csv/CSVState.kt @@ -1,6 +1,9 @@ package com.ivy.wallet.ui.csv +import com.ivy.wallet.domain.deprecated.logic.csv.model.ImportResult + data class CSVState( + val uiState: UIState, val columns: CSVRow?, val csv: List?, val important: ImportantFields?, @@ -9,6 +12,12 @@ data class CSVState( val continueEnabled: Boolean, ) +sealed interface UIState { + object Idle : UIState + data class Processing(val percent: Int) : UIState + data class Result(val importResult: ImportResult) : UIState +} + data class ImportantFields( val amount: ColumnMapping, val amountStatus: MappingStatus, diff --git a/app/src/main/java/com/ivy/wallet/ui/csv/CSVViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/csv/CSVViewModel.kt index cdbd1b4f90..36251d0560 100644 --- a/app/src/main/java/com/ivy/wallet/ui/csv/CSVViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/csv/CSVViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ivy.wallet.domain.deprecated.logic.csv.IvyFileReader import com.ivy.wallet.ui.csv.domain.* +import com.ivy.wallet.utils.uiThread import com.opencsv.CSVReaderBuilder import com.opencsv.validators.LineValidator import com.opencsv.validators.RowValidator @@ -17,11 +18,12 @@ import kotlinx.coroutines.withContext import java.io.StringReader import java.nio.charset.Charset import javax.inject.Inject +import kotlin.math.roundToInt @HiltViewModel class CSVViewModel @Inject constructor( private val fileReader: IvyFileReader, - private val csvImporterV2: CSVImporterV2, + private val csvImporter: CSVImporterV2, ) : ViewModel() { private var columns by mutableStateOf(null) @@ -179,16 +181,19 @@ class CSVViewModel @Inject constructor( ) // endregion + private var uiState by mutableStateOf(UIState.Idle) + @Composable fun uiState(): CSVState { val sampleCSV = remember(csv) { // drop the header - csv?.drop(1)?.shuffled()?.take(SAMPLE_SIZE) + csv?.drop(1)?.take(SAMPLE_SIZE) } val important = importantFields(sampleCSV) return CSVState( + uiState = uiState, columns = columns, csv = csv, important = important, @@ -239,7 +244,7 @@ class CSVViewModel @Inject constructor( private fun transferFields(sampleCSV: List?): TransferFields? { return produceState( initialValue = null, - sampleCSV, toAccount, toAccountCurrency, + sampleCSV, toAccount, toAccountCurrency, toAmount, ) { val result = withContext(Dispatchers.Default) { if (sampleCSV != null) { @@ -373,6 +378,9 @@ class CSVViewModel @Inject constructor( metadata = event.multiplier ) } + CSVEvent.ResetState -> { + uiState = UIState.Idle + } } } @@ -382,7 +390,7 @@ class CSVViewModel @Inject constructor( columns = csv?.firstOrNull() } - private suspend fun processFile( + private fun processFile( uri: Uri, charset: Charset = Charsets.UTF_8 ): List? { @@ -397,7 +405,7 @@ class CSVViewModel @Inject constructor( } } - private suspend fun parseCSV(csv: String): List { + private fun parseCSV(csv: String): List { val csvReader = CSVReaderBuilder(StringReader(csv)) .withLineValidator(object : LineValidator { override fun isValid(line: String?): Boolean { @@ -425,8 +433,52 @@ class CSVViewModel @Inject constructor( } // endregion - suspend private fun handleContinue() { + private suspend fun handleContinue() { + val csv = this.csv ?: return + val emptyStatus = MappingStatus(emptyList(), false) + + withContext(Dispatchers.IO) { + val result = csvImporter.import( + csv = csv, + importantFields = ImportantFields( + amount = amount, + date = date, + type = type, + account = account, + accountCurrency = account, + amountStatus = emptyStatus, + dateStatus = emptyStatus, + accountStatus = emptyStatus, + typeStatus = emptyStatus, + accountCurrencyStatus = emptyStatus, + ), + transferFields = TransferFields( + toAccount = toAccount, + toAccountCurrency = toAccountCurrency, + toAmount = toAmount, + toAmountStatus = emptyStatus, + toAccountStatus = emptyStatus, + toAccountCurrencyStatus = emptyStatus, + ), + optionalFields = OptionalFields( + category = category, + title = title, + description = description, + categoryStatus = emptyStatus, + titleStatus = emptyStatus, + descriptionStatus = emptyStatus, + ), + onProgress = { progressPercent -> + uiThread { + uiState = UIState.Processing( + (progressPercent * 100).roundToInt() + ) + } + } + ) + uiState = UIState.Result(result) + } } diff --git a/app/src/main/java/com/ivy/wallet/ui/csvimport/flow/ImportResultUI.kt b/app/src/main/java/com/ivy/wallet/ui/csvimport/flow/ImportResultUI.kt index 0760470e22..9eb0f366dc 100644 --- a/app/src/main/java/com/ivy/wallet/ui/csvimport/flow/ImportResultUI.kt +++ b/app/src/main/java/com/ivy/wallet/ui/csvimport/flow/ImportResultUI.kt @@ -26,7 +26,9 @@ import com.ivy.wallet.utils.format @Composable fun ImportResultUI( result: ImportResult, + isManualCsvImport: Boolean = false, + onTryAgain: (() -> Unit)? = null, onFinish: () -> Unit ) { Column( @@ -153,25 +155,27 @@ fun ImportResultUI( //TODO: Implement "See failed imports" - Spacer(modifier = Modifier.height(16.dp)) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = "If this didn't work, Try manual CSV import.", - color = UI.colors.pureInverse, - ) - Spacer(modifier = Modifier.height(8.dp)) - Button( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .padding(horizontal = 16.dp), - onClick = { - nav.navigateTo(CSVScreen) + if (!isManualCsvImport) { + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "If this didn't work, Try manual CSV import.", + color = UI.colors.pureInverse, + ) + Spacer(modifier = Modifier.height(8.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .padding(horizontal = 16.dp), + onClick = { + nav.navigateTo(CSVScreen) + } + ) { + Text(text = "Manual CSV import") } - ) { - Text(text = "Manual CSV import") } Spacer(Modifier.weight(1f)) @@ -189,6 +193,20 @@ fun ImportResultUI( onFinish() } + if (onTryAgain != null) { + Spacer(Modifier.height(12.dp)) + + Button( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + onClick = onTryAgain, + enabled = true + ) { + Text(text = "Try again") + } + } + Spacer(Modifier.height(16.dp)) } }