diff --git a/image/roborazzi/add_transaction_screen_date_picker.png b/image/roborazzi/add_transaction_screen_date_picker.png new file mode 100644 index 00000000..417d0baf Binary files /dev/null and b/image/roborazzi/add_transaction_screen_date_picker.png differ diff --git a/image/roborazzi/all_transactions_screen.png b/image/roborazzi/all_transactions_screen.png index 72571a35..8fcaef69 100644 Binary files a/image/roborazzi/all_transactions_screen.png and b/image/roborazzi/all_transactions_screen.png differ diff --git a/image/roborazzi/component_error_view.png b/image/roborazzi/component_error_view.png index 9c42c7fe..378551db 100644 Binary files a/image/roborazzi/component_error_view.png and b/image/roborazzi/component_error_view.png differ diff --git a/image/roborazzi/component_transaction_card.png b/image/roborazzi/component_transaction_card.png index 3b5e936c..324c5fca 100644 Binary files a/image/roborazzi/component_transaction_card.png and b/image/roborazzi/component_transaction_card.png differ diff --git a/image/roborazzi/home_screen.png b/image/roborazzi/home_screen.png index ae744e43..299addf2 100644 Binary files a/image/roborazzi/home_screen.png and b/image/roborazzi/home_screen.png differ diff --git a/image/roborazzi/money_flow_nav_host.png b/image/roborazzi/money_flow_nav_host.png index 902d998e..9b41e499 100644 Binary files a/image/roborazzi/money_flow_nav_host.png and b/image/roborazzi/money_flow_nav_host.png differ diff --git a/shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AddTransactionRoborazziTest.kt b/shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AddTransactionRoborazziTest.kt index 452cb3fd..5dbe5c3b 100644 --- a/shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AddTransactionRoborazziTest.kt +++ b/shared/src/androidUnitTest/kotlin/com/prof18/moneyflow/AddTransactionRoborazziTest.kt @@ -2,6 +2,8 @@ package com.prof18.moneyflow import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.prof18.moneyflow.database.model.TransactionType @@ -11,6 +13,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode +import kotlin.time.Clock @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @@ -35,11 +38,9 @@ class AddTransactionRoborazziTest : RoborazziTestBase() { updateDescriptionText = {}, selectedTransactionType = TransactionType.OUTCOME, updateTransactionType = {}, - updateYear = {}, - updateMonth = {}, - updateDay = {}, - saveDate = {}, + updateSelectedDate = {}, dateLabel = "11 July 2021", + selectedDateMillis = Clock.System.now().toEpochMilliseconds(), addTransactionAction = null, resetAction = {}, currencyConfig = RoborazziSampleData.sampleCurrencyConfig, @@ -49,4 +50,36 @@ class AddTransactionRoborazziTest : RoborazziTestBase() { capture("add_transaction_screen") } + + @Test + fun captureAddTransactionDatePicker() { + val dateLabel = "11 July 2021" + + composeRule.setContent { + MoneyFlowTheme { + AddTransactionScreen( + categoryState = remember { mutableStateOf(RoborazziSampleData.sampleCategory) }, + navigateUp = {}, + navigateToCategoryList = {}, + addTransaction = {}, + amountText = "10.00", + updateAmountText = {}, + descriptionText = "Pizza 🍕", + updateDescriptionText = {}, + selectedTransactionType = TransactionType.OUTCOME, + updateTransactionType = {}, + updateSelectedDate = {}, + dateLabel = dateLabel, + selectedDateMillis = Clock.System.now().toEpochMilliseconds(), + addTransactionAction = null, + resetAction = {}, + currencyConfig = RoborazziSampleData.sampleCurrencyConfig, + ) + } + } + + composeRule.onNodeWithText(dateLabel).performClick() + + capture("add_transaction_screen_date_picker") + } } diff --git a/shared/src/commonMain/kotlin/com/prof18/moneyflow/features/addtransaction/AddTransactionViewModel.kt b/shared/src/commonMain/kotlin/com/prof18/moneyflow/features/addtransaction/AddTransactionViewModel.kt index 8fe09fc0..a6d45dcb 100644 --- a/shared/src/commonMain/kotlin/com/prof18/moneyflow/features/addtransaction/AddTransactionViewModel.kt +++ b/shared/src/commonMain/kotlin/com/prof18/moneyflow/features/addtransaction/AddTransactionViewModel.kt @@ -19,10 +19,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.toLocalDateTime import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.amount_not_empty_error import kotlin.time.Clock @@ -32,26 +28,22 @@ internal class AddTransactionViewModel( private val errorMapper: MoneyFlowErrorMapper, ) : ViewModel() { + private val initialSelectedDateMillis: Long = Clock.System.now().toEpochMilliseconds() + private val _uiState = MutableStateFlow( AddTransactionUiState( selectedTransactionType = TransactionType.INCOME, amountText = "", descriptionText = null, - dateLabel = null, + dateLabel = initialSelectedDateMillis.formatDateDayMonthYear(), addTransactionAction = null, currencyConfig = null, + selectedDateMillis = initialSelectedDateMillis, ), ) val uiState: StateFlow = _uiState - // Private variables - private var selectedDateMillis: Long = Clock.System.now().toEpochMilliseconds() - private var yearNumber: Int = currentLocalDate().year - private var monthNumber: Int = currentLocalDate().month.ordinal - private var dayNumber: Int = currentLocalDate().day - init { - updateDateLabel() observeCurrencyConfig() } @@ -65,27 +57,12 @@ internal class AddTransactionViewModel( } } - fun setYearNumber(yearNumber: Int) { - this.yearNumber = yearNumber - } - - fun setMonthNumber(monthNumber: Int) { - this.monthNumber = monthNumber - 1 - } - - fun setDayNumber(dayNumber: Int) { - this.dayNumber = dayNumber - } - - fun saveDate() { - val localDate = LocalDate(yearNumber, monthNumber + 1, dayNumber) - selectedDateMillis = localDate.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() - updateDateLabel() - } - - private fun updateDateLabel() { + fun updateSelectedDate(selectedDateMillis: Long) { _uiState.update { state -> - state.copy(dateLabel = selectedDateMillis.formatDateDayMonthYear()) + state.copy( + dateLabel = selectedDateMillis.formatDateDayMonthYear(), + selectedDateMillis = selectedDateMillis, + ) } } @@ -106,7 +83,7 @@ internal class AddTransactionViewModel( val result = try { moneyRepository.insertTransaction( TransactionToSave( - dateMillis = selectedDateMillis, + dateMillis = uiState.value.selectedDateMillis, amountCents = amountCents, description = uiState.value.descriptionText, categoryId = categoryId, @@ -152,9 +129,6 @@ internal class AddTransactionViewModel( state.copy(selectedTransactionType = transactionType) } } - - private fun currentLocalDate(): LocalDate = - Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date } internal data class AddTransactionUiState( @@ -164,4 +138,5 @@ internal data class AddTransactionUiState( val dateLabel: String?, val addTransactionAction: AddTransactionAction?, val currencyConfig: CurrencyConfig?, + val selectedDateMillis: Long, ) diff --git a/shared/src/commonMain/kotlin/com/prof18/moneyflow/navigation/MoneyFlowNavHost.kt b/shared/src/commonMain/kotlin/com/prof18/moneyflow/navigation/MoneyFlowNavHost.kt index 0630f1f4..e01c610c 100644 --- a/shared/src/commonMain/kotlin/com/prof18/moneyflow/navigation/MoneyFlowNavHost.kt +++ b/shared/src/commonMain/kotlin/com/prof18/moneyflow/navigation/MoneyFlowNavHost.kt @@ -249,11 +249,9 @@ private fun EntryProviderScope.screens( updateDescriptionText = viewModel::updateDescriptionText, selectedTransactionType = uiState.selectedTransactionType, updateTransactionType = viewModel::updateTransactionType, - updateYear = viewModel::setYearNumber, - updateMonth = viewModel::setMonthNumber, - updateDay = viewModel::setDayNumber, - saveDate = viewModel::saveDate, + updateSelectedDate = viewModel::updateSelectedDate, dateLabel = uiState.dateLabel, + selectedDateMillis = uiState.selectedDateMillis, addTransactionAction = uiState.addTransactionAction, resetAction = viewModel::resetAction, currencyConfig = uiState.currencyConfig, diff --git a/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/AddTransactionScreen.kt b/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/AddTransactionScreen.kt index f1eac512..2405d485 100644 --- a/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/AddTransactionScreen.kt +++ b/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/AddTransactionScreen.kt @@ -3,12 +3,19 @@ package com.prof18.moneyflow.presentation.addtransaction import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDefaults +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -20,7 +27,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import com.prof18.moneyflow.database.model.TransactionType import com.prof18.moneyflow.domain.entities.CurrencyConfig -import com.prof18.moneyflow.presentation.addtransaction.components.DatePickerDialog import com.prof18.moneyflow.presentation.addtransaction.components.IconTextClickableRow import com.prof18.moneyflow.presentation.addtransaction.components.MFTextInput import com.prof18.moneyflow.presentation.addtransaction.components.TransactionTypeTabBar @@ -32,6 +38,8 @@ import com.prof18.moneyflow.ui.style.Margins import com.prof18.moneyflow.ui.style.MoneyFlowTheme import money_flow.shared.generated.resources.Res import money_flow.shared.generated.resources.add_transaction_screen +import money_flow.shared.generated.resources.cancel +import money_flow.shared.generated.resources.confirm import money_flow.shared.generated.resources.description import money_flow.shared.generated.resources.ic_calendar import money_flow.shared.generated.resources.ic_edit @@ -42,9 +50,11 @@ import money_flow.shared.generated.resources.select_category import money_flow.shared.generated.resources.today import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import kotlin.time.Clock @Composable @Suppress("LongMethod", "LongParameterList") // TODO: reduce method length +@OptIn(ExperimentalMaterial3Api::class) internal fun AddTransactionScreen( categoryState: State, navigateUp: () -> Unit, @@ -56,16 +66,19 @@ internal fun AddTransactionScreen( updateDescriptionText: (String?) -> Unit, selectedTransactionType: TransactionType, updateTransactionType: (TransactionType) -> Unit, - updateYear: (Int) -> Unit, - updateMonth: (Int) -> Unit, - updateDay: (Int) -> Unit, - saveDate: () -> Unit, + updateSelectedDate: (Long) -> Unit, dateLabel: String?, + selectedDateMillis: Long, addTransactionAction: AddTransactionAction?, resetAction: () -> Unit, currencyConfig: CurrencyConfig?, ) { val (showDatePickerDialog, setShowedDatePickerDialog) = remember { mutableStateOf(false) } + val datePickerState = rememberDatePickerState(initialSelectedDateMillis = selectedDateMillis) + + LaunchedEffect(selectedDateMillis) { + datePickerState.selectedDateMillis = selectedDateMillis + } val snackbarHostState = remember { SnackbarHostState() } addTransactionAction?.let { @@ -111,14 +124,37 @@ internal fun AddTransactionScreen( }, content = { innerPadding -> Column(modifier = Modifier.padding(innerPadding)) { - DatePickerDialog( - showDatePickerDialog, - setShowedDatePickerDialog, - onYearSelected = { updateYear(it) }, - onMonthSelected = { updateMonth(it) }, - onDaySelected = { updateDay(it) }, - onSave = { saveDate() }, - ) + if (showDatePickerDialog) { + DatePickerDialog( + onDismissRequest = { setShowedDatePickerDialog(false) }, + confirmButton = { + TextButton( + enabled = datePickerState.selectedDateMillis != null, + onClick = { + datePickerState.selectedDateMillis?.let { selectedDate -> + updateSelectedDate(selectedDate) + } + setShowedDatePickerDialog(false) + }, + ) { + Text(text = stringResource(Res.string.confirm)) + } + }, + dismissButton = { + TextButton(onClick = { setShowedDatePickerDialog(false) }) { + Text(text = stringResource(Res.string.cancel)) + } + }, + ) { + DatePicker( + state = datePickerState, + showModeToggle = false, + colors = DatePickerDefaults.colors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + } + } TransactionTypeTabBar( transactionType = selectedTransactionType, @@ -225,11 +261,9 @@ private fun AddTransactionScreenPreview() { updateDescriptionText = {}, selectedTransactionType = TransactionType.OUTCOME, updateTransactionType = {}, - updateYear = {}, - updateMonth = {}, - updateDay = {}, - saveDate = {}, + updateSelectedDate = {}, dateLabel = "11 July 2021", + selectedDateMillis = Clock.System.now().toEpochMilliseconds(), addTransactionAction = null, resetAction = {}, currencyConfig = CurrencyConfig( diff --git a/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/components/DatePicker.kt b/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/components/DatePicker.kt deleted file mode 100644 index 230f5197..00000000 --- a/shared/src/commonMain/kotlin/com/prof18/moneyflow/presentation/addtransaction/components/DatePicker.kt +++ /dev/null @@ -1,182 +0,0 @@ -@file:Suppress("MagicNumber") - -package com.prof18.moneyflow.presentation.addtransaction.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.prof18.moneyflow.ui.style.MoneyFlowTheme -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList -import money_flow.shared.generated.resources.Res -import money_flow.shared.generated.resources.cancel -import money_flow.shared.generated.resources.confirm -import money_flow.shared.generated.resources.select_date -import org.jetbrains.compose.resources.stringResource - -internal fun getYearList(): ImmutableList = (2015..2023).map { it.toString() }.toPersistentList() -internal fun getMonthList(): ImmutableList = (1..12).map { it.toString() }.toPersistentList() -internal fun getDayList(): ImmutableList = (1..31).map { it.toString() }.toPersistentList() - -// TODO: replace with a proper date picker -@Composable -internal fun DatePickerDialog( - showDialog: Boolean, - setDialogVisible: (Boolean) -> Unit, - onYearSelected: (Int) -> Unit, - onMonthSelected: (Int) -> Unit, - onDaySelected: (Int) -> Unit, - onSave: () -> Unit, -) { - if (showDialog) { - AlertDialog( - onDismissRequest = { - setDialogVisible(false) - }, - title = { - Text(text = stringResource(Res.string.select_date)) - }, - text = { - Row( - modifier = Modifier.fillMaxWidth(), - ) { - DatePickerItemDropdownMenu( - initialText = "Calendar.getInstance().get(Calendar.YEAR).toString()", - itemList = getYearList(), - onItemSelected = { - onYearSelected(it.toInt()) - }, - ) - - Spacer(modifier = Modifier.width(16.dp)) - - DatePickerItemDropdownMenu( - initialText = "(Calendar.getInstance().get(Calendar.MONTH) + 1).toString()", - itemList = getMonthList(), - onItemSelected = { - onMonthSelected(it.toInt()) - }, - ) - - Spacer(modifier = Modifier.width(16.dp)) - - DatePickerItemDropdownMenu( - initialText = "Calendar.getInstance().get(Calendar.DAY_OF_MONTH).toString()", - itemList = getDayList(), - onItemSelected = { - onDaySelected(it.toInt()) - }, - ) - } - }, - confirmButton = { - TextButton(onClick = { - onSave() - setDialogVisible(false) - }) { - Text(stringResource(Res.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = { - setDialogVisible(false) - }) { - Text(stringResource(Res.string.cancel)) - } - }, - ) - } -} - -@Composable -private fun DatePickerItemDropdownMenu( - initialText: String, - itemList: ImmutableList, - onItemSelected: (String) -> Unit, -) { - val (dropdownText, setDropdownText) = remember { mutableStateOf(initialText) } - var expanded by remember { mutableStateOf(false) } - - Box { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable(onClick = { - expanded = true - }), - ) { - Text( - text = dropdownText, - style = MaterialTheme.typography.bodyLarge, - ) - Spacer(Modifier.width(4.dp)) - Icon( - Icons.Rounded.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(16.dp), - ) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { - expanded = false - }, - ) { - itemList.forEach { - DropdownMenuItem( - text = { - Text( - text = it, - style = MaterialTheme.typography.bodyLarge, - ) - }, - onClick = { - setDropdownText(it) - expanded = false - onItemSelected(it) - }, - ) - } - } - } -} - -@Preview -@Composable -private fun DatePickerDialogPreview() { - MoneyFlowTheme { - Surface { - DatePickerDialog( - showDialog = true, - setDialogVisible = { }, - onYearSelected = { }, - onMonthSelected = { }, - onDaySelected = { }, - onSave = {}, - ) - } - } -}