diff --git a/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/transaction/SaveTransactionLocallyAct.kt b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/transaction/SaveTransactionLocallyAct.kt new file mode 100644 index 0000000000..1a6d261ee5 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/domain/action/viewmodel/transaction/SaveTransactionLocallyAct.kt @@ -0,0 +1,17 @@ +package com.ivy.wallet.domain.action.viewmodel.transaction + +import com.ivy.frp.action.FPAction +import com.ivy.frp.then +import com.ivy.wallet.domain.data.core.Transaction +import com.ivy.wallet.io.persistence.dao.TransactionDao +import javax.inject.Inject + +class SaveTransactionLocallyAct @Inject constructor( + private val transactionDao: TransactionDao +) : FPAction() { + override suspend fun Transaction.compose(): suspend () -> Unit = { + this.copy( + isSynced = false + ).toEntity() + } then transactionDao::save +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionScreen.kt b/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionScreen.kt index 65e9da1d36..a2b85dd7f6 100644 --- a/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionScreen.kt @@ -4,12 +4,16 @@ import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.runtime.Composable import com.ivy.frp.view.navigation.Screen import com.ivy.wallet.domain.data.TransactionType +import com.ivy.wallet.domain.data.core.Account +import com.ivy.wallet.domain.data.core.Category import com.ivy.wallet.domain.data.core.Transaction import com.ivy.wallet.ui.architecture.FRP sealed class TransactionScreen : Screen { data class NewTransaction( - val type: TransactionType + val type: TransactionType, + val account: Account?, + val category: Category? ) : TransactionScreen() data class EditTransaction( @@ -22,7 +26,11 @@ fun BoxWithConstraintsScope.TransactionScreen(screen: TransactionScreen) { FRP( initialEvent = when (screen) { is TransactionScreen.EditTransaction -> TrnEvent.LoadTransaction(screen.transaction) - is TransactionScreen.NewTransaction -> TrnEvent.NewTransaction(screen.type) + is TransactionScreen.NewTransaction -> TrnEvent.NewTransaction( + type = screen.type, + account = screen.account, + category = screen.category + ) } ) { state, onEvent -> UI(state = state, onEvent = onEvent) diff --git a/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionViewModel.kt index b8e99c9337..a3387f4705 100644 --- a/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/transaction/TransactionViewModel.kt @@ -1,18 +1,136 @@ package com.ivy.wallet.ui.transaction +import arrow.core.NonEmptyList +import com.ivy.frp.monad.Res +import com.ivy.frp.monad.mapError +import com.ivy.frp.monad.mapSuccess +import com.ivy.frp.then +import com.ivy.frp.thenInvokeAfter import com.ivy.frp.viewmodel.FRPViewModel +import com.ivy.wallet.domain.action.account.AccountsAct +import com.ivy.wallet.domain.action.category.CategoriesAct +import com.ivy.wallet.domain.action.viewmodel.transaction.SaveTransactionLocallyAct +import com.ivy.wallet.domain.data.TransactionType +import com.ivy.wallet.domain.data.core.Transaction +import com.ivy.wallet.ui.transaction.data.TrnDate +import com.ivy.wallet.utils.timeNowUTC import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal +import java.util.* import javax.inject.Inject @HiltViewModel class TransactionViewModel @Inject constructor( - + private val accountsAct: AccountsAct, + private val categoriesAct: CategoriesAct, + private val saveTransactionLocallyAct: SaveTransactionLocallyAct ) : FRPViewModel() { override val _state: MutableStateFlow = MutableStateFlow(TrnState.Initial) - override suspend fun handleEvent(event: TrnEvent): suspend () -> TrnState { - TODO("Not yet implemented") + override suspend fun handleEvent(event: TrnEvent): suspend () -> TrnState = when (event) { + is TrnEvent.NewTransaction -> newTransaction(event) + is TrnEvent.LoadTransaction -> loadTransaction(event) + is TrnEvent.AccountChanged -> TODO() + is TrnEvent.AmountChanged -> TODO() + is TrnEvent.CategoryChanged -> TODO() + is TrnEvent.DateChanged -> TODO() + is TrnEvent.DescriptionChanged -> TODO() + is TrnEvent.DueChanged -> TODO() + is TrnEvent.TitleChanged -> TODO() + is TrnEvent.ToAccountChanged -> TODO() + is TrnEvent.TypeChanged -> TODO() + is TrnEvent.SetExchangeRate -> TODO() + is TrnEvent.Save -> TODO() + TrnEvent.LoadTitleSuggestions -> TODO() + } + + private suspend fun newTransaction(event: TrnEvent.NewTransaction) = ::loadRequiredData then { + if (it.first.isEmpty()) { + Res.Err("No accounts created") + } else { + Res.Ok( + Pair(NonEmptyList.fromListUnsafe(it.first), it.second) + ) + } + } mapError { errMsg -> + TrnState.Invalid(message = errMsg) + } mapSuccess { (accounts, categories) -> + TrnState.NewTransaction( + type = event.type, + account = event.account ?: accounts.head, + amount = BigDecimal.ZERO, + date = TrnDate.ActualDate(timeNowUTC()), + category = event.category, + title = null, + description = null, + + //TODO: Handle transfers properly + toAccount = null, + toAmount = null, + exchangeRate = null, + //TODO: Handle transfers properly + + titleSuggestions = emptyList(), + + accounts = accounts, + categories = categories + ) + } then { + when (it) { + is Res.Ok -> it.data + is Res.Err -> it.error + } + } + + private suspend fun loadTransaction(event: TrnEvent.LoadTransaction) = + ::loadRequiredData then { (accounts, categories) -> + TrnState.EditTransaction( + transaction = event.transaction, + + titleSuggestions = emptyList(), + + accounts = accounts, + categories = categories + ) + } + + private suspend fun loadRequiredData() = + accountsAct thenInvokeAfter { accs -> + Pair(accs, categoriesAct(Unit)) + } + + private suspend fun createNewTransaction(state: TrnState.NewTransaction) = with(state) { + if (amount <= BigDecimal.ZERO) { + return@with Res.Err("Transaction's amount can NOT be zero. Must be >0!") + } + + if (type == TransactionType.TRANSFER && toAccount == null) { + //TRANSFER w/o toAccount + return@with Res.Err("Transfers must have \"To\" account.") + } + + Res.Ok( + Transaction( + id = UUID.randomUUID(), + amount = amount, + accountId = account.id, + type = type, + categoryId = category?.id, + title = title, + description = description, + toAccountId = toAccount?.id, + toAmount = toAmount ?: amount, //TODO: Handle properly transfers exchange rate + dateTime = (date as? TrnDate.ActualDate)?.dateTime, + dueDate = (date as? TrnDate.DueDate)?.dueDate?.atTime(12, 0), + + attachmentUrl = null, + + isDeleted = false, + isSynced = false, + ) + ) } + private fun isEditMode(): Boolean = stateVal() is TrnState.EditTransaction } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/transaction/TrnEvent.kt b/app/src/main/java/com/ivy/wallet/ui/transaction/TrnEvent.kt index 702ed5802a..38a6182e52 100644 --- a/app/src/main/java/com/ivy/wallet/ui/transaction/TrnEvent.kt +++ b/app/src/main/java/com/ivy/wallet/ui/transaction/TrnEvent.kt @@ -1,14 +1,68 @@ package com.ivy.wallet.ui.transaction import com.ivy.wallet.domain.data.TransactionType +import com.ivy.wallet.domain.data.core.Account +import com.ivy.wallet.domain.data.core.Category import com.ivy.wallet.domain.data.core.Transaction +import java.math.BigDecimal +import java.time.LocalDate +import java.time.LocalDateTime sealed class TrnEvent { data class NewTransaction( - val type: TransactionType + val type: TransactionType, + val account: Account?, + val category: Category? ) : TrnEvent() data class LoadTransaction( val transaction: Transaction ) : TrnEvent() + + // --------------------------------- + data class AmountChanged( + val newAmount: BigDecimal + ) : TrnEvent() + + data class TitleChanged( + val newTitle: String + ) : TrnEvent() + + data class DescriptionChanged( + val newDesc: String + ) : TrnEvent() + + data class AccountChanged( + val newAccount: Account + ) : TrnEvent() + + data class CategoryChanged( + val newCategory: Category + ) : TrnEvent() + + data class DateChanged( + val dateTime: LocalDateTime + ) : TrnEvent() + + data class DueChanged( + val dueDateTime: LocalDate + ) : TrnEvent() + + data class TypeChanged( + val type: TransactionType + ) : TrnEvent() + + data class ToAccountChanged( + val account: Account + ) : TrnEvent() + + data class SetExchangeRate( + val exchangeRate: BigDecimal + ) : TrnEvent() + + //---------------------------- + + object Save : TrnEvent() + + object LoadTitleSuggestions : TrnEvent() } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/transaction/TrnState.kt b/app/src/main/java/com/ivy/wallet/ui/transaction/TrnState.kt index b42f6e8a4c..314cb7a8af 100644 --- a/app/src/main/java/com/ivy/wallet/ui/transaction/TrnState.kt +++ b/app/src/main/java/com/ivy/wallet/ui/transaction/TrnState.kt @@ -4,8 +4,9 @@ import com.ivy.wallet.domain.data.TransactionType import com.ivy.wallet.domain.data.core.Account import com.ivy.wallet.domain.data.core.Category import com.ivy.wallet.domain.data.core.Transaction +import com.ivy.wallet.ui.transaction.data.TrnDate +import com.ivy.wallet.ui.transaction.data.TrnExchangeRate import java.math.BigDecimal -import java.time.LocalDateTime sealed class TrnState { object Initial : TrnState() @@ -13,15 +14,32 @@ sealed class TrnState { data class NewTransaction( val type: TransactionType, val account: Account, - val toAccount: Account?, val amount: BigDecimal, val category: Category?, - val dateTime: LocalDateTime, + val date: TrnDate, val title: String?, - val description: String? + val description: String?, + + //Transfers + val toAccount: Account?, + val toAmount: BigDecimal?, + val exchangeRate: TrnExchangeRate?, + //-------------------------- + + val titleSuggestions: List, + + val accounts: List, + val categories: List ) : TrnState() data class EditTransaction( - val transaction: Transaction + val transaction: Transaction, + + val titleSuggestions: List, + + val accounts: List, + val categories: List ) : TrnState() + + data class Invalid(val message: String) : TrnState() } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/transaction/data/TrnDate.kt b/app/src/main/java/com/ivy/wallet/ui/transaction/data/TrnDate.kt new file mode 100644 index 0000000000..91afcac578 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/ui/transaction/data/TrnDate.kt @@ -0,0 +1,10 @@ +package com.ivy.wallet.ui.transaction.data + +import java.time.LocalDate +import java.time.LocalDateTime + +sealed class TrnDate { + data class ActualDate(val dateTime: LocalDateTime) : TrnDate() + + data class DueDate(val dueDate: LocalDate) : TrnDate() +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/transaction/data/TrnExchangeRate.kt b/app/src/main/java/com/ivy/wallet/ui/transaction/data/TrnExchangeRate.kt new file mode 100644 index 0000000000..e0ed97f02f --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/ui/transaction/data/TrnExchangeRate.kt @@ -0,0 +1,9 @@ +package com.ivy.wallet.ui.transaction.data + +import java.math.BigDecimal + +data class TrnExchangeRate( + val fromCurrency: String, + val toCurrency: String, + val rate: BigDecimal +) \ No newline at end of file