From f461c4f1e0b6b79765656183c7232c4353af7e40 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Fri, 30 Aug 2024 23:42:58 +0300 Subject: [PATCH] Edit transaction time fixes (part 1) (#3446) * Minor refactor * Minor refactor * WIP: Rework time in the VM * WIP: Rework * WIP: Fix time problems * Fix Detekt --- .../ivy/transaction/EditTransactionScreen.kt | 84 +++++----- .../ivy/transaction/EditTransactionState.kt | 36 ----- ...onEvent.kt => EditTransactionViewEvent.kt} | 74 ++++++--- .../transaction/EditTransactionViewModel.kt | 144 +++++++++--------- .../com/ivy/features/FeaturesViewModel.kt | 2 +- .../java/com/ivy/base/time/TimeConverter.kt | 2 + .../base/time/impl/StandardTimeConverter.kt | 3 + .../com/ivy/domain/features/BoolFeature.kt | 11 +- .../legacy/ui/theme/components/DateTimeRow.kt | 27 +++- .../ui/component/edit/TransactionDateTime.kt | 31 ++-- .../legacy/ui/component/edit/core/DueDate.kt | 20 ++- .../main/java/com/ivy/legacy/utils/DateExt.kt | 28 +--- .../java/com/ivy/design/utils/Keyboard.kt | 1 - 13 files changed, 251 insertions(+), 212 deletions(-) delete mode 100644 screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionState.kt rename screen/edit-transaction/src/main/java/com/ivy/transaction/{EditTransactionEvent.kt => EditTransactionViewEvent.kt} (50%) diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt index 508d5a40c2..7aa6e97723 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue @@ -34,6 +35,7 @@ import com.ivy.base.model.TransactionType import com.ivy.data.model.Category import com.ivy.data.model.Tag import com.ivy.data.model.TagId +import com.ivy.design.api.LocalTimeConverter import com.ivy.design.l0_system.Orange import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style @@ -42,11 +44,10 @@ import com.ivy.legacy.IvyWalletPreview import com.ivy.legacy.data.EditTransactionDisplayLoan import com.ivy.legacy.datamodel.Account import com.ivy.legacy.ivyWalletCtx -import com.ivy.legacy.rootView import com.ivy.legacy.ui.component.edit.TransactionDateTime +import com.ivy.legacy.ui.component.edit.core.Description import com.ivy.legacy.ui.component.tags.AddTagButton import com.ivy.legacy.ui.component.tags.ShowTagModal -import com.ivy.legacy.utils.convertUTCtoLocal import com.ivy.legacy.utils.onScreenStart import com.ivy.navigation.EditPlannedScreen import com.ivy.navigation.EditTransactionScreen @@ -55,11 +56,10 @@ import com.ivy.navigation.navigation import com.ivy.navigation.screenScopedViewModel import com.ivy.ui.R import com.ivy.wallet.domain.data.CustomExchangeRateState +import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import com.ivy.wallet.ui.edit.core.Category -import com.ivy.legacy.ui.component.edit.core.Description -import com.ivy.wallet.domain.data.IvyCurrency import com.ivy.wallet.ui.edit.core.DueDate import com.ivy.wallet.ui.edit.core.EditBottomSheet import com.ivy.wallet.ui.edit.core.Title @@ -83,9 +83,11 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime +import java.time.ZoneOffset import java.util.UUID import kotlin.math.roundToInt @@ -99,7 +101,7 @@ fun BoxWithConstraintsScope.EditTransactionScreen(screen: EditTransactionScreen) viewModel.start(screen) } - val view = rootView() + val view = LocalView.current UI( screen = screen, @@ -124,62 +126,62 @@ fun BoxWithConstraintsScope.EditTransactionScreen(screen: EditTransactionScreen) transactionAssociatedTags = uiState.transactionAssociatedTags, hasChanges = uiState.hasChanges, onSetDate = { - viewModel.onEvent(EditTransactionEvent.OnSetDate(it)) + viewModel.onEvent(EditTransactionViewEvent.OnSetDate(it)) }, onSetTime = { - viewModel.onEvent(EditTransactionEvent.OnSetTime(it)) + viewModel.onEvent(EditTransactionViewEvent.OnSetTime(it)) }, onTitleChange = { - viewModel.onEvent(EditTransactionEvent.OnTitleChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnTitleChanged(it)) }, onDescriptionChange = { - viewModel.onEvent(EditTransactionEvent.OnDescriptionChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnDescriptionChanged(it)) }, onAmountChange = { - viewModel.onEvent(EditTransactionEvent.OnAmountChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnAmountChanged(it)) }, onCategoryChange = { - viewModel.onEvent(EditTransactionEvent.OnCategoryChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnCategoryChanged(it)) }, onAccountChange = { - viewModel.onEvent(EditTransactionEvent.OnAccountChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnAccountChanged(it)) }, onToAccountChange = { - viewModel.onEvent(EditTransactionEvent.OnToAccountChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnToAccountChanged(it)) }, onDueDateChange = { - viewModel.onEvent(EditTransactionEvent.OnDueDateChanged(it)) + viewModel.onEvent(EditTransactionViewEvent.OnDueDateChanged(it)) }, onSetTransactionType = { - viewModel.onEvent(EditTransactionEvent.OnSetTransactionType(it)) + viewModel.onEvent(EditTransactionViewEvent.OnSetTransactionType(it)) }, onCreateCategory = { - viewModel.onEvent(EditTransactionEvent.CreateCategory(it)) + viewModel.onEvent(EditTransactionViewEvent.CreateCategory(it)) }, onEditCategory = { - viewModel.onEvent(EditTransactionEvent.EditCategory(it)) + viewModel.onEvent(EditTransactionViewEvent.EditCategory(it)) }, onPayPlannedPayment = { - viewModel.onEvent(EditTransactionEvent.OnPayPlannedPayment) + viewModel.onEvent(EditTransactionViewEvent.OnPayPlannedPayment) }, onSave = { view.hideKeyboard() - viewModel.onEvent(EditTransactionEvent.Save(it)) + viewModel.onEvent(EditTransactionViewEvent.Save(it)) }, onSetHasChanges = { - viewModel.onEvent(EditTransactionEvent.SetHasChanges(it)) + viewModel.onEvent(EditTransactionViewEvent.SetHasChanges(it)) }, onDelete = { - viewModel.onEvent(EditTransactionEvent.Delete) + viewModel.onEvent(EditTransactionViewEvent.Delete) }, onDuplicate = { - viewModel.onEvent(EditTransactionEvent.Duplicate) + viewModel.onEvent(EditTransactionViewEvent.Duplicate) }, onCreateAccount = { - viewModel.onEvent(EditTransactionEvent.CreateAccount(it)) + viewModel.onEvent(EditTransactionViewEvent.CreateAccount(it)) }, onExchangeRateChange = { - viewModel.onEvent(EditTransactionEvent.UpdateExchangeRate(it)) + viewModel.onEvent(EditTransactionViewEvent.UpdateExchangeRate(it)) }, onTagOperation = { viewModel.onEvent(it) @@ -198,10 +200,10 @@ private fun BoxWithConstraintsScope.UI( titleSuggestions: ImmutableSet, description: String?, category: Category?, - dateTime: LocalDateTime?, + dateTime: Instant?, account: Account?, toAccount: Account?, - dueDate: LocalDateTime?, + dueDate: Instant?, amount: Double, customExchangeRateState: CustomExchangeRateState, @@ -229,7 +231,7 @@ private fun BoxWithConstraintsScope.UI( onDuplicate: () -> Unit, onCreateAccount: (CreateAccountData) -> Unit, onExchangeRateChange: (Double?) -> Unit = { }, - onTagOperation: (EditTransactionEvent.TagEvent) -> Unit = {}, + onTagOperation: (EditTransactionViewEvent.TagEvent) -> Unit = {}, loanData: EditTransactionDisplayLoan = EditTransactionDisplayLoan(), backgroundProcessing: Boolean = false, hasChanges: Boolean = false, @@ -356,10 +358,13 @@ private fun BoxWithConstraintsScope.UI( val ivyContext = ivyWalletCtx() + val timeConverter = LocalTimeConverter.current if (dueDate != null) { DueDate(dueDate = dueDate) { ivyContext.datePicker( - initialDate = dueDate.toLocalDate() + initialDate = with(timeConverter) { + dueDate.toLocalDate() + } ) { onDueDateChange(it.atTime(12, 0)) } @@ -379,14 +384,18 @@ private fun BoxWithConstraintsScope.UI( dueDateTime = dueDate, onEditDate = { ivyContext.datePicker( - initialDate = dateTime?.convertUTCtoLocal()?.toLocalDate() + initialDate = with(timeConverter) { + dateTime?.toLocalDate() + } ) { date -> onSetDate((date)) } }, onEditTime = { ivyContext.timePicker( - initialTime = dateTime?.toLocalTime() + initialTime = with(timeConverter) { + dateTime?.toLocalTime() + } ) { time -> onSetTime(time) } @@ -626,27 +635,27 @@ private fun BoxWithConstraintsScope.UI( onDismiss = { tagModelVisible = false // Reset TagList, avoids showing incorrect tag list when user has searched for a tag - onTagOperation(EditTransactionEvent.TagEvent.OnTagSearch("")) + onTagOperation(EditTransactionViewEvent.TagEvent.OnTagSearch("")) }, allTagList = tags, selectedTagList = transactionAssociatedTags, onTagAdd = { - onTagOperation(EditTransactionEvent.TagEvent.SaveTag(name = it)) + onTagOperation(EditTransactionViewEvent.TagEvent.SaveTag(name = it)) }, onTagEdit = { oldTag, newTag -> - onTagOperation(EditTransactionEvent.TagEvent.OnTagEdit(oldTag, newTag)) + onTagOperation(EditTransactionViewEvent.TagEvent.OnTagEdit(oldTag, newTag)) }, onTagDelete = { - onTagOperation(EditTransactionEvent.TagEvent.OnTagDelete(it)) + onTagOperation(EditTransactionViewEvent.TagEvent.OnTagDelete(it)) }, onTagSelected = { - onTagOperation(EditTransactionEvent.TagEvent.OnTagSelect(it)) + onTagOperation(EditTransactionViewEvent.TagEvent.OnTagSelect(it)) }, onTagDeSelected = { - onTagOperation(EditTransactionEvent.TagEvent.OnTagDeSelect(it)) + onTagOperation(EditTransactionViewEvent.TagEvent.OnTagDeSelect(it)) }, onTagSearch = { - onTagOperation(EditTransactionEvent.TagEvent.OnTagSearch(it)) + onTagOperation(EditTransactionViewEvent.TagEvent.OnTagSearch(it)) } ) } @@ -664,6 +673,7 @@ private fun shouldFocusAmount(amount: Double) = amount == 0.0 /** For Preview purpose **/ private val testDateTime = LocalDateTime.of(2023, 4, 27, 0, 35) + .toInstant(ZoneOffset.UTC) @ExperimentalFoundationApi @Preview diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionState.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionState.kt deleted file mode 100644 index 22a9378915..0000000000 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionState.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.ivy.transaction - -import androidx.compose.runtime.Immutable -import com.ivy.base.model.TransactionType -import com.ivy.data.model.Category -import com.ivy.data.model.Tag -import com.ivy.data.model.TagId -import com.ivy.legacy.data.EditTransactionDisplayLoan -import com.ivy.legacy.datamodel.Account -import com.ivy.wallet.domain.data.CustomExchangeRateState -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableSet -import java.time.LocalDateTime - -@Immutable -data class EditTransactionState( - val transactionType: TransactionType, - val initialTitle: String?, - val titleSuggestions: ImmutableSet, - val currency: String, - val description: String?, - val dateTime: LocalDateTime?, - val dueDate: LocalDateTime?, - val accounts: ImmutableList, - val categories: ImmutableList, - val account: Account?, - val toAccount: Account?, - val category: Category?, - val amount: Double, - val hasChanges: Boolean, - val displayLoanHelper: EditTransactionDisplayLoan, - val backgroundProcessingStarted: Boolean, - val customExchangeRateState: CustomExchangeRateState, - val tags: ImmutableList, - val transactionAssociatedTags: ImmutableList -) diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewEvent.kt similarity index 50% rename from screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt rename to screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewEvent.kt index 763eb689e1..3a5357aa4f 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionEvent.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewEvent.kt @@ -1,38 +1,68 @@ package com.ivy.transaction +import androidx.compose.runtime.Immutable import com.ivy.base.model.TransactionType import com.ivy.data.model.Category import com.ivy.data.model.Tag +import com.ivy.data.model.TagId +import com.ivy.legacy.data.EditTransactionDisplayLoan import com.ivy.legacy.datamodel.Account +import com.ivy.wallet.domain.data.CustomExchangeRateState import com.ivy.wallet.domain.deprecated.logic.model.CreateAccountData import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime -sealed interface EditTransactionEvent { - data class OnAmountChanged(val newAmount: Double) : EditTransactionEvent - data class OnTitleChanged(val newTitle: String?) : EditTransactionEvent - data class OnDescriptionChanged(val newDescription: String?) : EditTransactionEvent - data class OnCategoryChanged(val newCategory: Category?) : EditTransactionEvent - data class OnAccountChanged(val newAccount: Account) : EditTransactionEvent - data class OnToAccountChanged(val newAccount: Account) : EditTransactionEvent - data class OnDueDateChanged(val newDueDate: LocalDateTime?) : EditTransactionEvent - data class OnSetDateTime(val newDateTime: LocalDateTime) : EditTransactionEvent - data class OnSetDate(val newDate: LocalDate) : EditTransactionEvent - data class OnSetTime(val newTime: LocalTime) : EditTransactionEvent - data class OnSetTransactionType(val newTransactionType: TransactionType) : EditTransactionEvent - data object OnPayPlannedPayment : EditTransactionEvent - data object Delete : EditTransactionEvent - data object Duplicate : EditTransactionEvent - data class CreateCategory(val data: CreateCategoryData) : EditTransactionEvent - data class EditCategory(val updatedCategory: Category) : EditTransactionEvent - data class CreateAccount(val data: CreateAccountData) : EditTransactionEvent - data class Save(val closeScreen: Boolean) : EditTransactionEvent - data class SetHasChanges(val hasChangesValue: Boolean) : EditTransactionEvent - data class UpdateExchangeRate(val exRate: Double?) : EditTransactionEvent +@Immutable +data class EditTransactionViewState( + val transactionType: TransactionType, + val initialTitle: String?, + val titleSuggestions: ImmutableSet, + val currency: String, + val description: String?, + val dateTime: Instant?, + val dueDate: Instant?, + val accounts: ImmutableList, + val categories: ImmutableList, + val account: Account?, + val toAccount: Account?, + val category: Category?, + val amount: Double, + val hasChanges: Boolean, + val displayLoanHelper: EditTransactionDisplayLoan, + val backgroundProcessingStarted: Boolean, + val customExchangeRateState: CustomExchangeRateState, + val tags: ImmutableList, + val transactionAssociatedTags: ImmutableList +) - sealed interface TagEvent : EditTransactionEvent { +sealed interface EditTransactionViewEvent { + data class OnAmountChanged(val newAmount: Double) : EditTransactionViewEvent + data class OnTitleChanged(val newTitle: String?) : EditTransactionViewEvent + data class OnDescriptionChanged(val newDescription: String?) : EditTransactionViewEvent + data class OnCategoryChanged(val newCategory: Category?) : EditTransactionViewEvent + data class OnAccountChanged(val newAccount: Account) : EditTransactionViewEvent + data class OnToAccountChanged(val newAccount: Account) : EditTransactionViewEvent + data class OnDueDateChanged(val newDueDate: LocalDateTime?) : EditTransactionViewEvent + data class OnSetDateTime(val newDateTime: LocalDateTime) : EditTransactionViewEvent + data class OnSetDate(val newDate: LocalDate) : EditTransactionViewEvent + data class OnSetTime(val newTime: LocalTime) : EditTransactionViewEvent + data class OnSetTransactionType(val newTransactionType: TransactionType) : EditTransactionViewEvent + data object OnPayPlannedPayment : EditTransactionViewEvent + data object Delete : EditTransactionViewEvent + data object Duplicate : EditTransactionViewEvent + data class CreateCategory(val data: CreateCategoryData) : EditTransactionViewEvent + data class EditCategory(val updatedCategory: Category) : EditTransactionViewEvent + data class CreateAccount(val data: CreateAccountData) : EditTransactionViewEvent + data class Save(val closeScreen: Boolean) : EditTransactionViewEvent + data class SetHasChanges(val hasChangesValue: Boolean) : EditTransactionViewEvent + data class UpdateExchangeRate(val exRate: Double?) : EditTransactionViewEvent + + sealed interface TagEvent : EditTransactionViewEvent { data class SaveTag(val name: String) : TagEvent data class OnTagSelect(val selectedTag: Tag) : TagEvent data class OnTagDeSelect(val selectedTag: Tag) : TagEvent diff --git a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt index 2086a9d822..eb3e0b2220 100644 --- a/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt +++ b/screen/edit-transaction/src/main/java/com/ivy/transaction/EditTransactionViewModel.kt @@ -35,10 +35,7 @@ import com.ivy.legacy.datamodel.temp.toDomain import com.ivy.legacy.domain.deprecated.logic.AccountCreator import com.ivy.legacy.utils.computationThread import com.ivy.legacy.utils.convertUTCToLocal -import com.ivy.legacy.utils.dateNowLocal -import com.ivy.legacy.utils.getTrueDate import com.ivy.legacy.utils.ioThread -import com.ivy.legacy.utils.timeUTC import com.ivy.legacy.utils.toLowerCaseLocal import com.ivy.legacy.utils.uiThread import com.ivy.navigation.EditTransactionScreen @@ -71,10 +68,9 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import java.math.BigDecimal +import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime @@ -110,18 +106,15 @@ class EditTransactionViewModel @Inject constructor( private val features: Features, private val timeConverter: TimeConverter, private val timeProvider: TimeProvider, -) : ComposeViewModel() { +) : ComposeViewModel() { private val transactionType = mutableStateOf(TransactionType.EXPENSE) private val initialTitle = mutableStateOf(null) private val titleSuggestions = mutableStateOf(persistentSetOf()) private val currency = mutableStateOf("") private val description = mutableStateOf(null) - private val dateTime = mutableStateOf(null) - private val dueDate = mutableStateOf(null) - private val paidHistory = mutableStateOf(null) - private val date = MutableStateFlow(null) - private val time = MutableStateFlow(null) + private val dateTime = mutableStateOf(null) + private val dueDate = mutableStateOf(null) private val accounts = mutableStateOf>(persistentListOf()) private val categories = mutableStateOf>(persistentListOf()) private val tags = mutableStateOf>(persistentListOf()) @@ -133,6 +126,8 @@ class EditTransactionViewModel @Inject constructor( private val hasChanges = mutableStateOf(false) private val displayLoanHelper = mutableStateOf(EditTransactionDisplayLoan()) + private var paidHistory: Instant? = null + // This is used to when the transaction is associated with a loan/loan record, // used to indicate the background updating of loan/loanRecord data private val backgroundProcessingStarted = mutableStateOf(false) @@ -192,8 +187,8 @@ class EditTransactionViewModel @Inject constructor( } @Composable - override fun uiState(): EditTransactionState { - return EditTransactionState( + override fun uiState(): EditTransactionViewState { + return EditTransactionViewState( transactionType = getTransactionType(), initialTitle = getInitialTitle(), titleSuggestions = getTitleSuggestions(), @@ -242,12 +237,12 @@ class EditTransactionViewModel @Inject constructor( } @Composable - private fun getDateTime(): LocalDateTime? { + private fun getDateTime(): Instant? { return dateTime.value } @Composable - private fun getDueDate(): LocalDateTime? { + private fun getDueDate(): Instant? { return dueDate.value } @@ -311,40 +306,45 @@ class EditTransactionViewModel @Inject constructor( return transactionAssociatedTags.value } - override fun onEvent(event: EditTransactionEvent) { + @Suppress("CyclomaticComplexMethod") + override fun onEvent(event: EditTransactionViewEvent) { when (event) { - is EditTransactionEvent.CreateAccount -> createAccount(event.data) - is EditTransactionEvent.CreateCategory -> createCategory(event.data) - EditTransactionEvent.Delete -> delete() - EditTransactionEvent.Duplicate -> duplicate() - is EditTransactionEvent.EditCategory -> editCategory(event.updatedCategory) - is EditTransactionEvent.OnAccountChanged -> onAccountChanged(event.newAccount) - is EditTransactionEvent.OnAmountChanged -> onAmountChanged(event.newAmount) - is EditTransactionEvent.OnCategoryChanged -> onCategoryChanged(event.newCategory) - is EditTransactionEvent.OnDescriptionChanged -> + is EditTransactionViewEvent.CreateAccount -> createAccount(event.data) + is EditTransactionViewEvent.CreateCategory -> createCategory(event.data) + EditTransactionViewEvent.Delete -> delete() + EditTransactionViewEvent.Duplicate -> duplicate() + is EditTransactionViewEvent.EditCategory -> editCategory(event.updatedCategory) + is EditTransactionViewEvent.OnAccountChanged -> onAccountChanged(event.newAccount) + is EditTransactionViewEvent.OnAmountChanged -> onAmountChanged(event.newAmount) + is EditTransactionViewEvent.OnCategoryChanged -> onCategoryChanged(event.newCategory) + is EditTransactionViewEvent.OnDescriptionChanged -> onDescriptionChanged(event.newDescription) - is EditTransactionEvent.OnDueDateChanged -> onDueDateChanged(event.newDueDate) - EditTransactionEvent.OnPayPlannedPayment -> onPayPlannedPayment() - is EditTransactionEvent.OnSetDateTime -> onSetDateTime(event.newDateTime) - is EditTransactionEvent.OnSetDate -> onSetDate(event.newDate) - is EditTransactionEvent.OnSetTime -> onSetTime(event.newTime) - is EditTransactionEvent.OnSetTransactionType -> + is EditTransactionViewEvent.OnDueDateChanged -> onDueDateChanged(event.newDueDate) + EditTransactionViewEvent.OnPayPlannedPayment -> onPayPlannedPayment() + is EditTransactionViewEvent.OnSetDateTime -> onSetDateTime(event.newDateTime) + is EditTransactionViewEvent.OnSetDate -> onSetDate(event.newDate) + is EditTransactionViewEvent.OnSetTime -> onSetTime(event.newTime) + is EditTransactionViewEvent.OnSetTransactionType -> onSetTransactionType(event.newTransactionType) - is EditTransactionEvent.OnTitleChanged -> onTitleChanged(event.newTitle) - is EditTransactionEvent.OnToAccountChanged -> onToAccountChanged(event.newAccount) - is EditTransactionEvent.Save -> save(event.closeScreen) - is EditTransactionEvent.SetHasChanges -> setHasChanges(event.hasChangesValue) - is EditTransactionEvent.UpdateExchangeRate -> updateExchangeRate(event.exRate) - is EditTransactionEvent.TagEvent -> when (event) { - is EditTransactionEvent.TagEvent.SaveTag -> onTagSaved(event.name) - is EditTransactionEvent.TagEvent.OnTagSelect -> associateTagToTransaction(event.selectedTag) - is EditTransactionEvent.TagEvent.OnTagDeSelect -> removeTagAssociation(event.selectedTag) - is EditTransactionEvent.TagEvent.OnTagSearch -> searchTag(event.query) - is EditTransactionEvent.TagEvent.OnTagDelete -> deleteTag(event.selectedTag) - is EditTransactionEvent.TagEvent.OnTagEdit -> updateTagInformation(event.newTag) - } + is EditTransactionViewEvent.OnTitleChanged -> onTitleChanged(event.newTitle) + is EditTransactionViewEvent.OnToAccountChanged -> onToAccountChanged(event.newAccount) + is EditTransactionViewEvent.Save -> save(event.closeScreen) + is EditTransactionViewEvent.SetHasChanges -> setHasChanges(event.hasChangesValue) + is EditTransactionViewEvent.UpdateExchangeRate -> updateExchangeRate(event.exRate) + is EditTransactionViewEvent.TagEvent -> handleTagEvent(event) + } + } + + private fun handleTagEvent(event: EditTransactionViewEvent.TagEvent) { + when (event) { + is EditTransactionViewEvent.TagEvent.SaveTag -> onTagSaved(event.name) + is EditTransactionViewEvent.TagEvent.OnTagSelect -> associateTagToTransaction(event.selectedTag) + is EditTransactionViewEvent.TagEvent.OnTagDeSelect -> removeTagAssociation(event.selectedTag) + is EditTransactionViewEvent.TagEvent.OnTagSearch -> searchTag(event.query) + is EditTransactionViewEvent.TagEvent.OnTagDelete -> deleteTag(event.selectedTag) + is EditTransactionViewEvent.TagEvent.OnTagEdit -> updateTagInformation(event.newTag) } } @@ -378,10 +378,10 @@ class EditTransactionViewModel @Inject constructor( transactionType.value = transaction.type initialTitle.value = transaction.title - dateTime.value = with(timeConverter) { transaction.dateTime?.toLocalDateTime() } + dateTime.value = transaction.dateTime description.value = transaction.description - dueDate.value = with(timeConverter) { transaction.dueDate?.toLocalDateTime() } - paidHistory.value = with(timeConverter) { transaction.paidFor?.toLocalDateTime() } + dueDate.value = transaction.dueDate + paidHistory = transaction.paidFor val selectedAccount = accountByIdAct(transaction.accountId)!! account.value = selectedAccount toAccount.value = transaction.toAccountId?.let { @@ -527,19 +527,21 @@ class EditTransactionViewModel @Inject constructor( } private fun onDueDateChanged(newDueDate: LocalDateTime?) { + val newDueDateUtc = with(timeConverter) { newDueDate?.toUTC() } loadedTransaction = loadedTransaction().copy( - dueDate = with(timeConverter) { newDueDate?.toUTC() } + dueDate = newDueDateUtc ) - dueDate.value = newDueDate + dueDate.value = newDueDateUtc saveIfEditMode() } private fun onSetDateTime(newDateTime: LocalDateTime) { + val newDateTimeUtc = with(timeConverter) { newDateTime.toUTC() } loadedTransaction = loadedTransaction().copy( - dateTime = with(timeConverter) { newDateTime.toUTC() } + dateTime = newDateTimeUtc ) - dateTime.value = newDateTime + dateTime.value = newDateTimeUtc saveIfEditMode() } @@ -548,13 +550,14 @@ class EditTransactionViewModel @Inject constructor( loadedTransaction = loadedTransaction().copy( date = newDate ) - date.value = newDate + val localDateTime = with(timeConverter) { + (dateTime.value ?: timeProvider.utcNow()).toLocalDateTime() + } onSetDateTime( - getTrueDate( - loadedTransaction?.date ?: dateNowLocal(), - (dateTime.value?.toLocalTime() ?: timeUTC()), - true - ) + localDateTime + .withDayOfMonth(newDate.dayOfMonth) + .withMonth(newDate.monthValue) + .withYear(newDate.year) ) } @@ -562,13 +565,15 @@ class EditTransactionViewModel @Inject constructor( loadedTransaction = loadedTransaction().copy( time = newTime.convertUTCToLocal() ) - time.value = newTime + val localDateTime = with(timeConverter) { + (dateTime.value ?: timeProvider.utcNow()).toLocalDateTime() + } onSetDateTime( - getTrueDate( - dateTime.value?.toLocalDate() ?: dateNowLocal(), - loadedTransaction?.time ?: timeUTC(), - true - ) + localDateTime + .withHour(newTime.hour) + .withMinute(newTime.minute) + .withSecond(0) + .withNano(0) ) } @@ -587,10 +592,9 @@ class EditTransactionViewModel @Inject constructor( syncTransaction = false ) { paidTransaction -> loadedTransaction = paidTransaction - paidHistory.value = - with(timeConverter) { paidTransaction.paidFor?.toLocalDateTime() } - dueDate.value = with(timeConverter) { paidTransaction.dueDate?.toLocalDateTime() } - dateTime.value = with(timeConverter) { paidTransaction.dateTime?.toLocalDateTime() } + paidHistory = paidTransaction.paidFor + dueDate.value = paidTransaction.dueDate + dateTime.value = paidTransaction.dateTime saveIfEditMode( closeScreen = true @@ -702,8 +706,8 @@ class EditTransactionViewModel @Inject constructor( description = description.value?.trim(), amount = amount, type = transactionType.value, - dueDate = with(timeConverter) { dueDate.value?.toUTC() }, - paidFor = with(timeConverter) { paidHistory.value?.toUTC() }, + dueDate = dueDate.value, + paidFor = paidHistory, dateTime = when { loadedTransaction().dateTime == null && dueDate.value == null -> { @@ -961,6 +965,6 @@ class EditTransactionViewModel @Inject constructor( } private suspend fun shouldSortCategoriesAlphabetically(): Boolean { - return features.sortCategoriesAlphabetically.enabled(context).firstOrNull() ?: false + return features.sortCategoriesAlphabetically.isEnabled(context) } } diff --git a/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt b/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt index 8ca43c9fe1..923e14c1da 100644 --- a/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt +++ b/screen/features/src/main/java/com/ivy/features/FeaturesViewModel.kt @@ -51,7 +51,7 @@ class FeaturesViewModel @Inject constructor( private fun toggleFeature(event: FeaturesUiEvent.ToggleFeature) { viewModelScope.launch { val feature = features.allFeatures[event.index] - val enabled = feature.enabled(context).first() ?: false + val enabled = feature.enabledFlow(context).first() ?: false feature.set(context, !enabled) } } diff --git a/shared/base/src/main/java/com/ivy/base/time/TimeConverter.kt b/shared/base/src/main/java/com/ivy/base/time/TimeConverter.kt index 6fa1e93f9d..3d43582e30 100644 --- a/shared/base/src/main/java/com/ivy/base/time/TimeConverter.kt +++ b/shared/base/src/main/java/com/ivy/base/time/TimeConverter.kt @@ -3,10 +3,12 @@ package com.ivy.base.time import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime +import java.time.LocalTime interface TimeConverter { fun Instant.toLocalDateTime(): LocalDateTime fun Instant.toLocalDate(): LocalDate + fun Instant.toLocalTime(): LocalTime fun LocalDateTime.toUTC(): Instant } \ No newline at end of file diff --git a/shared/base/src/main/java/com/ivy/base/time/impl/StandardTimeConverter.kt b/shared/base/src/main/java/com/ivy/base/time/impl/StandardTimeConverter.kt index 4cbb375a8e..e815b63ecb 100644 --- a/shared/base/src/main/java/com/ivy/base/time/impl/StandardTimeConverter.kt +++ b/shared/base/src/main/java/com/ivy/base/time/impl/StandardTimeConverter.kt @@ -8,6 +8,7 @@ import java.time.DateTimeException import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime +import java.time.LocalTime import java.time.ZoneOffset import javax.inject.Inject @@ -43,4 +44,6 @@ class StandardTimeConverter @Inject constructor( val zoneId = timeZoneProvider.getZoneId() return this.atZone(zoneId).toInstant() } + + override fun Instant.toLocalTime(): LocalTime = toLocalDateTime().toLocalTime() } \ No newline at end of file diff --git a/shared/domain/src/main/java/com/ivy/domain/features/BoolFeature.kt b/shared/domain/src/main/java/com/ivy/domain/features/BoolFeature.kt index b7f7736879..f921359432 100644 --- a/shared/domain/src/main/java/com/ivy/domain/features/BoolFeature.kt +++ b/shared/domain/src/main/java/com/ivy/domain/features/BoolFeature.kt @@ -11,6 +11,7 @@ import androidx.datastore.preferences.core.edit import com.ivy.data.datastore.DatastoreKeys import com.ivy.data.datastore.dataStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @Immutable @@ -22,16 +23,18 @@ class BoolFeature( @Composable fun asEnabledState(): Boolean { val context = LocalContext.current - val featureFlag = remember { enabled(context) } + val featureFlag = remember { enabledFlow(context) } .collectAsState(false).value return featureFlag ?: false } - fun enabled(appContext: Context): Flow { - return appContext.dataStore.data.map { + suspend fun isEnabled(appContext: Context): Boolean = + enabledFlow(appContext).first() ?: false + + fun enabledFlow(appContext: Context): Flow = appContext.dataStore + .data.map { it[featureKey] } - } suspend fun set(appContext: Context, enabled: Boolean) { appContext.dataStore.edit { diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/DateTimeRow.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/DateTimeRow.kt index 7fc77fc86a..43662c5172 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/DateTimeRow.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/legacy/ui/theme/components/DateTimeRow.kt @@ -9,13 +9,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.ivy.legacy.ivyWalletCtx +import com.ivy.legacy.utils.convertLocalToUTC +import com.ivy.legacy.utils.convertUTCToLocal import com.ivy.legacy.utils.convertUTCtoLocal import com.ivy.legacy.utils.formatLocalTime import com.ivy.legacy.utils.formatNicely -import com.ivy.legacy.utils.getTrueDate +import com.ivy.legacy.utils.timeNowUTC import com.ivy.ui.R import com.ivy.wallet.ui.theme.components.IvyOutlinedButton +import java.time.LocalDate import java.time.LocalDateTime +import java.time.LocalTime @Composable fun DateTimeRow( @@ -57,4 +61,25 @@ fun DateTimeRow( Spacer(Modifier.width(24.dp)) } +} + +// The timepicker returns time in UTC, but the date picker returns date in LocalTimeZone +// hence use this method to get both date & time in UTC +@Deprecated("Rework this to use the TimeConverter API") +fun getTrueDate( + date: LocalDate, + time: LocalTime, + convert: Boolean = true +): LocalDateTime { + val timeLocal = if (convert) time.convertUTCToLocal() else time + + return timeNowUTC() + .withYear(date.year) + .withMonth(date.monthValue) + .withDayOfMonth(date.dayOfMonth) + .withHour(timeLocal.hour) + .withMinute(timeLocal.minute) + .withSecond(0) + .withNano(0) + .convertLocalToUTC() } \ No newline at end of file diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/TransactionDateTime.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/TransactionDateTime.kt index f1064330ab..a4980b5810 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/TransactionDateTime.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/TransactionDateTime.kt @@ -17,23 +17,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.ivy.design.api.LocalTimeConverter +import com.ivy.design.api.LocalTimeFormatter +import com.ivy.design.api.LocalTimeProvider import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.legacy.IvyWalletComponentPreview -import com.ivy.legacy.utils.formatNicely -import com.ivy.legacy.utils.formatTimeOnly -import com.ivy.legacy.utils.timeNowLocal -import com.ivy.legacy.utils.timeNowUTC import com.ivy.ui.R +import com.ivy.ui.time.TimeFormatter import com.ivy.wallet.ui.theme.components.IvyIcon -import java.time.LocalDateTime +import java.time.Instant @Suppress("MultipleEmitters") @Deprecated("Old design system. Use `:ivy-design` and Material3") @Composable fun TransactionDateTime( - dateTime: LocalDateTime?, - dueDateTime: LocalDateTime?, + dateTime: Instant?, + dueDateTime: Instant?, onEditDate: () -> Unit, onEditTime: () -> Unit, modifier: Modifier = Modifier @@ -67,10 +67,14 @@ fun TransactionDateTime( Spacer(Modifier.width(24.dp)) Spacer(Modifier.weight(1f)) + val localDateTime = with(LocalTimeConverter.current) { + (dateTime ?: LocalTimeProvider.current.utcNow()).toLocalDateTime() + } + val timeFormatter = LocalTimeFormatter.current Text( - text = (dateTime ?: timeNowUTC()).formatNicely( - noWeekDay = true - ), + text = with(timeFormatter) { + localDateTime.format(TimeFormatter.Style.DateOnly(includeWeekDay = false)) + }, style = UI.typo.nB2.style( color = UI.colors.pureInverse, fontWeight = FontWeight.ExtraBold @@ -79,8 +83,11 @@ fun TransactionDateTime( onEditDate() } ) + Text( - text = " " + (dateTime?.formatTimeOnly() ?: timeNowLocal().formatTimeOnly()), + text = " " + with(timeFormatter) { + localDateTime.toLocalTime().format() + }, style = UI.typo.nB2.style( color = UI.colors.pureInverse, fontWeight = FontWeight.ExtraBold @@ -99,7 +106,7 @@ fun TransactionDateTime( private fun Preview() { IvyWalletComponentPreview { TransactionDateTime( - dateTime = timeNowUTC(), + dateTime = LocalTimeProvider.current.utcNow(), dueDateTime = null, onEditDate = { }, diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/DueDate.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/DueDate.kt index c9d95cd66d..3bac46a856 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/DueDate.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/ui/component/edit/core/DueDate.kt @@ -16,19 +16,21 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.ivy.design.api.LocalTimeFormatter +import com.ivy.design.api.LocalTimeProvider import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.legacy.IvyWalletComponentPreview -import com.ivy.legacy.utils.formatDateOnly -import com.ivy.legacy.utils.timeNowUTC import com.ivy.ui.R +import com.ivy.ui.time.TimeFormatter import com.ivy.wallet.ui.theme.components.IvyIcon -import java.time.LocalDateTime +import java.time.Instant +import java.util.concurrent.TimeUnit @Deprecated("Old design system. Use `:ivy-design` and Material3") @Composable fun DueDate( - dueDate: LocalDateTime, + dueDate: Instant, onPickDueDate: () -> Unit, ) { DueDateCard( @@ -41,7 +43,7 @@ fun DueDate( @Composable private fun DueDateCard( - dueDate: LocalDateTime, + dueDate: Instant, onClick: () -> Unit, ) { Row( @@ -71,7 +73,9 @@ private fun DueDateCard( Spacer(Modifier.weight(1f)) Text( - text = dueDate.toLocalDate().formatDateOnly(), + text = with(LocalTimeFormatter.current) { + dueDate.formatLocal(TimeFormatter.Style.DateOnly(includeWeekDay = false)) + }, style = UI.typo.nB2.style( fontWeight = FontWeight.ExtraBold ) @@ -81,12 +85,14 @@ private fun DueDateCard( } } +@Suppress("MagicNumber") @Preview @Composable private fun Preview_OneTime() { IvyWalletComponentPreview { DueDate( - dueDate = timeNowUTC().plusDays(5), + dueDate = LocalTimeProvider.current.utcNow() + .plusSeconds(TimeUnit.DAYS.toSeconds(6)), ) { } } diff --git a/temp/legacy-code/src/main/java/com/ivy/legacy/utils/DateExt.kt b/temp/legacy-code/src/main/java/com/ivy/legacy/utils/DateExt.kt index ce6a0e79bc..0d4ca48160 100644 --- a/temp/legacy-code/src/main/java/com/ivy/legacy/utils/DateExt.kt +++ b/temp/legacy-code/src/main/java/com/ivy/legacy/utils/DateExt.kt @@ -42,6 +42,7 @@ fun LocalDateTime.toEpochSeconds() = this.toEpochSecond(ZoneOffset.UTC) fun LocalDateTime.millis() = this.toInstant(ZoneOffset.UTC).toEpochMilli() +@Deprecated("Use the TimeConverter interface via DI") fun LocalDateTime.formatNicely( noWeekDay: Boolean = false, zone: ZoneId = ZoneOffset.systemDefault() @@ -84,6 +85,7 @@ fun LocalDateTime.formatNicely( fun LocalDateTime.getISOFormattedDateTime(): String = this.formatLocal("yyyyMMdd-HHmm") +@Deprecated("Use the TimeConverter interface via DI") fun LocalDateTime.formatNicelyWithTime( noWeekDay: Boolean = true, zone: ZoneId = ZoneOffset.systemDefault() @@ -124,25 +126,25 @@ fun LocalDateTime.formatNicelyWithTime( } } +@Deprecated("Use the TimeConverter interface via DI") @Composable fun LocalDateTime.formatLocalTime(): String { val timeFormat = android.text.format.DateFormat.getTimeFormat(LocalContext.current) return timeFormat.format(this.millis()) } +@Deprecated("Use the TimeConverter interface via DI") fun LocalDate.formatDateOnly(): String = this.formatLocal("MMM. dd", ZoneOffset.systemDefault()) -fun LocalDateTime.formatTimeOnly(): String = this.format(DateTimeFormatter.ofPattern("HH:mm")) - +@Deprecated("Use the TimeConverter interface via DI") fun LocalDate.formatDateOnlyWithYear(): String = this.formatLocal("dd MMM, yyyy", ZoneOffset.systemDefault()) -fun LocalDate.formatDateWeekDay(): String = - this.formatLocal("EEE, dd MMM", ZoneOffset.systemDefault()) - +@Deprecated("Use the TimeConverter interface via DI") fun LocalDate.formatDateWeekDayLong(): String = this.formatLocal("EEEE, dd MMM", ZoneOffset.systemDefault()) +@Deprecated("Use the TimeConverter interface via DI") fun LocalDate.formatNicely( pattern: String = "EEE, dd MMM", patternNoWeekDay: String = "dd MMM", @@ -229,22 +231,6 @@ fun LocalDateTime.convertLocalToUTC(): LocalDateTime { return this.minusSeconds(offset) } -// The timepicker returns time in UTC, but the date picker returns date in LocalTimeZone -// hence use this method to get both date & time in UTC -fun getTrueDate(date: LocalDate, time: LocalTime, convert: Boolean = true): LocalDateTime { - val timeLocal = if (convert) time.convertUTCToLocal() else time - - return timeNowUTC() - .withYear(date.year) - .withMonth(date.monthValue) - .withDayOfMonth(date.dayOfMonth) - .withHour(timeLocal.hour) - .withMinute(timeLocal.minute) - .withSecond(0) - .withNano(0) - .convertLocalToUTC() -} - fun LocalDate.formatLocal( pattern: String = "dd MMM yyyy", zone: ZoneId = ZoneOffset.systemDefault() diff --git a/temp/old-design/src/main/java/com/ivy/design/utils/Keyboard.kt b/temp/old-design/src/main/java/com/ivy/design/utils/Keyboard.kt index 89e47f0ce6..bf8104d3a7 100644 --- a/temp/old-design/src/main/java/com/ivy/design/utils/Keyboard.kt +++ b/temp/old-design/src/main/java/com/ivy/design/utils/Keyboard.kt @@ -23,7 +23,6 @@ fun hideKeyboard() { LocalView.current.hideKeyboard() } -@Deprecated("Old design system. Use `:ivy-design` and Material3") fun View.hideKeyboard() { val imm: InputMethodManager = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager