diff --git a/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/CreateScheduledPaymentDto.kt b/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/CreateScheduledPaymentDto.kt index 157a13e6..8c2e5483 100644 --- a/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/CreateScheduledPaymentDto.kt +++ b/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/CreateScheduledPaymentDto.kt @@ -4,7 +4,6 @@ import com.spendoo.categories.domain.entity.scheduledPayment.CreateScheduledPaym import kotlinx.serialization.Serializable import com.spendoo.categories.domain.entity.scheduledPayment.PaymentFrequency import com.spendoo.categories.domain.entity.scheduledPayment.ReminderUnit -import com.spendoo.shared.domain.utils.toLocalDateTime import com.spendoo.shared.domain.utils.toUtcInstant @Serializable @@ -23,7 +22,7 @@ fun CreateScheduledPayment.toDto(): CreateScheduledPaymentDto { title = this.title, amount = this.amount, categoryId = this.categoryId, - startDate = this.startDate.toLocalDateTime().toUtcInstant().toString(), + startDate = this.startDate.toUtcInstant().toString(), frequency = if (this.frequency == PaymentFrequency.CUSTOM) this.customFrequencyDays ?: 0 else this.frequency.toDays(), reminderPeriod = this.reminderPeriod, reminderUnit = this.reminderUnit diff --git a/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/ScheduledPaymentDto.kt b/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/ScheduledPaymentDto.kt index d32cc260..8178e793 100644 --- a/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/ScheduledPaymentDto.kt +++ b/categoriesData/src/commonMain/kotlin/com/spendoo/categories/data/dataSource/remote/dto/scheduledPayment/ScheduledPaymentDto.kt @@ -33,7 +33,7 @@ fun ScheduledPaymentDto.toDomain(): ScheduledPayment { amount = this.amount, categoryId = this.categoryId, categoryIcon = CategoryIcon.fromStringOrDefault(this.categoryIcon), - startDate = this.startDate?.toLocalDateTimeOrDefault()?.date ?: today, + startDate = this.startDate?.toLocalDateTimeOrDefault() ?: now, nextDueDate = this.nextDueDate?.toLocalDateTimeOrDefault() ?: now, nextReminderDate = this.nextReminderDate?.toLocalDateTimeOrDefault()?.date ?: today, frequency = domainFrequency, diff --git a/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/CreateScheduledPayment.kt b/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/CreateScheduledPayment.kt index 9d57229f..2bab15b9 100644 --- a/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/CreateScheduledPayment.kt +++ b/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/CreateScheduledPayment.kt @@ -1,12 +1,13 @@ package com.spendoo.categories.domain.entity.scheduledPayment import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime data class CreateScheduledPayment( val title: String, val amount: Double, val categoryId: String, - val startDate: LocalDate, + val startDate: LocalDateTime, val frequency: PaymentFrequency, val customFrequencyDays: Int? = null, val reminderPeriod: Int, diff --git a/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/ScheduledPayment.kt b/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/ScheduledPayment.kt index aeadf6da..318359ce 100644 --- a/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/ScheduledPayment.kt +++ b/categoriesDomain/src/commonMain/kotlin/com/spendoo/categories/domain/entity/scheduledPayment/ScheduledPayment.kt @@ -10,7 +10,7 @@ data class ScheduledPayment( val amount: Double, val categoryId: String, val categoryIcon: CategoryIcon, - val startDate: LocalDate, + val startDate: LocalDateTime, val nextDueDate: LocalDateTime, val nextReminderDate: LocalDate, val frequency: PaymentFrequency, diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentBottomSheet.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentBottomSheet.kt index 0bac5c1e..0d775b54 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentBottomSheet.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentBottomSheet.kt @@ -29,6 +29,7 @@ import org.jetbrains.compose.resources.stringResource import spendoo.designsystem.generated.resources.enter_custom_days import com.spendoo.designsystem.components.button.AppButtonState import com.spendoo.designsystem.components.dialog.DatePicker +import com.spendoo.designsystem.components.dialog.TimePicker import com.spendoo.designsystem.components.dropdownMenu.DropdownMenu import com.spendoo.designsystem.components.dropdownMenu.DropdownMenuItem import com.spendoo.designsystem.components.icon.Icon @@ -55,6 +56,8 @@ import spendoo.designsystem.generated.resources.enter_title import spendoo.designsystem.generated.resources.frequency import spendoo.designsystem.generated.resources.ic_arrow_down import spendoo.designsystem.generated.resources.ic_date +import spendoo.designsystem.generated.resources.ic_clock_red +import spendoo.designsystem.generated.resources.time_label import spendoo.designsystem.generated.resources.reminder_period import spendoo.designsystem.generated.resources.save_payment @@ -170,33 +173,64 @@ private fun AddScheduledPaymentContent( trailingIconColor = Theme.colorScheme.text.link ) - CustomTextField( - value = state.startDate.format(), - onValueChange = { }, - hint = Res.string.enter_start_date.asString(), - enabled = false, + Row( modifier = Modifier .fillMaxWidth() - .clickableNoRipple { + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CustomTextField( + value = state.startDate.date.format(), + onValueChange = { }, + hint = Res.string.enter_start_date.asString(), + enabled = false, + modifier = Modifier + .weight(1f) + .clickableNoRipple { + focusManager.clearFocus() + interactionListener.onShowDatePicker(true) + }, + onTrailingIconClick = { focusManager.clearFocus() interactionListener.onShowDatePicker(true) - } - .padding(bottom = 16.dp), - onTrailingIconClick = { - focusManager.clearFocus() - interactionListener.onShowDatePicker(true) - }, - trailingIcon = Res.drawable.ic_date.painter(), - trailingIconColor = Theme.colorScheme.text.label - ) + }, + trailingIcon = Res.drawable.ic_date.painter(), + trailingIconColor = Theme.colorScheme.text.label + ) + CustomTextField( + value = state.startDate.time.format(), + onValueChange = { }, + hint = stringResource(Res.string.time_label), + enabled = false, + modifier = Modifier + .weight(1f) + .clickableNoRipple { + focusManager.clearFocus() + interactionListener.onShowTimePicker(true) + }, + onTrailingIconClick = { + focusManager.clearFocus() + interactionListener.onShowTimePicker(true) + }, + trailingIcon = Res.drawable.ic_clock_red.painter(), + trailingIconColor = Theme.colorScheme.text.label + ) + } DatePicker( showDialog = state.showDatePicker, - selectedDate = state.startDate, + selectedDate = state.startDate.date, onDateSelected = interactionListener::onStartDateChanged, onDismiss = { interactionListener.onShowDatePicker(false) } ) + TimePicker( + showDialog = state.showTimePicker, + selectedTime = state.startDate.time, + onTimeSelected = interactionListener::onStartTimeChanged, + onDismiss = { interactionListener.onShowTimePicker(false) } + ) + Text( text = Res.string.frequency.asString(), style = Theme.typography.label.medium.small, diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentInteractionListener.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentInteractionListener.kt index e888b55a..b008fe73 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentInteractionListener.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentInteractionListener.kt @@ -12,6 +12,8 @@ interface AddScheduledPaymentInteractionListener { fun onCategorySelected(category: CategoryItemUiState) fun onStartDateChanged(date: LocalDate) fun onShowDatePicker(show: Boolean) + fun onShowTimePicker(show: Boolean) + fun onStartTimeChanged(time: kotlinx.datetime.LocalTime) fun onFrequencyChanged(frequency: PaymentFrequency) fun onCustomFrequencyDaysChanged(days: String) fun onReminderPeriodValueChanged(value: String) diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentUiState.kt index eb3bfdbd..e14e8d96 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentUiState.kt @@ -3,7 +3,9 @@ package com.spendoo.categories.presentation.screen.addScheduledPaymentBottomShee import com.spendoo.shared.domain.entity.CategoryIcon import com.spendoo.categories.domain.entity.scheduledPayment.PaymentFrequency import com.spendoo.categories.domain.entity.scheduledPayment.ReminderUnit -import com.spendoo.shared.domain.utils.getToday +import com.spendoo.shared.domain.utils.getNow +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime import kotlinx.datetime.LocalDate import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.daily @@ -31,8 +33,9 @@ data class AddScheduledPaymentUiState( val categoryIcon: CategoryIcon = CategoryIcon.DEFAULT, val categoryName: String = "", val isCategorySelectionSheetVisible: Boolean = false, - val startDate: LocalDate = getToday(), + val startDate: LocalDateTime = getNow(), val showDatePicker: Boolean = false, + val showTimePicker: Boolean = false, val frequency: PaymentFrequency = PaymentFrequency.MONTHLY, val customFrequencyDays: String = "", val customFrequencyDaysError: StringResource? = null, diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentViewModel.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentViewModel.kt index 7a48046d..d36b5614 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentViewModel.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addScheduledPaymentBottomSheet/AddScheduledPaymentViewModel.kt @@ -10,6 +10,8 @@ import com.spendoo.designsystem.navigation.BaseViewModel import com.spendoo.designsystem.utils.UiText import com.spendoo.shared.domain.utils.toCleanDoubleOrNull import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.LocalDateTime import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.an_error_occurred import spendoo.designsystem.generated.resources.custom_frequency_must_be_greater_than_zero @@ -58,13 +60,21 @@ class AddScheduledPaymentViewModel( } override fun onStartDateChanged(date: LocalDate) { - updateState { copy(startDate = date, showDatePicker = false) } + updateState { copy(startDate = LocalDateTime(date, startDate.time), showDatePicker = false) } } override fun onShowDatePicker(show: Boolean) { updateState { copy(showDatePicker = show) } } + override fun onShowTimePicker(show: Boolean) { + updateState { copy(showTimePicker = show) } + } + + override fun onStartTimeChanged(time: LocalTime) { + updateState { copy(startDate = LocalDateTime(startDate.date, time), showTimePicker = false) } + } + override fun onFrequencyChanged(frequency: PaymentFrequency) { updateState { val customDays = diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionBottomSheet.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionBottomSheet.kt index 1073e822..3f92a4e1 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionBottomSheet.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionBottomSheet.kt @@ -5,8 +5,10 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -23,6 +25,7 @@ import com.spendoo.categories.presentation.screen.addTransactionBottomSheet.comp import com.spendoo.categories.presentation.screen.addTransactionBottomSheet.components.IncomeEntriesSection import com.spendoo.categories.presentation.screen.categorySelectionSheet.CategorySelectionSheet import com.spendoo.designsystem.components.dialog.DatePicker +import com.spendoo.designsystem.components.dialog.TimePicker import com.spendoo.designsystem.components.general.AppSegmentedControl import com.spendoo.designsystem.components.sheet.BottomSheet import com.spendoo.designsystem.components.sheet.BottomSheetTemplate @@ -36,6 +39,7 @@ import com.spendoo.designsystem.utils.extentions.painter import com.spendoo.shared.domain.entity.CategoryIcon import io.github.vinceglb.filekit.PlatformFile import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import spendoo.designsystem.generated.resources.Res @@ -43,7 +47,9 @@ import spendoo.designsystem.generated.resources.add import spendoo.designsystem.generated.resources.add_transaction import spendoo.designsystem.generated.resources.cancel import spendoo.designsystem.generated.resources.enter_your_date +import spendoo.designsystem.generated.resources.ic_clock_red import spendoo.designsystem.generated.resources.ic_date +import spendoo.designsystem.generated.resources.time_label import spendoo.designsystem.generated.resources.write_a_note_optional @Composable @@ -77,6 +83,13 @@ fun AddTransactionBottomSheet( onDateSelected = viewModel::onDateSelected, onDismiss = viewModel::onDatePickerDismissed ) + + TimePicker( + showDialog = state.showTimePicker, + selectedTime = state.time, + onTimeSelected = viewModel::onTimeSelected, + onDismiss = viewModel::onTimePickerDismissed + ) } @Composable @@ -124,26 +137,50 @@ private fun AddTransactionBottomSheetContent( enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { - CustomTextField( - value = state.date.format(), - onValueChange = { }, - hint = stringResource(Res.string.enter_your_date), - enabled = false, + Row( modifier = Modifier .fillMaxWidth() - .clickableNoRipple { - focusManager.clearFocus() - interactionListener.onDatePickerRequested() - } .padding(bottom = 8.dp) .padding(horizontal = 16.dp), - onTrailingIconClick = { - focusManager.clearFocus() - interactionListener.onDatePickerRequested() - }, - trailingIcon = Res.drawable.ic_date.painter(), - trailingIconColor = Theme.colorScheme.text.label - ) + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CustomTextField( + value = state.date.format(), + onValueChange = { }, + hint = stringResource(Res.string.enter_your_date), + enabled = false, + modifier = Modifier + .weight(1f) + .clickableNoRipple { + focusManager.clearFocus() + interactionListener.onDatePickerRequested() + }, + onTrailingIconClick = { + focusManager.clearFocus() + interactionListener.onDatePickerRequested() + }, + trailingIcon = Res.drawable.ic_date.painter(), + trailingIconColor = Theme.colorScheme.text.label + ) + CustomTextField( + value = state.time.format(), + onValueChange = { }, + hint = stringResource(Res.string.time_label), + enabled = false, + modifier = Modifier + .weight(1f) + .clickableNoRipple { + focusManager.clearFocus() + interactionListener.onTimePickerRequested() + }, + onTrailingIconClick = { + focusManager.clearFocus() + interactionListener.onTimePickerRequested() + }, + trailingIcon = Res.drawable.ic_clock_red.painter(), + trailingIconColor = Theme.colorScheme.text.label + ) + } } AnimatedVisibility( @@ -168,7 +205,7 @@ private fun AddTransactionBottomSheetContent( } item { - Spacer(modifier = Modifier.padding(bottom = 60.dp)) + Spacer(modifier = Modifier.padding(bottom = 16.dp)) } } @@ -199,6 +236,9 @@ val previewInteractionListener = object : AddTransactionInteractionListener { override fun onSavingAmountChanged(amount: String) {} override fun onDatePickerRequested() {} override fun onDatePickerDismissed() {} + override fun onTimeSelected(time: LocalTime) {} + override fun onTimePickerRequested() {} + override fun onTimePickerDismissed() {} override fun onNoteChanged(note: String) {} override fun onDateSelected(date: LocalDate) {} override fun submit() {} diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionInteractionListener.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionInteractionListener.kt index a85ba748..339bc4f8 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionInteractionListener.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionInteractionListener.kt @@ -14,6 +14,9 @@ interface AddTransactionInteractionListener { fun onDateSelected(date: LocalDate) fun onDatePickerRequested() fun onDatePickerDismissed() + fun onTimeSelected(time: kotlinx.datetime.LocalTime) + fun onTimePickerRequested() + fun onTimePickerDismissed() fun onSavingChecked(checked: Boolean) // Expense Entry Actions diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionUiState.kt index 72ce3e05..6d1093f5 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionUiState.kt @@ -3,8 +3,10 @@ package com.spendoo.categories.presentation.screen.addTransactionBottomSheet import com.spendoo.categories.domain.entity.category.Category import com.spendoo.shared.domain.entity.CategoryIcon import com.spendoo.shared.domain.utils.getToday +import com.spendoo.shared.domain.utils.getNow import com.spendoo.designsystem.utils.UiText import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime import org.jetbrains.compose.resources.StringResource import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.expenses @@ -51,9 +53,11 @@ data class AddTransactionUiState( val incomeAmount: Double? = null, val incomeNote: String = "", val date: LocalDate = getToday(), + val time: LocalTime = getNow().time, val isSavingChecked: Boolean = false, val savingAmount: Double? = null, val showDatePicker: Boolean = false, + val showTimePicker: Boolean = false, val isSubmitting: Boolean = false, val isProcessingMedia: Boolean = false, val showCategorySheet: Boolean = false, diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionViewModel.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionViewModel.kt index ec2a9948..5e31a372 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionViewModel.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/addTransactionBottomSheet/AddTransactionViewModel.kt @@ -8,6 +8,8 @@ import com.spendoo.categories.domain.entity.transaction.IncomeEntry import com.spendoo.categories.domain.entity.transaction.ReadyTransactionEntry import com.spendoo.categories.domain.repository.TransactionsRepository import com.spendoo.shared.domain.utils.toLocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.LocalDateTime import com.spendoo.designsystem.navigation.BaseViewModel import com.spendoo.designsystem.utils.UiText import com.spendoo.designsystem.utils.toUiText @@ -100,6 +102,18 @@ class AddTransactionViewModel( updateState { it.copy(showDatePicker = false) } } + override fun onTimeSelected(time: LocalTime) { + updateState { it.copy(time = time, showTimePicker = false) } + } + + override fun onTimePickerRequested() { + updateState { it.copy(showTimePicker = true) } + } + + override fun onTimePickerDismissed() { + updateState { it.copy(showTimePicker = false) } + } + override fun onSavingChecked(checked: Boolean) { updateState { it.copy(isSavingChecked = checked, savingAmountError = null) } } @@ -375,7 +389,7 @@ class AddTransactionViewModel( IncomeEntry( title = state.value.incomeTitle, amount = amount, - transactionDate = state.value.date.toLocalDateTime(), + transactionDate = LocalDateTime(state.value.date, state.value.time), note = state.value.incomeNote.ifBlank { null } ) ) @@ -418,7 +432,7 @@ class AddTransactionViewModel( title = it.title, amount = it.amount ?: 0.0, categoryId = it.categoryId ?: "", - transactionDate = state.value.date.toLocalDateTime(), + transactionDate = LocalDateTime(state.value.date, state.value.time), note = it.note.ifBlank { null } ) } diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionBottomSheet.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionBottomSheet.kt index db3237db..f7d68f3f 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionBottomSheet.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionBottomSheet.kt @@ -2,6 +2,7 @@ package com.spendoo.categories.presentation.screen.editTransactionBottomSheet import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -20,6 +21,7 @@ import com.spendoo.designsystem.components.button.AppButton import com.spendoo.designsystem.components.button.AppButtonState import com.spendoo.designsystem.components.button.AppButtonType import com.spendoo.designsystem.components.dialog.DatePicker +import com.spendoo.designsystem.components.dialog.TimePicker import com.spendoo.designsystem.components.icon.Icon import com.spendoo.designsystem.components.sheet.BottomSheet import com.spendoo.designsystem.components.textField.CustomTextField @@ -38,9 +40,11 @@ import spendoo.designsystem.generated.resources.amount import spendoo.designsystem.generated.resources.choose_category import spendoo.designsystem.generated.resources.enter_your_date import spendoo.designsystem.generated.resources.ic_arrow_down +import spendoo.designsystem.generated.resources.ic_clock_red import spendoo.designsystem.generated.resources.ic_date import spendoo.designsystem.generated.resources.note_optional import spendoo.designsystem.generated.resources.save +import spendoo.designsystem.generated.resources.time_label import spendoo.designsystem.generated.resources.title @Composable @@ -112,24 +116,47 @@ fun EditTransactionBottomSheet( ) } - CustomTextField( - value = state.date.format(), - onValueChange = { }, - hint = stringResource(Res.string.enter_your_date), - enabled = false, - modifier = Modifier - .fillMaxWidth() - .clickableNoRipple { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CustomTextField( + value = state.date.format(), + onValueChange = { }, + hint = stringResource(Res.string.enter_your_date), + enabled = false, + modifier = Modifier + .weight(1f) + .clickableNoRipple { + focusManager.clearFocus() + viewModel.onDatePickerRequested() + }, + onTrailingIconClick = { focusManager.clearFocus() viewModel.onDatePickerRequested() }, - onTrailingIconClick = { - focusManager.clearFocus() - viewModel.onDatePickerRequested() - }, - trailingIcon = Res.drawable.ic_date.painter(), - trailingIconColor = Theme.colorScheme.text.label - ) + trailingIcon = Res.drawable.ic_date.painter(), + trailingIconColor = Theme.colorScheme.text.label + ) + CustomTextField( + value = state.time.format(), + onValueChange = { }, + hint = stringResource(Res.string.time_label), + enabled = false, + modifier = Modifier + .weight(1f) + .clickableNoRipple { + focusManager.clearFocus() + viewModel.onTimePickerRequested() + }, + onTrailingIconClick = { + focusManager.clearFocus() + viewModel.onTimePickerRequested() + }, + trailingIcon = Res.drawable.ic_clock_red.painter(), + trailingIconColor = Theme.colorScheme.text.label + ) + } CustomTextField( modifier = Modifier.fillMaxWidth(), value = state.note, @@ -160,4 +187,11 @@ fun EditTransactionBottomSheet( onDateSelected = viewModel::onDateSelected, selectedDate = state.date ) + + TimePicker( + showDialog = state.showTimePicker, + onDismiss = viewModel::onTimePickerDismissed, + onTimeSelected = viewModel::onTimeSelected, + selectedTime = state.time + ) } diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionInteractionListener.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionInteractionListener.kt index d2486923..ed3a68b9 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionInteractionListener.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionInteractionListener.kt @@ -11,6 +11,9 @@ interface EditTransactionInteractionListener { fun onDateSelected(date: LocalDate) fun onDatePickerRequested() fun onDatePickerDismissed() + fun onTimeSelected(time: kotlinx.datetime.LocalTime) + fun onTimePickerRequested() + fun onTimePickerDismissed() fun onCategoryFieldClicked() fun onCategorySelected(category: CategoryItemUiState) fun onCategorySheetDismissed() diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionUiState.kt index 2589beee..a530dd1a 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionUiState.kt @@ -4,7 +4,9 @@ import com.spendoo.categories.domain.entity.transaction.TransactionType import com.spendoo.designsystem.utils.UiText import com.spendoo.shared.domain.entity.CategoryIcon import com.spendoo.shared.domain.utils.getToday +import com.spendoo.shared.domain.utils.getNow import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime data class EditTransactionUiState( val transactionId: String = "", @@ -13,12 +15,14 @@ data class EditTransactionUiState( val amount: Double? = null, val note: String = "", val date: LocalDate = getToday(), + val time: LocalTime = getNow().time, val categoryId: String? = null, val categoryName: String = "", val categoryIcon: CategoryIcon? = null, val isSaving: Boolean = false, val isLoading: Boolean = true, val showDatePicker: Boolean = false, + val showTimePicker: Boolean = false, val showCategorySheet: Boolean = false, val titleError: UiText? = null, val amountError: UiText? = null, diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionViewModel.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionViewModel.kt index 7ff58968..6b00f413 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionViewModel.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/editTransactionBottomSheet/EditTransactionViewModel.kt @@ -10,6 +10,8 @@ import com.spendoo.shared.domain.utils.toCleanDoubleOrNull import com.spendoo.shared.domain.utils.toLocalDateTime import com.spendoo.shared.domain.utils.toUtcInstant import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime +import kotlinx.datetime.LocalDateTime import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.an_error_occurred import spendoo.designsystem.generated.resources.enter_a_title @@ -43,6 +45,7 @@ class EditTransactionViewModel( amount = transaction.amount.absoluteValue, note = transaction.note ?: "", date = transaction.date.date, + time = transaction.date.time, categoryId = transaction.category?.categoryId, categoryName = transaction.category?.categoryName ?: "", categoryIcon = transaction.category?.categoryIcon, @@ -85,6 +88,18 @@ class EditTransactionViewModel( updateState { it.copy(showDatePicker = false) } } + override fun onTimeSelected(time: LocalTime) { + updateState { it.copy(time = time, showTimePicker = false) } + } + + override fun onTimePickerRequested() { + updateState { it.copy(showTimePicker = true) } + } + + override fun onTimePickerDismissed() { + updateState { it.copy(showTimePicker = false) } + } + override fun onCategoryFieldClicked() { updateState { it.copy(showCategorySheet = true) } } @@ -152,7 +167,7 @@ class EditTransactionViewModel( val amountValue = state.value.amount ?: 0.0 val request = UpdateTransaction( title = state.value.title, - transactionDate = state.value.date.toLocalDateTime().toUtcInstant().toString(), + transactionDate = LocalDateTime(state.value.date, state.value.time).toUtcInstant().toString(), note = state.value.note.takeIf { it.isNotBlank() }, amount = amountValue, categoryId = state.value.categoryId diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionInteractionListener.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionInteractionListener.kt index e3543257..44954bef 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionInteractionListener.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionInteractionListener.kt @@ -2,4 +2,5 @@ package com.spendoo.categories.presentation.screen.financialActionScreen interface FinancialActionInteractionListener { fun onDismiss() + fun onExecute() } \ No newline at end of file diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt index 941b0c78..0b771b32 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionScreen.kt @@ -15,7 +15,7 @@ import org.koin.compose.viewmodel.koinViewModel import org.koin.core.parameter.parametersOf import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.cancel -import spendoo.designsystem.generated.resources.ic_cancel +import spendoo.designsystem.generated.resources.img_alert import spendoo.designsystem.generated.resources.ok @Composable @@ -50,13 +50,14 @@ fun FinancialActionBottomSheetContent( AppAlertContent( modifier = Modifier .fillMaxWidth(), - iconRes = Res.drawable.ic_cancel, + iconRes = Res.drawable.img_alert, title = state.tile, minWidth = Dp.Unspecified, shape = RectangleShape, + buttonState = state.buttonState, description = state.body, actionText = Res.string.ok.asString(), - onActionClick = {}, + onActionClick = interactionListener::onExecute, dismissText = Res.string.cancel.asString(), onDismissRequest = interactionListener::onDismiss ) diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt index dfd0e40d..b95bdfba 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionUiState.kt @@ -1,7 +1,10 @@ package com.spendoo.categories.presentation.screen.financialActionScreen +import com.spendoo.designsystem.components.button.AppButtonState + data class FinancialActionUiState( val tile: String = "", val body: String = "", - val payload: Map = emptyMap() + val payload: Map = emptyMap(), + val buttonState: AppButtonState = AppButtonState.Enabled ) \ No newline at end of file diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt index e459fd03..04c86819 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/financialActionScreen/FinancialActionViewModel.kt @@ -1,11 +1,18 @@ package com.spendoo.categories.presentation.screen.financialActionScreen +import com.spendoo.categories.domain.repository.BudgetActionRepository +import com.spendoo.designsystem.components.button.AppButtonState import com.spendoo.designsystem.navigation.BaseViewModel +import com.spendoo.designsystem.utils.UiText +import spendoo.designsystem.generated.resources.Res +import spendoo.designsystem.generated.resources.an_error_occurred +import spendoo.designsystem.generated.resources.unknown_error class FinancialActionViewModel( private val navTile: String, private val navBody: String, - private val navPayload: Map + private val navPayload: Map, + private val budgetActionRepository: BudgetActionRepository ) : BaseViewModel(FinancialActionUiState()), FinancialActionInteractionListener { @@ -22,4 +29,28 @@ class FinancialActionViewModel( override fun onDismiss() { popBackStack() } + + override fun onExecute() { + val actionId = navPayload["actionId"] ?: return + tryToCall( + onStart = { + updateState { copy(buttonState = AppButtonState.Loading) } + }, + block = { budgetActionRepository.executeProposedAction(actionId) }, + onSuccess = { + popBackStack() + }, + onError = { throwable -> + showSnackBar( + title = UiText.StringRes(Res.string.an_error_occurred), + message = throwable.message?.let { msg -> UiText.DynamicString(msg) } + ?: UiText.StringRes(Res.string.unknown_error), + isSuccess = false + ) + }, + onEnd = { + updateState { copy(buttonState = AppButtonState.Enabled) } + } + ) + } } diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPaymentDetails/ScheduledPaymentDetailsUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPaymentDetails/ScheduledPaymentDetailsUiState.kt index cb9a7dda..d25dd420 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPaymentDetails/ScheduledPaymentDetailsUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPaymentDetails/ScheduledPaymentDetailsUiState.kt @@ -5,8 +5,10 @@ import com.spendoo.categories.domain.entity.scheduledPayment.PaymentFrequency import com.spendoo.categories.domain.entity.scheduledPayment.ReminderUnit import com.spendoo.categories.presentation.screen.addScheduledPaymentBottomSheet.AddScheduledPaymentUiState import com.spendoo.shared.domain.utils.getToday +import com.spendoo.shared.domain.utils.getNow import com.spendoo.designsystem.utils.UiText import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime data class ScheduledPaymentDetailsUiState( val isLoading: Boolean = true, @@ -16,7 +18,7 @@ data class ScheduledPaymentDetailsUiState( val amount: String = "", val categoryId: String = "", val categoryIcon: CategoryIcon = CategoryIcon.DEFAULT, - val startDate: LocalDate = getToday(), + val startDate: LocalDateTime = getNow(), val frequency: PaymentFrequency = PaymentFrequency.DAILY, val customFrequencyDays: Int? = null, val reminderPeriod: Int = 0, diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsUiState.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsUiState.kt index a729c4f3..31afbeea 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsUiState.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsUiState.kt @@ -7,7 +7,9 @@ import com.spendoo.categories.presentation.screen.addScheduledPaymentBottomSheet import com.spendoo.categories.domain.entity.scheduledPayment.PaymentFrequency import com.spendoo.categories.domain.entity.scheduledPayment.ReminderUnit import com.spendoo.shared.domain.utils.getToday +import com.spendoo.shared.domain.utils.getNow import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime import com.spendoo.designsystem.utils.UiText data class ScheduledPaymentsUiState( @@ -32,7 +34,7 @@ data class ScheduledPaymentUiState( val leftOverOption: LeftOverOption = LeftOverOption.MOVE_TO_SAVINGS, val timeLeft: UiText = UiText.DynamicString(""), val isDueSoon: Boolean = false, - val startDate: LocalDate = getToday(), + val startDate: LocalDateTime = getNow(), val categoryId: String = "", val frequency: PaymentFrequency = PaymentFrequency.DAILY, val customFrequencyDays: String = "", diff --git a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsViewModel.kt b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsViewModel.kt index e9f884aa..731fc44d 100644 --- a/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsViewModel.kt +++ b/categoriesPresentation/src/commonMain/kotlin/com/spendoo/categories/presentation/screen/scheduledPayments/ScheduledPaymentsViewModel.kt @@ -10,10 +10,8 @@ import com.spendoo.designsystem.utils.UiText import com.spendoo.designsystem.utils.extentions.toTimeLeftText import com.spendoo.shared.domain.utils.getNow import kotlinx.coroutines.launch -import kotlin.time.Clock import kotlin.time.ExperimentalTime import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toInstant import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.an_error_occurred diff --git a/designSystem/src/commonMain/composeResources/drawable-hdpi/img_alert.png b/designSystem/src/commonMain/composeResources/drawable-hdpi/img_alert.png new file mode 100644 index 00000000..51e1628b Binary files /dev/null and b/designSystem/src/commonMain/composeResources/drawable-hdpi/img_alert.png differ diff --git a/designSystem/src/commonMain/composeResources/drawable-xhdpi/img_alert.png b/designSystem/src/commonMain/composeResources/drawable-xhdpi/img_alert.png new file mode 100644 index 00000000..356a6b48 Binary files /dev/null and b/designSystem/src/commonMain/composeResources/drawable-xhdpi/img_alert.png differ diff --git a/designSystem/src/commonMain/composeResources/drawable-xxhdpi/img_alert.png b/designSystem/src/commonMain/composeResources/drawable-xxhdpi/img_alert.png new file mode 100644 index 00000000..aa6fd17a Binary files /dev/null and b/designSystem/src/commonMain/composeResources/drawable-xxhdpi/img_alert.png differ diff --git a/designSystem/src/commonMain/composeResources/drawable-xxxhdpi/img_alert.png b/designSystem/src/commonMain/composeResources/drawable-xxxhdpi/img_alert.png new file mode 100644 index 00000000..0bf60aca Binary files /dev/null and b/designSystem/src/commonMain/composeResources/drawable-xxxhdpi/img_alert.png differ diff --git a/designSystem/src/commonMain/composeResources/drawable/ic_logout.xml b/designSystem/src/commonMain/composeResources/drawable/ic_logout.xml new file mode 100644 index 00000000..a70eeb21 --- /dev/null +++ b/designSystem/src/commonMain/composeResources/drawable/ic_logout.xml @@ -0,0 +1,9 @@ + + + diff --git a/designSystem/src/commonMain/composeResources/drawable/ic_win_cup.xml b/designSystem/src/commonMain/composeResources/drawable/ic_win_cup.xml new file mode 100644 index 00000000..d8bd7ead --- /dev/null +++ b/designSystem/src/commonMain/composeResources/drawable/ic_win_cup.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/designSystem/src/commonMain/composeResources/drawable/img_alert.png b/designSystem/src/commonMain/composeResources/drawable/img_alert.png new file mode 100644 index 00000000..466c10a3 Binary files /dev/null and b/designSystem/src/commonMain/composeResources/drawable/img_alert.png differ diff --git a/designSystem/src/commonMain/composeResources/values-ar/strings.xml b/designSystem/src/commonMain/composeResources/values-ar/strings.xml index d19066ce..5dc953bd 100644 --- a/designSystem/src/commonMain/composeResources/values-ar/strings.xml +++ b/designSystem/src/commonMain/composeResources/values-ar/strings.xml @@ -374,5 +374,8 @@ سلسلة تسجيل الدخول سلسلة المعاملات الفئات + اختر الوقت + تسجيل الخروج + مطلوب الدفع diff --git a/designSystem/src/commonMain/composeResources/values/strings.xml b/designSystem/src/commonMain/composeResources/values/strings.xml index f8bfcff8..8fa381ea 100644 --- a/designSystem/src/commonMain/composeResources/values/strings.xml +++ b/designSystem/src/commonMain/composeResources/values/strings.xml @@ -374,4 +374,7 @@ Login Streak Transaction Streak Categories + Select Time + Logout + Payment Required diff --git a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/AppAlert.kt b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/AppAlert.kt index 9b7b2c93..e9e71e18 100644 --- a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/AppAlert.kt +++ b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/AppAlert.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -28,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.spendoo.designsystem.components.appBar.SpendooIconButton import com.spendoo.designsystem.components.button.AppButton +import com.spendoo.designsystem.components.button.AppButtonState import com.spendoo.designsystem.components.button.AppButtonType import com.spendoo.designsystem.components.text.Text import com.spendoo.designsystem.theme.theme.Theme @@ -82,6 +84,7 @@ fun AppAlertContent( actionText: String, onActionClick: () -> Unit, dismissText: String, + buttonState: AppButtonState = AppButtonState.Enabled, minWidth: Dp = 328.dp, shape: Shape = RoundedCornerShape(20.dp), onDismissRequest: () -> Unit, @@ -91,6 +94,7 @@ fun AppAlertContent( .width(minWidth) .clip(shape) .background(Theme.colorScheme.background.tertiary) + .navigationBarsPadding() .padding(vertical = 16.dp) ) { LazyColumn( @@ -124,6 +128,7 @@ fun AppAlertContent( iconRes = iconRes, description = description, actionText = actionText, + buttonState = buttonState, onActionClick = onActionClick, dismissText = dismissText, onDismissRequest = onDismissRequest @@ -139,7 +144,8 @@ private fun LazyListScope.bodySection( actionText: String, onActionClick: () -> Unit, dismissText: String, - onDismissRequest: () -> Unit + onDismissRequest: () -> Unit, + buttonState: AppButtonState ) { item { Image( @@ -168,6 +174,7 @@ private fun LazyListScope.bodySection( AppButton( type = AppButtonType.Primary, onClick = onActionClick, + state = buttonState, text = actionText, modifier = Modifier.fillMaxWidth() ) diff --git a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/DatePicker.kt b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/DatePicker.kt index 93f66eb5..eac8cd44 100644 --- a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/DatePicker.kt +++ b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/DatePicker.kt @@ -13,6 +13,8 @@ import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_TYPE_NORMAL import androidx.compose.ui.tooling.preview.Preview import com.spendoo.designsystem.theme.theme.SpendooTheme import com.spendoo.designsystem.theme.theme.Theme @@ -34,7 +36,7 @@ fun DatePicker( dismissText: String = stringResource(Res.string.cancel), containerColor: Color = Theme.colorScheme.background.tertiary, contentColor: Color = Theme.colorScheme.text.body, - brandColor: Color = Theme.colorScheme.brand.primary, + brandColor: Color = Theme.colorScheme.icon.primary, errorColor: Color = Theme.colorScheme.additional.onError, ) { if (showDialog) { @@ -129,8 +131,8 @@ fun DatePicker( } @Composable -@Preview -fun DatePickerDialogPreview() = SpendooTheme { +@Preview(widthDp = 360, heightDp = 640, name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL) +fun DatePickerDialogPreview() = SpendooTheme(darkTheme = true) { DatePicker( showDialog = true, selectedDate = LocalDate(2026, 1, 7), diff --git a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/TimePicker.kt b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/TimePicker.kt new file mode 100644 index 00000000..4ebf9fc5 --- /dev/null +++ b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/dialog/TimePicker.kt @@ -0,0 +1,141 @@ +package com.spendoo.designsystem.components.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerDefaults +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_TYPE_NORMAL +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.spendoo.designsystem.theme.theme.SpendooTheme +import com.spendoo.designsystem.theme.theme.Theme +import kotlinx.datetime.LocalTime +import org.jetbrains.compose.resources.stringResource +import spendoo.designsystem.generated.resources.Res +import spendoo.designsystem.generated.resources.cancel +import spendoo.designsystem.generated.resources.ok +import spendoo.designsystem.generated.resources.select_time + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimePicker( + showDialog: Boolean, + selectedTime: LocalTime?, + onTimeSelected: (LocalTime) -> Unit, + onDismiss: () -> Unit, + confirmText: String = stringResource(Res.string.ok), + dismissText: String = stringResource(Res.string.cancel), + containerColor: Color = Theme.colorScheme.background.tertiary, + contentColor: Color = Theme.colorScheme.text.body, + brandColor: Color = Theme.colorScheme.icon.primary, +) { + if (showDialog) { + val initialTime = selectedTime ?: LocalTime(12, 0) + val timePickerState = rememberTimePickerState( + initialHour = initialTime.hour, + initialMinute = initialTime.minute, + is24Hour = false + ) + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + shape = RoundedCornerShape(20.dp), + color = containerColor, + modifier = Modifier + .width(IntrinsicSize.Min) + .padding(24.dp) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(Res.string.select_time), + style = Theme.typography.label.medium.medium, + color = Theme.colorScheme.text.title, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp) + ) + + TimePicker( + state = timePickerState, + colors = TimePickerDefaults.colors( + clockDialColor = Theme.colorScheme.background.secondary, + clockDialSelectedContentColor = Theme.colorScheme.text.title, + clockDialUnselectedContentColor = Theme.colorScheme.text.body, + selectorColor = brandColor, + periodSelectorBorderColor = Theme.colorScheme.border.secondary, + periodSelectorSelectedContainerColor = brandColor, + periodSelectorUnselectedContainerColor = Color.Transparent, + periodSelectorSelectedContentColor = Theme.colorScheme.text.title, + periodSelectorUnselectedContentColor = Theme.colorScheme.text.body, + timeSelectorSelectedContainerColor = brandColor.copy(alpha = 0.2f), + timeSelectorUnselectedContainerColor = Theme.colorScheme.background.secondary, + timeSelectorSelectedContentColor = brandColor, + timeSelectorUnselectedContentColor = Theme.colorScheme.text.body + ) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + TextButton( + colors = ButtonDefaults.textButtonColors(contentColor = contentColor), + onClick = onDismiss + ) { + Text(dismissText, style = Theme.typography.label.medium.small) + } + Spacer(modifier = Modifier.width(8.dp)) + TextButton( + colors = ButtonDefaults.textButtonColors(contentColor = contentColor), + onClick = { + onTimeSelected(LocalTime(timePickerState.hour, timePickerState.minute)) + } + ) { + Text(confirmText, style = Theme.typography.label.medium.small) + } + } + } + } + } + } +} + +@Composable +@Preview(widthDp = 360, heightDp = 640, name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL) +private fun TimePickerDialogPreview() = SpendooTheme(darkTheme = true) { + TimePicker( + showDialog = true, + selectedTime = LocalTime(12, 30), + onTimeSelected = {}, + onDismiss = {} + ) +} diff --git a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/sheet/BottomSheet.kt b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/sheet/BottomSheet.kt index 8d8e79de..1a176719 100644 --- a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/sheet/BottomSheet.kt +++ b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/components/sheet/BottomSheet.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -71,6 +72,7 @@ fun BottomSheet( onDismiss() } }, + contentWindowInsets = { WindowInsets(0.dp) }, sheetState = sheetState, containerColor = containerColor, scrimColor = scrimColor, diff --git a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/theme/theme/SpendooTheme.kt b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/theme/theme/SpendooTheme.kt index 6717a846..2a981a79 100644 --- a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/theme/theme/SpendooTheme.kt +++ b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/theme/theme/SpendooTheme.kt @@ -29,7 +29,7 @@ fun SpendooTheme( val typography = createThemeTypography() val layoutDirection = remember(language) { - if (language == AppLanguage.English.iso) LayoutDirection.Ltr else LayoutDirection.Rtl + if (language.lowercase().startsWith("en") || language == AppLanguage.English.iso) LayoutDirection.Ltr else LayoutDirection.Rtl } CompositionLocalProvider( diff --git a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/utils/extentions/format.kt b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/utils/extentions/format.kt index 09b0e0ff..b69fc9ea 100644 --- a/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/utils/extentions/format.kt +++ b/designSystem/src/commonMain/kotlin/com/spendoo/designsystem/utils/extentions/format.kt @@ -1,6 +1,7 @@ package com.spendoo.designsystem.utils.extentions import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalTime import kotlinx.datetime.number fun LocalDate?.format() = this?.let { @@ -9,3 +10,9 @@ fun LocalDate?.format() = this?.let { val year = it.year.toString().padStart(4, '0') "$day/$month/$year" }.orEmpty() + +fun LocalTime?.format() = this?.let { + val hour = it.hour.toString().padStart(2, '0') + val minute = it.minute.toString().padStart(2, '0') + "$hour:$minute" +}.orEmpty() diff --git a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileInteractionListener.kt b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileInteractionListener.kt index 6a977aa0..0d991541 100644 --- a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileInteractionListener.kt +++ b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileInteractionListener.kt @@ -20,4 +20,6 @@ interface ProfileInteractionListener { fun onDismissLanguageBottomSheet() fun onToggleTheme(isDark: Boolean) fun onReload() + fun onClickLogout() + fun onClickUser(userId: String, userName: String, imageUrl: String?) } diff --git a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileScreen.kt b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileScreen.kt index 2c1a38b8..175124f0 100644 --- a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileScreen.kt +++ b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileScreen.kt @@ -159,6 +159,8 @@ private fun ProfileScreenPreview() { override fun onDismissLanguageBottomSheet() {} override fun onToggleTheme(isDark: Boolean) {} override fun onReload() {} + override fun onClickLogout() {} + override fun onClickUser(userId: String, userName: String, imageUrl: String?) {} } ) } diff --git a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileViewModel.kt b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileViewModel.kt index 6ff856c3..ad63b396 100644 --- a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileViewModel.kt +++ b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/ProfileViewModel.kt @@ -9,6 +9,7 @@ import com.spendoo.home.api.NotificationsRoute import com.spendoo.identity.api.EditProfileRoute import com.spendoo.identity.api.FollowersRoute import com.spendoo.identity.api.FollowingRoute +import com.spendoo.identity.domain.repository.AuthenticationRepository import com.spendoo.identity.domain.repository.FollowCodeRepository import com.spendoo.identity.domain.repository.FollowRepository import com.spendoo.identity.domain.repository.ProfileRepository @@ -16,6 +17,7 @@ import com.spendoo.identity.domain.repository.SettingsRepository import com.spendoo.identity.domain.util.AppLanguage import com.spendoo.identity.domain.util.AppTheme import com.spendoo.shared.domain.utils.PageQuery +import com.spendoo.statistics.api.StatisticsRoute import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.an_error_occurred import spendoo.designsystem.generated.resources.copied @@ -26,6 +28,7 @@ class ProfileViewModel( private val followRepository: FollowRepository, private val followCodeRepository: FollowCodeRepository, private val settingsRepository: SettingsRepository, + private val authenticationRepository: AuthenticationRepository, ) : BaseViewModel(ProfileUiState()), ProfileInteractionListener { init { @@ -180,4 +183,16 @@ class ProfileViewModel( onError = { handleError(it) } ) } + + override fun onClickLogout() { + tryToCall( + block = { authenticationRepository.logout() }, + onSuccess = {}, + onError = { handleError(it) } + ) + } + + override fun onClickUser(userId: String, userName: String, imageUrl: String?) { + navigate(StatisticsRoute(userId = userId, userName = userName, userImageUrl = imageUrl)) + } } diff --git a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/FollowingSection.kt b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/FollowingSection.kt index d10842ed..420a7300 100644 --- a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/FollowingSection.kt +++ b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/FollowingSection.kt @@ -94,7 +94,7 @@ fun FollowingSection( } items(state.followings) { user -> - UserAvatarChip(user = user) + UserAvatarChip(user = user, modifier = Modifier.clickableNoRipple { listener.onClickUser(user.userId, user.fullName, user.imageUrl) }) } } } diff --git a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/ProfileSettingsList.kt b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/ProfileSettingsList.kt index 0a600faa..c2b33298 100644 --- a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/ProfileSettingsList.kt +++ b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/ProfileSettingsList.kt @@ -31,11 +31,13 @@ import spendoo.designsystem.generated.resources.change_language import spendoo.designsystem.generated.resources.change_theme import spendoo.designsystem.generated.resources.ic_bell import spendoo.designsystem.generated.resources.ic_categories +import spendoo.designsystem.generated.resources.ic_logout import spendoo.designsystem.generated.resources.ic_mobile import spendoo.designsystem.generated.resources.ic_profile_details import spendoo.designsystem.generated.resources.ic_repeat import spendoo.designsystem.generated.resources.ic_stats -import spendoo.designsystem.generated.resources.img_coin +import spendoo.designsystem.generated.resources.ic_win_cup +import spendoo.designsystem.generated.resources.logout import spendoo.designsystem.generated.resources.notifications import spendoo.designsystem.generated.resources.profile_details import spendoo.designsystem.generated.resources.scheduled_payments @@ -64,7 +66,7 @@ fun ProfileSettingsList( onClickArrow = { listener.onClickScheduledPayments() } ) ProfileSettingItem( - icon = Res.drawable.img_coin, + icon = Res.drawable.ic_win_cup, title = stringResource(Res.string.achievements), onClickArrow = { listener.onClickAchievements() } ) @@ -117,5 +119,11 @@ fun ProfileSettingsList( onCheckedChange = { listener.onToggleTheme(!isDark) } ) } + + ProfileSettingItem( + icon = Res.drawable.ic_logout, + title = stringResource(Res.string.logout), + onClickArrow = { listener.onClickLogout() } + ) } } diff --git a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/UserAvatarChip.kt b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/UserAvatarChip.kt index 43d6de3c..a33a2345 100644 --- a/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/UserAvatarChip.kt +++ b/identityPresentation/src/commonMain/kotlin/com/spendoo/identity/presentation/screen/profile/components/UserAvatarChip.kt @@ -26,10 +26,13 @@ import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.ic_profile_details @Composable -fun UserAvatarChip(user: UserSearch) { +fun UserAvatarChip( + user: UserSearch, + modifier: Modifier = Modifier +) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.width(70.dp) + modifier = modifier.width(70.dp) ) { if (!user.imageUrl.isNullOrBlank()) { AsyncImage( diff --git a/sharedData/src/commonMain/kotlin/com/spendoo/shared/data/shared/BaseGateway.kt b/sharedData/src/commonMain/kotlin/com/spendoo/shared/data/shared/BaseGateway.kt index 93a89528..1b3e79a6 100644 --- a/sharedData/src/commonMain/kotlin/com/spendoo/shared/data/shared/BaseGateway.kt +++ b/sharedData/src/commonMain/kotlin/com/spendoo/shared/data/shared/BaseGateway.kt @@ -5,6 +5,7 @@ import com.spendoo.shared.domain.exception.InternetException import com.spendoo.shared.domain.exception.InvalidCredentialsException import com.spendoo.shared.domain.exception.InvalidRequestException import com.spendoo.shared.domain.exception.NoNetworkException +import com.spendoo.shared.domain.exception.PaymentRequiredException import com.spendoo.shared.domain.exception.TooManyRequestsException import com.spendoo.shared.domain.exception.UnAuthorizedException import com.spendoo.shared.domain.exception.UnknownErrorException @@ -35,7 +36,7 @@ abstract class BaseGateway(val client: HttpClient): KoinComponent { when { status == HttpStatusCode.PaymentRequired -> { globalNavigationHandler.onPaymentRequiredError() - throw CancellationException(message = "Payment required.", cause = e) + throw CancellationException(message = "Payment required.", cause = PaymentRequiredException()) } status == HttpStatusCode.Unauthorized -> throw UnAuthorizedException() status == HttpStatusCode.NotFound -> throw InvalidCredentialsException() diff --git a/sharedDomain/src/commonMain/kotlin/com/spendoo/shared/domain/exception/NetworkException.kt b/sharedDomain/src/commonMain/kotlin/com/spendoo/shared/domain/exception/NetworkException.kt index 59843937..f692184e 100644 --- a/sharedDomain/src/commonMain/kotlin/com/spendoo/shared/domain/exception/NetworkException.kt +++ b/sharedDomain/src/commonMain/kotlin/com/spendoo/shared/domain/exception/NetworkException.kt @@ -21,3 +21,5 @@ open class InternetException(errorMessage: String = "") : AuthenticationExceptio } class UnknownErrorException(message: String) : AuthenticationException(message) + +class PaymentRequiredException : AuthenticationException("Payment required") \ No newline at end of file diff --git a/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt b/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt index 25d3f385..5ec3b1e8 100644 --- a/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt +++ b/spendooApp/src/commonMain/kotlin/com/spendoo/App.kt @@ -18,6 +18,7 @@ import com.spendoo.designsystem.navigation.effector.Effector import com.spendoo.designsystem.theme.theme.SpendooTheme import com.spendoo.identity.domain.repository.AuthenticationRepository import com.spendoo.identity.domain.repository.SettingsRepository +import com.spendoo.identity.domain.util.AppLanguage import com.spendoo.identity.domain.util.AppLocalizer import com.spendoo.identity.domain.util.AppTheme import com.spendoo.util.SetSystemBarsAppearance @@ -81,8 +82,14 @@ fun App( }) } + val languageCode = if (currentLanguage == AppLanguage.DEFAULT) { + appLocalizer.getDeviceLanguageIso() + } else { + currentLanguage.iso + } + SpendooTheme( - language = currentLanguage.iso, + language = languageCode, darkTheme = isDarkTheme, content = { SetSystemBarsAppearance(currentTheme) diff --git a/spendooApp/src/commonMain/kotlin/com/spendoo/appEntryPoint/EntryPoint.kt b/spendooApp/src/commonMain/kotlin/com/spendoo/appEntryPoint/EntryPoint.kt index bbd3ad45..03cd5e1a 100644 --- a/spendooApp/src/commonMain/kotlin/com/spendoo/appEntryPoint/EntryPoint.kt +++ b/spendooApp/src/commonMain/kotlin/com/spendoo/appEntryPoint/EntryPoint.kt @@ -86,8 +86,8 @@ fun EntryPoint( || currentRoute == LoginRoute || currentRoute == SignUpRoute || currentRoute == ForgetPasswordRoute - || currentRoute == VerifyEmailRoute - || currentRoute == CreateNewPasswordRoute + || currentRoute is VerifyEmailRoute + || currentRoute is CreateNewPasswordRoute if (targetRoute == HomeRoute) { if (isUnauthRoute) { diff --git a/statisticsData/src/commonMain/kotlin/com/spendoo/statistics/data/repository/StatisticsRepositoryImpl.kt b/statisticsData/src/commonMain/kotlin/com/spendoo/statistics/data/repository/StatisticsRepositoryImpl.kt index aa053751..8f568570 100644 --- a/statisticsData/src/commonMain/kotlin/com/spendoo/statistics/data/repository/StatisticsRepositoryImpl.kt +++ b/statisticsData/src/commonMain/kotlin/com/spendoo/statistics/data/repository/StatisticsRepositoryImpl.kt @@ -12,6 +12,7 @@ import com.spendoo.statistics.domain.entity.Granularity import com.spendoo.statistics.domain.entity.ReportDataType import com.spendoo.statistics.domain.repository.StatisticsRepository import io.ktor.client.HttpClient +import io.ktor.client.plugins.timeout import io.ktor.client.request.get import kotlinx.datetime.LocalDateTime @@ -58,6 +59,10 @@ class StatisticsRepositoryImpl( ): ByteArray { val response = tryToExecute { get("/api/v1/statistics/pdf") { + timeout { //TODO: Check the enough timeout for large PDFs + requestTimeoutMillis = Long.MAX_VALUE + socketTimeoutMillis = Long.MAX_VALUE + } url { parameters.append("start_date", startDate.toUtcInstant().toString()) parameters.append("end_date", endDate.toUtcInstant().toString()) diff --git a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/download/DownloadViewModel.kt b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/download/DownloadViewModel.kt index 5263d691..fc45faa5 100644 --- a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/download/DownloadViewModel.kt +++ b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/download/DownloadViewModel.kt @@ -4,6 +4,7 @@ import com.spendoo.designsystem.navigation.BaseViewModel import com.spendoo.designsystem.utils.UiText import com.spendoo.identity.domain.repository.SettingsRepository import com.spendoo.identity.domain.util.AppTheme +import com.spendoo.shared.domain.exception.PaymentRequiredException import com.spendoo.statistics.domain.entity.DataToInclude import com.spendoo.statistics.domain.entity.ExportChoices import com.spendoo.statistics.domain.entity.Granularity @@ -18,6 +19,7 @@ import com.spendoo.statistics.presentation.screen.statistics.PieChartUiState import com.spendoo.statistics.presentation.screen.statistics.getBarXLabels import com.spendoo.statistics.presentation.screen.statistics.getDashedRanges import com.spendoo.statistics.presentation.screen.statistics.getXAxisLabels +import kotlinx.coroutines.CancellationException import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -30,6 +32,7 @@ import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import spendoo.designsystem.generated.resources.Res import spendoo.designsystem.generated.resources.an_error_occurred +import spendoo.designsystem.generated.resources.payment_required import kotlin.time.Clock class DownloadViewModel( @@ -38,8 +41,11 @@ class DownloadViewModel( ) : BaseViewModel(DownloadUiState()), DownloadInteractionListener { private var isSystemDarkTheme: Boolean = false + private var isInitialized = false fun initialize(isSystemDarkTheme: Boolean) { + if (isInitialized) return + isInitialized = true this.isSystemDarkTheme = isSystemDarkTheme val choices = statisticsRepository.getExportChoices() updateState { copy(exportChoices = choices) } @@ -106,22 +112,34 @@ class DownloadViewModel( tryToCall( block = { - val targetUserId = choices.targetUserId - if (targetUserId != null) { - statisticsRepository.getUserStatisticsPdf( - targetUserId = targetUserId, - startDate = startDateTime, - endDate = endDateTime, - reportDataType = reportDataType, - theme = theme - ) - } else { - statisticsRepository.getStatisticsPdf( - startDate = startDateTime, - endDate = endDateTime, - reportDataType = reportDataType, - theme = theme - ) + try { + val targetUserId = choices.targetUserId + if (targetUserId != null) { + statisticsRepository.getUserStatisticsPdf( + targetUserId = targetUserId, + startDate = startDateTime, + endDate = endDateTime, + reportDataType = reportDataType, + theme = theme + ) + } else { + statisticsRepository.getStatisticsPdf( + startDate = startDateTime, + endDate = endDateTime, + reportDataType = reportDataType, + theme = theme + ) + } + } catch (e: CancellationException) { + if (e.cause is PaymentRequiredException) { + updateState { + copy( + isLoading = false, + errorMessage = UiText.StringRes(Res.string.payment_required) + ) + } + } + throw e } }, onSuccess = { pdfBytes -> @@ -174,11 +192,23 @@ class DownloadViewModel( tryToCall( block = { - val targetUserId = choices.targetUserId - if (targetUserId != null) { - statisticsRepository.getUserStatistics(targetUserId, granularity, startDateTime, endDateTime) - } else { - statisticsRepository.getStatistics(granularity, startDateTime, endDateTime) + try { + val targetUserId = choices.targetUserId + if (targetUserId != null) { + statisticsRepository.getUserStatistics(targetUserId, granularity, startDateTime, endDateTime) + } else { + statisticsRepository.getStatistics(granularity, startDateTime, endDateTime) + } + } catch (e: CancellationException) { + if (e.cause is PaymentRequiredException) { + updateState { + copy( + isLoading = false, + errorMessage = UiText.StringRes(Res.string.payment_required) + ) + } + } + throw e } }, onSuccess = { stats -> diff --git a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsInteractionListener.kt b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsInteractionListener.kt index 354e57c0..c496f421 100644 --- a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsInteractionListener.kt +++ b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsInteractionListener.kt @@ -22,4 +22,5 @@ interface StatisticsInteractionListener { fun onEditTransaction(transactionId: String) fun onDeleteTransaction(transactionId: String) fun onTransactionClicked(transactionId: String) + fun onClickUserProfile() } diff --git a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsScreen.kt b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsScreen.kt index 3b1104c7..34abebe0 100644 --- a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsScreen.kt +++ b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsScreen.kt @@ -42,6 +42,7 @@ import com.spendoo.designsystem.components.appBar.SpendooIconButton import com.spendoo.designsystem.components.appBar.TopBar import com.spendoo.designsystem.components.general.AppSegmentedControl import com.spendoo.designsystem.components.indicator.PullToRefresh +import com.spendoo.designsystem.modifier.clickableNoRipple import com.spendoo.designsystem.theme.theme.SpendooTheme import com.spendoo.designsystem.theme.theme.Theme import com.spendoo.designsystem.utils.UiText @@ -157,6 +158,7 @@ private fun StatisticsContent( modifier = Modifier .padding(end = 12.dp) .size(50.dp) + .clickableNoRipple(enabled = state.userImageUrl != null) { listener.onClickUserProfile() } .clip(CircleShape) .border(0.5.dp, Theme.colorScheme.button.secondary, CircleShape), contentScale = ContentScale.Crop @@ -293,7 +295,6 @@ private fun StatisticsContentTransactionsPreview() { override fun onClickBack() {} override fun onDownloadReportClicked() {} override fun onFollowUserClicked() {} - override fun onOpenScheduledPayments() {} override fun onTabSelected(tab: StatisticsTab) {} override fun onGranularitySelected(granularity: Granularity) {} @@ -309,6 +310,7 @@ private fun StatisticsContentTransactionsPreview() { override fun onEditTransaction(transactionId: String) {} override fun onDeleteTransaction(transactionId: String) {} override fun onTransactionClicked(transactionId: String) {} + override fun onClickUserProfile() {} } ) } @@ -452,6 +454,7 @@ private fun StatisticsContentChartsPreview() { override fun onEditTransaction(transactionId: String) {} override fun onDeleteTransaction(transactionId: String) {} override fun onTransactionClicked(transactionId: String) {} + override fun onClickUserProfile() {} } ) } diff --git a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsViewModel.kt b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsViewModel.kt index 5e573eec..bd31fb41 100644 --- a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsViewModel.kt +++ b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/StatisticsViewModel.kt @@ -11,13 +11,16 @@ import com.spendoo.designsystem.navigation.BaseViewModel import com.spendoo.designsystem.utils.UiText import com.spendoo.designsystem.utils.toUiText import com.spendoo.identity.api.FollowingRoute +import com.spendoo.identity.api.ProfileRoute import com.spendoo.identity.domain.repository.ProfileRepository +import com.spendoo.shared.domain.exception.PaymentRequiredException import com.spendoo.shared.domain.utils.PageQuery import com.spendoo.shared.domain.utils.getNow import com.spendoo.shared.domain.utils.getToday import com.spendoo.statistics.api.ExportRoute import com.spendoo.statistics.domain.entity.Granularity import com.spendoo.statistics.domain.repository.StatisticsRepository +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -230,15 +233,28 @@ class StatisticsViewModel( updateState { copy(isLoading = true) } tryToCall( block = { - if (targetUserId != null) { - statisticsRepository.getUserStatistics( - targetUserId, - granularity, - startDateTime, - endDateTime - ) - } else { - statisticsRepository.getStatistics(granularity, startDateTime, endDateTime) + try { + if (targetUserId != null) { + statisticsRepository.getUserStatistics( + targetUserId, + granularity, + startDateTime, + endDateTime + ) + } else { + statisticsRepository.getStatistics(granularity, startDateTime, endDateTime) + } + } catch (e: CancellationException) { + if (e.cause is PaymentRequiredException) { + updateState { + copy( + selectedGranularity = Granularity.WEEK, + isLoading = false + ) + } + loadStatistics(Granularity.WEEK) + } + throw e } }, onSuccess = { stats -> @@ -398,6 +414,10 @@ class StatisticsViewModel( navigate(TransactionDetailsRoute(transactionId)) } + override fun onClickUserProfile() { + navigate(ProfileRoute) + } + companion object { const val INITIAL_PAGE = 0 const val PAGE_SIZE = 20 diff --git a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/components/StatisticsChartsContent.kt b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/components/StatisticsChartsContent.kt index 75e1ea41..4199ec84 100644 --- a/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/components/StatisticsChartsContent.kt +++ b/statisticsPresentation/src/commonMain/kotlin/com/spendoo/statistics/presentation/screen/statistics/components/StatisticsChartsContent.kt @@ -472,6 +472,7 @@ private fun StatisticsChartsContentPreview() { override fun onEditTransaction(transactionId: String) {} override fun onDeleteTransaction(transactionId: String) {} override fun onTransactionClicked(transactionId: String) {} + override fun onClickUserProfile() {} } ) }