diff --git a/sdk/src/qa/java/br/com/tick/sdk/repositories/FakeUserRepository.kt b/sdk/src/qa/java/br/com/tick/sdk/repositories/FakeUserRepository.kt index 7bd4630..fe76b24 100644 --- a/sdk/src/qa/java/br/com/tick/sdk/repositories/FakeUserRepository.kt +++ b/sdk/src/qa/java/br/com/tick/sdk/repositories/FakeUserRepository.kt @@ -17,6 +17,7 @@ class FakeUserRepository: UserRepository { } override fun getUser() = user + override suspend fun setInitialUser() { user.tryEmit(User.initial()) } diff --git a/ui/src/main/java/br/com/tick/ui/core/TeiraNavigationDrawer.kt b/ui/src/main/java/br/com/tick/ui/core/TeiraNavigationDrawer.kt index 1f9d907..3549735 100644 --- a/ui/src/main/java/br/com/tick/ui/core/TeiraNavigationDrawer.kt +++ b/ui/src/main/java/br/com/tick/ui/core/TeiraNavigationDrawer.kt @@ -1,7 +1,13 @@ package br.com.tick.ui.core +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.DismissibleDrawerSheet import androidx.compose.material3.DismissibleNavigationDrawer import androidx.compose.material3.DrawerState @@ -13,13 +19,16 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavBackStackEntry import br.com.tick.ui.NavigationItem import br.com.tick.ui.R import br.com.tick.ui.theme.spacing +import br.com.tick.ui.theme.textStyle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -31,7 +40,6 @@ fun TeiraNavigationDrawer( navigateToRoute: (NavigationItem) -> Unit, content: @Composable () -> Unit ) { - val coroutineScope = rememberCoroutineScope() val currentRoute = navBackStackEntry?.destination?.route @@ -42,60 +50,73 @@ fun TeiraNavigationDrawer( drawerContainerColor = MaterialTheme.colorScheme.surface, drawerContentColor = MaterialTheme.colorScheme.onSurface ) { - Spacer(modifier = Modifier.height(MaterialTheme.spacing.large)) - TeiraNavigationDrawerItem( - drawerState = drawerState, - painter = painterResource(id = NavigationItem.Settings.iconResource), - text = stringResource(id = NavigationItem.Settings.titleResource), - coroutineScope = coroutineScope, - isCurrentRoute = currentRoute == NavigationItem.Settings.route - ) { - navigateToRoute(NavigationItem.Settings) - } - TeiraNavigationDrawerItem( - drawerState = drawerState, - painter = painterResource(id = NavigationItem.Wallet.iconResource), - text = stringResource(id = NavigationItem.Wallet.titleResource), - coroutineScope = coroutineScope, - isCurrentRoute = currentRoute == NavigationItem.Wallet.route + Column( + modifier = Modifier.fillMaxSize().padding(MaterialTheme.spacing.large), + verticalArrangement = Arrangement.SpaceBetween ) { - navigateToRoute(NavigationItem.Wallet) + Column { + Text( + modifier = Modifier.padding(MaterialTheme.spacing.large), + text = stringResource(id = R.string.app_name), + style = MaterialTheme.textStyle.h1extra, + color = MaterialTheme.colorScheme.tertiary + ) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.large)) + TeiraNavigationDrawerItem( + drawerState = drawerState, + painter = painterResource(id = NavigationItem.Settings.iconResource), + text = stringResource(id = NavigationItem.Settings.titleResource), + isCurrentRoute = currentRoute == NavigationItem.Settings.route + ) { + navigateToRoute(NavigationItem.Settings) + } + TeiraNavigationDrawerItem( + drawerState = drawerState, + painter = painterResource(id = NavigationItem.Wallet.iconResource), + text = stringResource(id = NavigationItem.Wallet.titleResource), + isCurrentRoute = currentRoute == NavigationItem.Wallet.route + ) { + navigateToRoute(NavigationItem.Wallet) + } + TeiraNavigationDrawerItem( + drawerState = drawerState, + painter = painterResource(id = NavigationItem.Analysis.iconResource), + text = stringResource(id = NavigationItem.Analysis.titleResource), + isCurrentRoute = currentRoute == NavigationItem.Analysis.route + ) { + navigateToRoute(NavigationItem.Analysis) + } + Spacer( + modifier = Modifier + .padding(horizontal = MaterialTheme.spacing.medium, vertical = MaterialTheme.spacing.small) + .fillMaxWidth() + .height(0.5.dp) + .background(MaterialTheme.colorScheme.tertiary) + ) + TeiraNavigationDrawerItem( + drawerState = drawerState, + painter = painterResource(id = NavigationItem.History.iconResource), + text = stringResource(id = NavigationItem.History.titleResource), + isCurrentRoute = currentRoute == NavigationItem.History.route + ) { + navigateToRoute(NavigationItem.History) + } + TeiraNavigationDrawerItem( + drawerState = drawerState, + painter = painterResource(id = NavigationItem.Expense.iconResource), + text = stringResource(id = NavigationItem.Expense.titleResource), + isCurrentRoute = currentRoute == NavigationItem.Expense.route + ) { + navigateToParentRoute(NavigationItem.Expense) + } + } + TeiraNavigationDrawerItem( + drawerState = drawerState, + painter = painterResource(id = R.drawable.ic_clear), + text = stringResource(id = R.string.generic_close), + isCurrentRoute = false + ) } - TeiraNavigationDrawerItem( - drawerState = drawerState, - painter = painterResource(id = NavigationItem.Analysis.iconResource), - text = stringResource(id = NavigationItem.Analysis.titleResource), - coroutineScope = coroutineScope, - isCurrentRoute = currentRoute == NavigationItem.Analysis.route - ) { - navigateToRoute(NavigationItem.Analysis) - } - TeiraNavigationDrawerItem( - drawerState = drawerState, - painter = painterResource(id = NavigationItem.History.iconResource), - text = stringResource(id = NavigationItem.History.titleResource), - coroutineScope = coroutineScope, - isCurrentRoute = currentRoute == NavigationItem.History.route - ) { - navigateToRoute(NavigationItem.History) - } - TeiraNavigationDrawerItem( - drawerState = drawerState, - painter = painterResource(id = NavigationItem.Expense.iconResource), - text = stringResource(id = NavigationItem.Expense.titleResource), - coroutineScope = coroutineScope, - isCurrentRoute = currentRoute == NavigationItem.Expense.route - ) { - navigateToParentRoute(NavigationItem.Expense) - } - Spacer(modifier = Modifier.height(MaterialTheme.spacing.extraLarge)) - TeiraNavigationDrawerItem( - drawerState = drawerState, - painter = painterResource(id = R.drawable.ic_clear), - text = stringResource(id = R.string.generic_close), - coroutineScope = coroutineScope, - isCurrentRoute = false - ) } }, content = content @@ -107,10 +128,11 @@ private fun TeiraNavigationDrawerItem( drawerState: DrawerState, painter: Painter, text: String, - coroutineScope: CoroutineScope, isCurrentRoute: Boolean, onDrawerItemClick: (() -> Unit)? = null ) { + val coroutineScope = rememberCoroutineScope() + NavigationDrawerItem( icon = { Icon(painter = painter, contentDescription = null) diff --git a/ui/src/main/java/br/com/tick/ui/screens/analysis/AnalysisScreen.kt b/ui/src/main/java/br/com/tick/ui/screens/analysis/AnalysisScreen.kt index cc8b8c6..ccbb679 100644 --- a/ui/src/main/java/br/com/tick/ui/screens/analysis/AnalysisScreen.kt +++ b/ui/src/main/java/br/com/tick/ui/screens/analysis/AnalysisScreen.kt @@ -26,12 +26,12 @@ fun AnalysisScreen() { MostExpensiveCategory( modifier = Modifier .fillMaxWidth() - .padding(top = MaterialTheme.spacing.large) + .padding(top = MaterialTheme.spacing.small) ) FinancialHealthComposable( modifier = Modifier .fillMaxWidth() - .padding(top = MaterialTheme.spacing.large) + .padding(top = MaterialTheme.spacing.small) ) } } diff --git a/ui/src/main/java/br/com/tick/ui/screens/analysis/FinancialHealth.kt b/ui/src/main/java/br/com/tick/ui/screens/analysis/FinancialHealth.kt index 003f8ce..9dec2af 100644 --- a/ui/src/main/java/br/com/tick/ui/screens/analysis/FinancialHealth.kt +++ b/ui/src/main/java/br/com/tick/ui/screens/analysis/FinancialHealth.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel +import br.com.tick.sdk.domain.CurrencyFormat import br.com.tick.ui.R import br.com.tick.ui.core.TeiraNoAvailableDataState import br.com.tick.ui.screens.analysis.states.FinancialHealth diff --git a/ui/src/main/java/br/com/tick/ui/screens/analysis/MostExpensiveCategory.kt b/ui/src/main/java/br/com/tick/ui/screens/analysis/MostExpensiveCategory.kt index 456eac7..5ab9931 100644 --- a/ui/src/main/java/br/com/tick/ui/screens/analysis/MostExpensiveCategory.kt +++ b/ui/src/main/java/br/com/tick/ui/screens/analysis/MostExpensiveCategory.kt @@ -1,10 +1,14 @@ package br.com.tick.ui.screens.analysis +import androidx.compose.foundation.background 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Card @@ -23,8 +27,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import br.com.tick.sdk.domain.CurrencyFormat import br.com.tick.ui.R import br.com.tick.ui.core.TeiraNoAvailableDataState +import br.com.tick.ui.extensions.getLabelResource import br.com.tick.ui.screens.analysis.models.MostExpensiveCategory import br.com.tick.ui.screens.analysis.states.MostExpensiveCategoriesStates import br.com.tick.ui.screens.analysis.viewmodels.AnalysisScreenViewModel @@ -38,10 +44,11 @@ fun MostExpensiveCategory( ) { val mostExpensiveCategoriesStates by viewModel.mostExpenseCategoryList .collectAsState(initial = MostExpensiveCategoriesStates.NoDataAvailable) + val currency by viewModel.currency.collectAsState() Column(modifier = modifier.fillMaxWidth()) { when (val state = mostExpensiveCategoriesStates) { - is MostExpensiveCategoriesStates.Full -> MostExpensiveCategoryBody(modifier, state) + is MostExpensiveCategoriesStates.Full -> MostExpensiveCategoryBody(modifier, currency, state) MostExpensiveCategoriesStates.NoDataAvailable -> TeiraNoAvailableDataState(modifier) } } @@ -50,6 +57,7 @@ fun MostExpensiveCategory( @Composable private fun MostExpensiveCategoryBody( modifier: Modifier = Modifier, + currencyFormat: CurrencyFormat, mostExpensiveCategoriesState: MostExpensiveCategoriesStates.Full ) { Column(modifier = modifier.fillMaxWidth()) { @@ -59,17 +67,21 @@ private fun MostExpensiveCategoryBody( style = MaterialTheme.textStyle.h2 ) Row( - modifier = modifier.fillMaxWidth().padding(top = MaterialTheme.spacing.medium), + modifier = modifier + .fillMaxWidth() + .padding(top = MaterialTheme.spacing.medium), horizontalArrangement = Arrangement.SpaceEvenly ) { mostExpensiveCategoriesState.mostExpensiveCategories.forEach { mostExpensiveCategory -> val categoryCardColor = mostExpensiveCategory.color?.let { Color(it) - } ?: MaterialTheme.colorScheme.secondary + } ?: MaterialTheme.colorScheme.surface + + val currencyLabel = stringResource(id = currencyFormat.getLabelResource()) CategoryCard( label = mostExpensiveCategory.categoryName, - subLabel = mostExpensiveCategory.amount.toString(), + subLabel = "$currencyLabel${mostExpensiveCategory.amount}", color = categoryCardColor ) } @@ -83,7 +95,7 @@ fun CategoryCard(modifier: Modifier = Modifier, label: String, subLabel: String, modifier = modifier .size(80.dp) .padding(MaterialTheme.spacing.smallest), - colors = CardDefaults.cardColors(containerColor = color) + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) ) { Column( modifier = Modifier.fillMaxSize(), @@ -93,15 +105,24 @@ fun CategoryCard(modifier: Modifier = Modifier, label: String, subLabel: String, Text( text = label, style = MaterialTheme.textStyle.h2bold, - color = MaterialTheme.colorScheme.onSecondary, + color = MaterialTheme.colorScheme.onTertiary, maxLines = 1, overflow = TextOverflow.Ellipsis ) Text( text = subLabel, - style = MaterialTheme.textStyle.h3small, - color = MaterialTheme.colorScheme.onSecondary + style = MaterialTheme.textStyle.h3, + color = MaterialTheme.colorScheme.onTertiary ) + Box(modifier = Modifier.fillMaxSize()) { + Spacer( + modifier = Modifier + .height(6.dp) + .background(color) + .align(Alignment.BottomCenter) + .fillMaxWidth() + ) + } } } } @@ -110,6 +131,7 @@ fun CategoryCard(modifier: Modifier = Modifier, label: String, subLabel: String, @Composable fun MostExpensiveCategoryBodyPreview() { MostExpensiveCategoryBody( + currencyFormat = CurrencyFormat.EURO, mostExpensiveCategoriesState = MostExpensiveCategoriesStates.Full( listOf(MostExpensiveCategory("Test", Color.Red.toArgb(), 56.0)) ) diff --git a/ui/src/main/java/br/com/tick/ui/screens/analysis/states/MostExpensiveCategoriesStates.kt b/ui/src/main/java/br/com/tick/ui/screens/analysis/states/MostExpensiveCategoriesStates.kt index 9b7f670..852185e 100644 --- a/ui/src/main/java/br/com/tick/ui/screens/analysis/states/MostExpensiveCategoriesStates.kt +++ b/ui/src/main/java/br/com/tick/ui/screens/analysis/states/MostExpensiveCategoriesStates.kt @@ -13,7 +13,7 @@ sealed class MostExpensiveCategoriesStates { Full( mostExpensiveCategories .sortedByDescending { it.amount } - .take(5) + .take(4) ) } } diff --git a/ui/src/main/java/br/com/tick/ui/screens/analysis/viewmodels/AnalysisScreenViewModel.kt b/ui/src/main/java/br/com/tick/ui/screens/analysis/viewmodels/AnalysisScreenViewModel.kt index f6d7c2f..44a274d 100644 --- a/ui/src/main/java/br/com/tick/ui/screens/analysis/viewmodels/AnalysisScreenViewModel.kt +++ b/ui/src/main/java/br/com/tick/ui/screens/analysis/viewmodels/AnalysisScreenViewModel.kt @@ -1,7 +1,10 @@ package br.com.tick.ui.screens.analysis.viewmodels import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import br.com.tick.sdk.dispatchers.DispatcherProvider +import br.com.tick.sdk.domain.CurrencyFormat +import br.com.tick.sdk.repositories.user.UserRepository import br.com.tick.ui.screens.analysis.states.AnalysisGraphStates import br.com.tick.ui.screens.analysis.states.FinancialHealth import br.com.tick.ui.screens.analysis.states.MostExpensiveCategoriesStates @@ -10,8 +13,16 @@ import br.com.tick.ui.screens.analysis.usecases.FetchLastMonthExpenses import br.com.tick.ui.screens.analysis.usecases.GetMostExpensiveCategories import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -19,6 +30,7 @@ class AnalysisScreenViewModel @Inject constructor( private val fetchLastMonthExpenses: FetchLastMonthExpenses, private val getMostExpensiveCategories: GetMostExpensiveCategories, private val calculateFinancialHealthSituation: CalculateFinancialHealthSituation, + userRepository: UserRepository, private val dispatcherProvider: DispatcherProvider ) : ViewModel() { @@ -36,10 +48,23 @@ class AnalysisScreenViewModel @Inject constructor( } }.flowOn(dispatcherProvider.io()) - val financialHealthSituation: Flow - get() = flow { + val currency = userRepository.getUser() + .flowOn(dispatcherProvider.io()) + .map { it.currency } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = CurrencyFormat.EURO + ) + + private val _financialHealthSituation = MutableStateFlow(FinancialHealth.NoDataAvailable) + val financialHealthSituation: StateFlow = _financialHealthSituation + + init { + viewModelScope.launch(dispatcherProvider.io()) { calculateFinancialHealthSituation().collect { - emit(it) + _financialHealthSituation.emit(it) } - }.flowOn(dispatcherProvider.io()) -} \ No newline at end of file + } + } +} diff --git a/ui/src/main/java/br/com/tick/ui/theme/Color.kt b/ui/src/main/java/br/com/tick/ui/theme/Color.kt index 71f61b5..736af20 100644 --- a/ui/src/main/java/br/com/tick/ui/theme/Color.kt +++ b/ui/src/main/java/br/com/tick/ui/theme/Color.kt @@ -2,13 +2,13 @@ package br.com.tick.ui.theme import androidx.compose.ui.graphics.Color -val TeiraPrimaryColor = Color(0xFF6d7c8e) -val TeiraSecondaryColor = Color(0xFF6a6b68) -val TeiraTertiaryColor = Color(0xFFa45c3c) +val TeiraPrimaryColor = Color(0xFF6D788E) +val TeiraSecondaryColor = Color(0xFF989996) +val TeiraTertiaryColor = Color(0xFFCA5E2F) val TeiraOnPrimaryColor = Color(0xFFEFF1E9) -val TeiraOnSecondaryColor = Color(0xFFDCDDD9) -val TeiraOnTertiaryColor = Color(0xFF0c0c0c) +val TeiraOnSecondaryColor = Color(0xFFEFF1E9) +val TeiraOnTertiaryColor = Color(0xFF474444) -val TeiraSurfaceColor = Color(0xFFCFD1CA) +val TeiraSurfaceColor = Color(0xFFeeeeee) val TeiraSurfaceVariantColor = Color(0xFFA0AF7B) \ No newline at end of file diff --git a/ui/src/main/java/br/com/tick/ui/theme/Spacing.kt b/ui/src/main/java/br/com/tick/ui/theme/Spacing.kt index c827474..d8fdc03 100644 --- a/ui/src/main/java/br/com/tick/ui/theme/Spacing.kt +++ b/ui/src/main/java/br/com/tick/ui/theme/Spacing.kt @@ -37,6 +37,13 @@ data class TeiraTextStyle( lineHeight = 24.sp, letterSpacing = 3.sp ), + val h1extra: TextStyle = TextStyle( + fontFamily = Mulish, + fontWeight = FontWeight.Bold, + fontSize = 26.sp, + lineHeight = 24.sp, + letterSpacing = 3.sp + ), val h2: TextStyle = TextStyle( fontFamily = Mulish, fontWeight = FontWeight.Medium, diff --git a/ui/src/main/res/values/strings_analysis.xml b/ui/src/main/res/values/strings_analysis.xml index 6cfd3c3..428e752 100644 --- a/ui/src/main/res/values/strings_analysis.xml +++ b/ui/src/main/res/values/strings_analysis.xml @@ -5,7 +5,7 @@ Most expensive categories Financial Health - Your expenses is %1\$.2f percent of your income. %2\$s! + Your expenses is %1$.2f%% of your income. %2$s! You are safe! You need to be cautious. diff --git a/ui/src/test/java/br/com/tick/ui/viewmodels/AnalysisScreenViewModelTest.kt b/ui/src/test/java/br/com/tick/ui/viewmodels/AnalysisScreenViewModelTest.kt index 884670c..92eb82c 100644 --- a/ui/src/test/java/br/com/tick/ui/viewmodels/AnalysisScreenViewModelTest.kt +++ b/ui/src/test/java/br/com/tick/ui/viewmodels/AnalysisScreenViewModelTest.kt @@ -41,6 +41,7 @@ class AnalysisScreenViewModelTest { fetchLastMonthExpenses, getMostExpensiveCategories, calculateFinancialHealthSituation, + userRepository, FakeDispatcher() ) } @@ -49,20 +50,24 @@ class AnalysisScreenViewModelTest { fun `When user made an expense, financial health should reflect this change`() = runTest { val expenseRepository = FakeCategorizedExpenseRepository() val userRepository: UserRepository = FakeUserRepository() + + userRepository.setMonthlyIncome(1500.0) + expenseRepository.addExpense( + categoryId = 1, + name = "Something", + value = 15.0, + expenseDate = LocalDate.now(), + location = null, + photoUri = null + ) + val analysisScreenViewModel = getViewModel( categorizedExpenseRepository = expenseRepository, userRepository = userRepository ) - userRepository.setMonthlyIncome(1500.0) - expenseRepository.addExpense(0, "Name_1", 10.0, LocalDate.now()) - analysisScreenViewModel.financialHealthSituation.test { - val financialHealthState = awaitItem() - Truth.assertThat(financialHealthState).isInstanceOf(FinancialHealth.Situation::class.java) - Truth.assertThat( - (financialHealthState as FinancialHealth.Situation).percentageOfCompromisedIncome - ).isGreaterThan(0) + Truth.assertThat(awaitItem()).isInstanceOf(FinancialHealth.Situation::class.java) } } @@ -70,16 +75,14 @@ class AnalysisScreenViewModelTest { fun `When user has no expenses, financial health should have no available data`() = runTest { val expenseRepository = FakeCategorizedExpenseRepository() val userRepository: UserRepository = FakeUserRepository() + val analysisScreenViewModel = getViewModel( categorizedExpenseRepository = expenseRepository, userRepository = userRepository ) - userRepository.setMonthlyIncome(1500.0) - analysisScreenViewModel.financialHealthSituation.test { - val financialHealthState = awaitItem() - Truth.assertThat(financialHealthState).isInstanceOf(FinancialHealth.NoDataAvailable::class.java) + Truth.assertThat(awaitItem()).isInstanceOf(FinancialHealth.NoDataAvailable::class.java) } }