From b92f5cae8d8dd0eb040efbb590165c76ba23e238 Mon Sep 17 00:00:00 2001 From: Rodrigo M Date: Sat, 6 Jul 2024 22:16:12 -0500 Subject: [PATCH] Start budget detail screen --- .../bridge/adapter/AnalyzeFeatureAdapter.kt | 4 +- .../bridge/adapter/BudgetFeatureAdapter.kt | 9 +- .../bridge/adapter/HomeFeatureAdapter.kt | 4 +- .../bridge/adapter/RecurringFeatureAdapter.kt | 4 +- .../bridge/adapter/SettingsFeatureAdapter.kt | 4 +- .../adapter/TransactionFeatureAdapter.kt | 4 +- .../bridge/mapper/BudgetViewMapper.kt | 21 ++- .../composition/data/mapper/BudgetMapper.kt | 39 ++-- .../{LunchRepository.kt => AppRepository.kt} | 28 ++- .../data/repository/ScreenShootRepository.kt | 92 +++++++--- .../companion/composition/di/CompositionDI.kt | 6 +- .../companion/composition/di/FeaturesDI.kt | 22 ++- .../companion/composition/di/NetworkDI.kt | 4 +- .../composition/domain/model/BudgetModel.kt | 3 +- ...{ILunchRepository.kt => IAppRepository.kt} | 4 +- .../usecase/ExecuteStartupLogicUseCase.kt | 4 +- .../domain/usecase/IsUserAuthenticated.kt | 4 +- .../lunch/money/companion/core/cache/Cache.kt | 2 +- .../companion/core/cache/InMemoryCache.kt | 3 +- .../features/analyze/FilterBottomSheet.kt | 45 +---- .../features/budget/BudgetDetailScreen.kt | 90 --------- .../companion/features/budget/BudgetItem.kt | 120 ++---------- .../companion/features/budget/BudgetScreen.kt | 12 +- .../companion/features/budget/BudgetView.kt | 32 ++-- .../features/budget/BudgetViewModel.kt | 10 +- .../features/budget/DummyIBudgetUIModel.kt | 2 +- .../features/budget/IBudgetUIModel.kt | 2 +- .../features/budget/detail/BudgetDataItem.kt | 154 ++++++++++++++++ .../budget/detail/BudgetDetailScreen.kt | 172 ++++++++++++++++++ .../budget/detail/BudgetDetailUiState.kt | 9 + .../budget/detail/BudgetDetailViewModel.kt | 31 ++++ .../detail/DummyIBudgetDetailUIModel.kt | 28 +++ .../budget/detail/IBudgetDetailUIModel.kt | 18 ++ .../features/navigation/BottomNavigation.kt | 8 +- .../features/navigation/NavigationGraph.kt | 26 +++ .../features/transactions/ui/FilterState.kt | 5 + .../uikit/components/MonthSelector.kt | 71 ++++++++ .../domain/usecase/ExecuteStartupLogicTest.kt | 4 +- .../domain/usecase/IsUserAuthenticatedTest.kt | 4 +- 39 files changed, 739 insertions(+), 365 deletions(-) rename app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/{LunchRepository.kt => AppRepository.kt} (92%) rename app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/{ILunchRepository.kt => IAppRepository.kt} (95%) delete mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetDetailScreen.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDataItem.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailScreen.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailUiState.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailViewModel.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/DummyIBudgetDetailUIModel.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/IBudgetDetailUIModel.kt create mode 100644 app/src/main/java/com/rodrigolmti/lunch/money/companion/uikit/components/MonthSelector.kt diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/AnalyzeFeatureAdapter.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/AnalyzeFeatureAdapter.kt index fb01888..90a1f3a 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/AnalyzeFeatureAdapter.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/AnalyzeFeatureAdapter.kt @@ -1,6 +1,6 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.adapter -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.composition.domain.usecase.GetTransactionSumByCategoryUseCase import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome @@ -12,7 +12,7 @@ import java.util.Date internal class AnalyzeFeatureAdapter( val sumTransactionUseCase: GetTransactionSumByCategoryUseCase, - private val lunchRepository: ILunchRepository, + private val lunchRepository: IAppRepository, ) { suspend fun getSumGroupedTransactions( diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/BudgetFeatureAdapter.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/BudgetFeatureAdapter.kt index e5d58aa..3a8ceae 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/BudgetFeatureAdapter.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/BudgetFeatureAdapter.kt @@ -1,7 +1,7 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.adapter import com.rodrigolmti.lunch.money.companion.composition.bridge.mapper.toView -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.DEFAULT_CURRENCY import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome @@ -12,7 +12,7 @@ import java.util.Date private const val CATEGORY_FILTER_KEY = "uncategorized" -internal class BudgetFeatureAdapter(private val lunchRepository: ILunchRepository) { +internal class BudgetFeatureAdapter(private val lunchRepository: IAppRepository) { suspend fun getBudget( start: Date, @@ -26,4 +26,9 @@ internal class BudgetFeatureAdapter(private val lunchRepository: ILunchRepositor } } } + + fun getBudget(budgetId: Int): BudgetView? { + return lunchRepository.getBudgetById(budgetId) + ?.toView(lunchRepository.getPrimaryCurrency() ?: DEFAULT_CURRENCY) + } } \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/HomeFeatureAdapter.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/HomeFeatureAdapter.kt index 81d619f..16baafc 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/HomeFeatureAdapter.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/HomeFeatureAdapter.kt @@ -3,7 +3,7 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.adapter import com.rodrigolmti.lunch.money.companion.composition.bridge.mapper.toView import com.rodrigolmti.lunch.money.companion.composition.domain.model.AssetStatus import com.rodrigolmti.lunch.money.companion.composition.domain.model.TransactionModel -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.DEFAULT_CURRENCY import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome @@ -22,7 +22,7 @@ import kotlinx.collections.immutable.toImmutableList import java.util.Date internal class HomeFeatureAdapter( - private val lunchRepository: ILunchRepository, + private val lunchRepository: IAppRepository, ) { suspend fun getAssetOverview( diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/RecurringFeatureAdapter.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/RecurringFeatureAdapter.kt index 8e841b9..1715c64 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/RecurringFeatureAdapter.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/RecurringFeatureAdapter.kt @@ -1,13 +1,13 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.adapter import com.rodrigolmti.lunch.money.companion.composition.bridge.mapper.toView -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome import com.rodrigolmti.lunch.money.companion.core.map import com.rodrigolmti.lunch.money.companion.features.recurring.RecurringView -internal class RecurringFeatureAdapter(private val lunchRepository: ILunchRepository) { +internal class RecurringFeatureAdapter(private val lunchRepository: IAppRepository) { suspend fun getRecurring(): Outcome, LunchError> { return lunchRepository.getRecurring().map { response -> diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/SettingsFeatureAdapter.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/SettingsFeatureAdapter.kt index 5aec86f..4a43251 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/SettingsFeatureAdapter.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/SettingsFeatureAdapter.kt @@ -1,13 +1,13 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.adapter import com.rodrigolmti.lunch.money.companion.composition.bridge.mapper.toView -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.DEFAULT_CURRENCY import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome import com.rodrigolmti.lunch.money.companion.features.settings.model.SettingsView -internal class SettingsFeatureAdapter(private val lunchRepository: ILunchRepository) { +internal class SettingsFeatureAdapter(private val lunchRepository: IAppRepository) { fun getUserData(): Outcome { lunchRepository.getSessionUser()?.let { user -> diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/TransactionFeatureAdapter.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/TransactionFeatureAdapter.kt index 3c2ce38..1cfa4b5 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/TransactionFeatureAdapter.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/adapter/TransactionFeatureAdapter.kt @@ -2,7 +2,7 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.adapter import com.rodrigolmti.lunch.money.companion.composition.bridge.mapper.toDto import com.rodrigolmti.lunch.money.companion.composition.bridge.mapper.toView -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.DEFAULT_CURRENCY import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome @@ -15,7 +15,7 @@ import com.rodrigolmti.lunch.money.companion.features.transactions.ui.summary.Tr import java.util.Date internal class TransactionFeatureAdapter( - private val lunchRepository: ILunchRepository + private val lunchRepository: IAppRepository ) { suspend fun getTransactionSummary( diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/mapper/BudgetViewMapper.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/mapper/BudgetViewMapper.kt index e990ac2..dd3d913 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/mapper/BudgetViewMapper.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/bridge/mapper/BudgetViewMapper.kt @@ -1,19 +1,24 @@ package com.rodrigolmti.lunch.money.companion.composition.bridge.mapper import com.rodrigolmti.lunch.money.companion.composition.domain.model.BudgetModel +import com.rodrigolmti.lunch.money.companion.composition.domain.model.CategoryModel import com.rodrigolmti.lunch.money.companion.features.budget.BudgetItemView import com.rodrigolmti.lunch.money.companion.features.budget.BudgetView internal fun BudgetModel.toView(currency: String): BudgetView { return BudgetView( category = categoryName, - items = data.map { - BudgetItemView( - totalTransactions = it.numTransactions, - totalSpending = it.spendingToBase, - totalBudget = it.budgetAmount, - currency = it.budgetCurrency ?: currency, - ) - } + items = data.mapValues { + it.value.toView(currency) + }, + ) +} + +internal fun CategoryModel.toView(currency: String): BudgetItemView { + return BudgetItemView( + totalTransactions = numTransactions, + totalSpending = spendingToBase, + totalBudget = budgetToBase, + currency = currency, ) } \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/mapper/BudgetMapper.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/mapper/BudgetMapper.kt index ed2051f..6bef90f 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/mapper/BudgetMapper.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/mapper/BudgetMapper.kt @@ -6,8 +6,8 @@ import com.rodrigolmti.lunch.money.companion.composition.data.model.response.Cat import com.rodrigolmti.lunch.money.companion.composition.data.model.response.CategoryResponse import com.rodrigolmti.lunch.money.companion.composition.domain.model.BudgetModel import com.rodrigolmti.lunch.money.companion.composition.domain.model.BudgetRecurringModel -import com.rodrigolmti.lunch.money.companion.composition.domain.model.CategoryModel import com.rodrigolmti.lunch.money.companion.composition.domain.model.CategoryConfigModel +import com.rodrigolmti.lunch.money.companion.composition.domain.model.CategoryModel internal fun mapBudget(budgets: List): List { return budgets.map { @@ -15,20 +15,26 @@ internal fun mapBudget(budgets: List): List { } } -internal fun BudgetResponse.toModel() = BudgetModel( - categoryName = categoryName, - categoryId = categoryId, - categoryGroupName = categoryGroupName, - groupId = groupId, - isGroup = isGroup ?: false, - isIncome = isIncome, - excludeFromBudget = excludeFromBudget, - excludeFromTotals = excludeFromTotals, - data = data.map { it.value?.toModel(it.key) }.filterNotNull(), - config = config?.toModel(), - order = order, - recurring = recurring?.list?.map { it.toModel() } ?: emptyList(), -) +internal fun BudgetResponse.toModel(): BudgetModel { + val data: Map = data + .filter { it.value != null } + .mapValues { it.value!!.toModel() } + + return BudgetModel( + categoryName = categoryName, + categoryId = categoryId, + categoryGroupName = categoryGroupName, + groupId = groupId, + isGroup = isGroup ?: false, + isIncome = isIncome, + excludeFromBudget = excludeFromBudget, + excludeFromTotals = excludeFromTotals, + data = data, + config = config?.toModel(), + order = order, + recurring = recurring?.list?.map { it.toModel() } ?: emptyList(), + ) +} internal fun BudgetRecurringResponse.toModel() = BudgetRecurringModel( payee = payee, @@ -36,14 +42,13 @@ internal fun BudgetRecurringResponse.toModel() = BudgetRecurringModel( currency = currency, ) -internal fun CategoryResponse.toModel(date: String) = CategoryModel( +internal fun CategoryResponse.toModel() = CategoryModel( numTransactions = numTransactions ?: 0, spendingToBase = spendingToBase ?: 0.0f, budgetToBase = budgetToBase ?: 0.0f, budgetAmount = budgetAmount ?: 0.0f, budgetCurrency = budgetCurrency, isAutomated = isAutomated ?: false, - date = date, ) internal fun CategoryConfigResponse.toModel() = CategoryConfigModel( diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/LunchRepository.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/AppRepository.kt similarity index 92% rename from app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/LunchRepository.kt rename to app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/AppRepository.kt index ff4078f..74f9d44 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/LunchRepository.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/AppRepository.kt @@ -17,7 +17,7 @@ import com.rodrigolmti.lunch.money.companion.composition.domain.model.RecurringM import com.rodrigolmti.lunch.money.companion.composition.domain.model.TransactionCategoryModel import com.rodrigolmti.lunch.money.companion.composition.domain.model.TransactionModel import com.rodrigolmti.lunch.money.companion.composition.domain.model.UserModel -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.ConnectionChecker import com.rodrigolmti.lunch.money.companion.core.DEFAULT_EMPTY_STRING import com.rodrigolmti.lunch.money.companion.core.IDispatchersProvider @@ -42,8 +42,9 @@ private const val CURRENCY_KEY = "currency_key" private const val CATEGORIES_CACHE = "categories_cache" private const val ASSET_CACHE = "asset_cache" private const val TRANSACTION_CACHE = "transaction_cache" +private const val BUDGET_CACHE = "budget_cache" -internal class LunchRepository( +internal class AppRepository( private val json: Json, private val lunchService: LunchService, cacheManager: ICacheManager, @@ -51,7 +52,7 @@ internal class LunchRepository( private val dispatchers: IDispatchersProvider, private val preferences: SharedPreferencesDelegateFactory, private val iCrashlyticsSdk: ICrashlyticsSdk, -) : ILunchRepository { +) : IAppRepository { private var user: String by preferences.create(DEFAULT_EMPTY_STRING, USER_KEY) private var token: String by preferences.create(DEFAULT_EMPTY_STRING, TOKEN_KEY) @@ -65,6 +66,7 @@ internal class LunchRepository( private val assetCache = cacheManager.createCache>(ASSET_CACHE) private val transactionCache = cacheManager.createCache(TRANSACTION_CACHE) + private val budgetCache = cacheManager.createCache>(BUDGET_CACHE) override suspend fun authenticateUser(tokenDTO: TokenDTO): Outcome { if (!connectionChecker.isConnected()) return Outcome.failure(LunchError.NoConnectionError) @@ -72,8 +74,8 @@ internal class LunchRepository( return withContext(dispatchers.io()) { runCatching { val userResponse = lunchService.getUser(tokenDTO.format()) - this@LunchRepository.token = tokenDTO.value - this@LunchRepository.user = json + this@AppRepository.token = tokenDTO.value + this@AppRepository.user = json .encodeToString(UserResponse.serializer(), userResponse) }.mapThrowable { handleNetworkError(it) @@ -87,6 +89,8 @@ internal class LunchRepository( categoriesCache.clear() transactionCache.clear() assetCache.clear() + budgetCache.clear() + Unit }.mapThrowable { iCrashlyticsSdk.logNonFatal(it) LunchError.UnsuccessfulLogoutError @@ -163,13 +167,17 @@ internal class LunchRepository( } } + override fun getBudgetById(id: Int): BudgetModel? { + return budgetCache.get(BUDGET_CACHE, emptyList()).find { it.categoryId == id } + } + override fun getAssets(): List = assetCache.get(ASSET_CACHE, emptyList()) override suspend fun cacheTransactionCategories() { withContext(dispatchers.io()) { val categories = lunchService.getCategories().categories.map { it.toModel() } categoriesCache.clear() - categoriesCache.put(CATEGORIES_CACHE, categories) + .put(CATEGORIES_CACHE, categories) } } @@ -178,8 +186,9 @@ internal class LunchRepository( val assets = lunchService.getAssets().assets.map { it.toModel() } val plaidAccounts = lunchService.getPlaidAccounts().accounts.map { it.toModel() } val crypto = lunchService.getCrypto().crypto.map { it.toModel() } - assetCache.clear() - assetCache.put(ASSET_CACHE, assets + plaidAccounts + crypto) + assetCache + .clear() + .put(ASSET_CACHE, assets + plaidAccounts + crypto) } } @@ -192,6 +201,9 @@ internal class LunchRepository( return withContext(dispatchers.io()) { runCatching { val response = lunchService.getBudgets(start, end) + budgetCache + .clear() + .put(BUDGET_CACHE, mapBudget(response)) mapBudget(response) }.mapThrowable { handleNetworkError(it) diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/ScreenShootRepository.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/ScreenShootRepository.kt index 2b1b39a..1399b07 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/ScreenShootRepository.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/data/repository/ScreenShootRepository.kt @@ -14,7 +14,7 @@ import com.rodrigolmti.lunch.money.companion.composition.domain.model.Transactio import com.rodrigolmti.lunch.money.companion.composition.domain.model.TransactionModel import com.rodrigolmti.lunch.money.companion.composition.domain.model.TransactionStatus import com.rodrigolmti.lunch.money.companion.composition.domain.model.UserModel -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome import com.rodrigolmti.lunch.money.companion.core.cache.ICacheManager @@ -30,7 +30,7 @@ private const val ASSET_CACHE = "asset_cache" */ internal class ScreenShootRepository( cacheManager: ICacheManager, -) : ILunchRepository { +) : IAppRepository { private val _tickFlow = MutableSharedFlow(replay = 0) override val transactionUpdateFlow: SharedFlow = _tickFlow @@ -227,6 +227,10 @@ internal class ScreenShootRepository( ) } + override fun getBudgetById(id: Int): BudgetModel? { + return null + } + override suspend fun getBudgets( start: String, end: String @@ -245,15 +249,22 @@ internal class ScreenShootRepository( recurring = emptyList(), config = null, order = 1, - data = listOf( - CategoryModel( - numTransactions = 2, - spendingToBase = 45.0f, - budgetToBase = 100.0f, - budgetAmount = 100.0f, + data = mapOf( + "2021-01-01" to CategoryModel( + numTransactions = 1, + spendingToBase = 12.5f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, + budgetCurrency = "CAD", + isAutomated = false, + ), + "2021-01-02" to CategoryModel( + numTransactions = 1, + spendingToBase = 32.7f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, budgetCurrency = "CAD", isAutomated = false, - date = "2021-01-01", ) ) ), @@ -276,15 +287,22 @@ internal class ScreenShootRepository( autoSuggest = "yes", ), order = 2, - data = listOf( - CategoryModel( + data = mapOf( + "2021-01-01" to CategoryModel( + numTransactions = 1, + spendingToBase = 12.5f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, + budgetCurrency = "CAD", + isAutomated = false, + ), + "2021-01-02" to CategoryModel( numTransactions = 1, - spendingToBase = 0.0f, + spendingToBase = 32.7f, budgetToBase = 0.0f, budgetAmount = 0.0f, budgetCurrency = "CAD", isAutomated = false, - date = "2021-01-01", ) ) ), @@ -300,15 +318,22 @@ internal class ScreenShootRepository( recurring = emptyList(), config = null, order = 3, - data = listOf( - CategoryModel( - numTransactions = 0, - spendingToBase = 0.0f, - budgetToBase = 220.0f, - budgetAmount = 250.0f, + data = mapOf( + "2021-01-01" to CategoryModel( + numTransactions = 1, + spendingToBase = 12.5f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, + budgetCurrency = "CAD", + isAutomated = false, + ), + "2021-01-02" to CategoryModel( + numTransactions = 1, + spendingToBase = 32.7f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, budgetCurrency = "CAD", isAutomated = false, - date = "2021-01-01", ) ) ), @@ -324,7 +349,7 @@ internal class ScreenShootRepository( recurring = emptyList(), config = null, order = 4, - data = listOf() + data = mapOf() ), BudgetModel( categoryName = "Groceries", @@ -345,15 +370,22 @@ internal class ScreenShootRepository( autoSuggest = "yes", ), order = 5, - data = listOf( - CategoryModel( - numTransactions = 4, - spendingToBase = 282.0f, - budgetToBase = 600.0f, - budgetAmount = 600.0f, + data = mapOf( + "2021-01-01" to CategoryModel( + numTransactions = 1, + spendingToBase = 12.5f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, + budgetCurrency = "CAD", + isAutomated = false, + ), + "2021-01-02" to CategoryModel( + numTransactions = 1, + spendingToBase = 32.7f, + budgetToBase = 0.0f, + budgetAmount = 0.0f, budgetCurrency = "CAD", isAutomated = false, - date = "2021-01-01", ) ) ), @@ -369,7 +401,7 @@ internal class ScreenShootRepository( recurring = emptyList(), config = null, order = 6, - data = listOf() + data = mapOf() ), BudgetModel( categoryName = "Other", @@ -383,7 +415,7 @@ internal class ScreenShootRepository( recurring = emptyList(), config = null, order = 7, - data = listOf() + data = mapOf() ), ) ) diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/CompositionDI.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/CompositionDI.kt index 2356f81..271b9e2 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/CompositionDI.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/CompositionDI.kt @@ -6,8 +6,8 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.rodrigolmti.lunch.money.companion.application.main.IMainActivityViewModel import com.rodrigolmti.lunch.money.companion.application.main.MainActivityViewModel import com.rodrigolmti.lunch.money.companion.composition.data.network.LunchService -import com.rodrigolmti.lunch.money.companion.composition.data.repository.LunchRepository -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.data.repository.AppRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.composition.domain.usecase.ExecuteStartupLogic import com.rodrigolmti.lunch.money.companion.composition.domain.usecase.ExecuteStartupLogicUseCase import com.rodrigolmti.lunch.money.companion.composition.domain.usecase.GetTransactionSumByCategory @@ -45,7 +45,7 @@ private val dataModule = module { single { SharedPreferencesDelegateFactory(get()) } // single { ScreenShootRepository(get()) } - single { LunchRepository(get(), get(), get(), get(), get(), get(), get()) } + single { AppRepository(get(), get(), get(), get(), get(), get(), get()) } } private val domainModule = module { diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/FeaturesDI.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/FeaturesDI.kt index 8baf7f2..b38620d 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/FeaturesDI.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/FeaturesDI.kt @@ -7,7 +7,7 @@ import com.rodrigolmti.lunch.money.companion.composition.bridge.adapter.Recurrin import com.rodrigolmti.lunch.money.companion.composition.bridge.adapter.SettingsFeatureAdapter import com.rodrigolmti.lunch.money.companion.composition.bridge.adapter.TransactionFeatureAdapter import com.rodrigolmti.lunch.money.companion.composition.data.model.dto.TokenDTO -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.composition.domain.usecase.ExecuteStartupLogicUseCase import com.rodrigolmti.lunch.money.companion.features.analyze.AnalyzeViewModel import com.rodrigolmti.lunch.money.companion.features.analyze.IAnalyzeViewModel @@ -15,6 +15,8 @@ import com.rodrigolmti.lunch.money.companion.features.authentication.ui.Authenti import com.rodrigolmti.lunch.money.companion.features.authentication.ui.IAuthenticationViewModel import com.rodrigolmti.lunch.money.companion.features.budget.BudgetViewModel import com.rodrigolmti.lunch.money.companion.features.budget.IBudgetViewModel +import com.rodrigolmti.lunch.money.companion.features.budget.detail.BudgetDetailViewModel +import com.rodrigolmti.lunch.money.companion.features.budget.detail.IBudgetDetailViewModel import com.rodrigolmti.lunch.money.companion.features.home.ui.HomeViewModel import com.rodrigolmti.lunch.money.companion.features.home.ui.IHomeViewModel import com.rodrigolmti.lunch.money.companion.features.recurring.IRecurringViewModel @@ -33,7 +35,7 @@ import org.koin.dsl.module private val authenticationModule = module { viewModel { AuthenticationViewModel( - authenticateUser = { get().authenticateUser(TokenDTO(it)) }, + authenticateUser = { get().authenticateUser(TokenDTO(it)) }, postAuthentication = { get().invoke() }, ) } @@ -67,7 +69,7 @@ private val transactionsModule = module { TransactionFeatureAdapter(get()).getTransactions(start, end) }, listenForTransactionUpdate = { - get().transactionUpdateFlow + get().transactionUpdateFlow } ) } @@ -103,18 +105,26 @@ private val recurringModel = module { private val budgetModule = module { viewModel { BudgetViewModel( - getBudget = { start, end -> + getBudgetLambda = { start, end -> BudgetFeatureAdapter(get()).getBudget(start, end) }, ) } + viewModel { + BudgetDetailViewModel( + getBudgetLambda = { id -> + BudgetFeatureAdapter(get()).getBudget(id) + }, + ) + } + } private val settingsModule = module { viewModel { SettingsViewModel( - logoutUserRunner = { get().logoutUser() }, - updateCurrency = { get().updatePrimaryCurrency(it) }, + logoutUserRunner = { get().logoutUser() }, + updateCurrency = { get().updatePrimaryCurrency(it) }, getUserDataRunner = { SettingsFeatureAdapter(get()).getUserData() }, diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/NetworkDI.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/NetworkDI.kt index f59c0ee..abd724e 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/NetworkDI.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/di/NetworkDI.kt @@ -2,7 +2,7 @@ package com.rodrigolmti.lunch.money.companion.composition.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.rodrigolmti.lunch.money.companion.BuildConfig -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.ConnectionChecker import com.rodrigolmti.lunch.money.companion.core.SERVER_URL import com.rodrigolmti.lunch.money.companion.core.network.AuthInterceptor @@ -33,7 +33,7 @@ internal val networkModule = module { single { OkHttpClient.Builder() .addInterceptor(AuthInterceptor { - get().getSessionToken()?.format() + get().getSessionToken()?.format() }) .addInterceptor(get()) .build() diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/model/BudgetModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/model/BudgetModel.kt index e973a2d..f581369 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/model/BudgetModel.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/model/BudgetModel.kt @@ -9,7 +9,7 @@ internal data class BudgetModel( val isIncome: Boolean, val excludeFromBudget: Boolean, val excludeFromTotals: Boolean, - val data: List, + val data: Map, val config: CategoryConfigModel?, val order: Int, val recurring: List = emptyList() @@ -28,7 +28,6 @@ internal data class CategoryModel( val budgetAmount: Float, val budgetCurrency: String?, val isAutomated: Boolean, - val date: String ) internal data class CategoryConfigModel( diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/ILunchRepository.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/IAppRepository.kt similarity index 95% rename from app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/ILunchRepository.kt rename to app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/IAppRepository.kt index 9751e67..1d3b572 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/ILunchRepository.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/repository/IAppRepository.kt @@ -12,7 +12,7 @@ import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome import kotlinx.coroutines.flow.SharedFlow -internal interface ILunchRepository { +internal interface IAppRepository { suspend fun authenticateUser(tokenDTO: TokenDTO): Outcome suspend fun logoutUser(): Outcome suspend fun getTransactions( @@ -23,6 +23,8 @@ internal interface ILunchRepository { suspend fun getRecurring(): Outcome, LunchError> suspend fun getTransaction(id: Long): Outcome suspend fun updateTransaction(dto: UpdateTransactionDTO): Outcome + + fun getBudgetById(id: Int): BudgetModel? suspend fun getBudgets( start: String, end: String, diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicUseCase.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicUseCase.kt index 917830d..e5b6cc6 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicUseCase.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicUseCase.kt @@ -1,6 +1,6 @@ package com.rodrigolmti.lunch.money.companion.composition.domain.usecase -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.ConnectionChecker import com.rodrigolmti.lunch.money.companion.core.LunchError import com.rodrigolmti.lunch.money.companion.core.Outcome @@ -13,7 +13,7 @@ internal interface ExecuteStartupLogicUseCase { } internal class ExecuteStartupLogic( - private val lunchRepository: ILunchRepository, + private val lunchRepository: IAppRepository, private val connectionChecker: ConnectionChecker, private val iCrashlyticsSdk: ICrashlyticsSdk, ) : ExecuteStartupLogicUseCase { diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticated.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticated.kt index 2a000cf..607f8cb 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticated.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticated.kt @@ -1,13 +1,13 @@ package com.rodrigolmti.lunch.money.companion.composition.domain.usecase -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository internal interface IsUserAuthenticatedUseCase { operator fun invoke(): Boolean } internal class IsUserAuthenticated( - private val lunchRepository: ILunchRepository + private val lunchRepository: IAppRepository ) : IsUserAuthenticatedUseCase { override operator fun invoke(): Boolean { val user = lunchRepository.getSessionUser() diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/Cache.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/Cache.kt index 47c5855..9de1bf4 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/Cache.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/Cache.kt @@ -4,6 +4,6 @@ interface Cache { fun put(key: K, value: V, policy: CachePolicy = CachePolicy.NeverExpire) fun get(key: K, default: V): V fun get(key: K): V? - fun clear() + fun clear() : Cache } diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/InMemoryCache.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/InMemoryCache.kt index c648dc0..92877ab 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/InMemoryCache.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/core/cache/InMemoryCache.kt @@ -31,8 +31,9 @@ class InMemoryCache : Cache { return entry.value } - override fun clear() { + override fun clear(): Cache { cache.clear() + return this } private fun isEntryExpired(entry: CacheEntry): Boolean { diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/analyze/FilterBottomSheet.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/analyze/FilterBottomSheet.kt index 987dd9f..cc72006 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/analyze/FilterBottomSheet.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/analyze/FilterBottomSheet.kt @@ -10,9 +10,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowLeft -import androidx.compose.material.icons.filled.KeyboardArrowRight -import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -30,6 +29,7 @@ import com.rodrigolmti.lunch.money.companion.core.utils.LunchMoneyPreview import com.rodrigolmti.lunch.money.companion.features.transactions.ui.FilterPreset import com.rodrigolmti.lunch.money.companion.uikit.components.HorizontalSpacer import com.rodrigolmti.lunch.money.companion.uikit.components.LunchButton +import com.rodrigolmti.lunch.money.companion.uikit.components.MonthSelector import com.rodrigolmti.lunch.money.companion.uikit.components.VerticalSpacer import com.rodrigolmti.lunch.money.companion.uikit.theme.CompanionTheme import com.rodrigolmti.lunch.money.companion.uikit.theme.MidnightSlate @@ -59,7 +59,6 @@ fun FilterBottomSheetPreview() { } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterBottomSheet( label: String, @@ -116,39 +115,11 @@ fun FilterBottomSheet( if (selected == FilterPreset.CUSTOM) { VerticalSpacer(height = CompanionTheme.spacings.spacingE) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton(onClick = onPreviousMonthClick) { - Icon( - Icons.Filled.KeyboardArrowLeft, - contentDescription = null, - tint = SilverLining, - ) - } - - HorizontalSpacer(width = CompanionTheme.spacings.spacingB) - - Text( - text = label, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - textAlign = TextAlign.Center, - modifier = Modifier.weight(1f), - color = White, - style = CompanionTheme.typography.header, - ) - - HorizontalSpacer(width = CompanionTheme.spacings.spacingB) - - IconButton(onClick = onNextMonthClick) { - Icon( - Icons.Filled.KeyboardArrowRight, - contentDescription = null, - tint = SilverLining, - ) - } - } + MonthSelector( + label = label, + onPreviousMonthClick = onPreviousMonthClick, + onNextMonthClick = onNextMonthClick, + ) } VerticalSpacer(height = CompanionTheme.spacings.spacingF) diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetDetailScreen.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetDetailScreen.kt deleted file mode 100644 index 4e2eb6e..0000000 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetDetailScreen.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.rodrigolmti.lunch.money.companion.features.budget - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.rodrigolmti.lunch.money.companion.core.utils.LunchMoneyPreview -import com.rodrigolmti.lunch.money.companion.uikit.components.LunchAppBar -import com.rodrigolmti.lunch.money.companion.uikit.components.LunchTextField -import com.rodrigolmti.lunch.money.companion.uikit.theme.CompanionTheme -import com.rodrigolmti.lunch.money.companion.uikit.theme.White - -/** - * Add a component to control which month the budget should be set to; - * Add a list of recurring items related to this budget on a horizontal list; - * Add a list of months that contains this budget on a horizontal list; - * With budget value and total spending; - * Remove from the list of budgets the fields budget value and total spending; - * Cache the budget item on the repository, so the detail screen have access to it; - */ - -@Composable -fun BudgetDetailScreen( - onBackClick: () -> Unit = {}, - modifier: Modifier = Modifier, -) { - var value by remember { mutableLongStateOf(0) } - val scrollState = rememberScrollState() - - Scaffold( - topBar = { - LunchAppBar( - title = "Budget Detail", - onBackClick = onBackClick, - ) - }, - modifier = modifier, - ) { padding -> - Column( - modifier = Modifier - .fillMaxHeight() - .verticalScroll(scrollState) - .padding( - top = CompanionTheme.spacings.spacingJ, - start = CompanionTheme.spacings.spacingD, - end = CompanionTheme.spacings.spacingD, - bottom = CompanionTheme.spacings.spacingD, - ), - verticalArrangement = Arrangement.spacedBy(CompanionTheme.spacings.spacingD) - ) { - LunchTextField( - label = "Budget Value", - text = value.toString(), - enabled = false, - disabledTextColor = White, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - ), - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .clickable { - - } - ) - } - } -} - -@Composable -@LunchMoneyPreview -private fun BudgetDetailScreenPreview() { - CompanionTheme { - BudgetDetailScreen() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetItem.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetItem.kt index 22d31f8..f169a07 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetItem.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetItem.kt @@ -1,6 +1,7 @@ package com.rodrigolmti.lunch.money.companion.features.budget import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,22 +15,20 @@ 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.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp -import com.rodrigolmti.lunch.money.companion.R import com.rodrigolmti.lunch.money.companion.core.utils.LunchMoneyPreview -import com.rodrigolmti.lunch.money.companion.core.utils.formatCurrency import com.rodrigolmti.lunch.money.companion.uikit.components.VerticalSpacer import com.rodrigolmti.lunch.money.companion.uikit.theme.CharcoalMist import com.rodrigolmti.lunch.money.companion.uikit.theme.CompanionTheme -import com.rodrigolmti.lunch.money.companion.uikit.theme.EmeraldSpring -import com.rodrigolmti.lunch.money.companion.uikit.theme.FadedBlood -import com.rodrigolmti.lunch.money.companion.uikit.theme.SunburstGold import com.rodrigolmti.lunch.money.companion.uikit.theme.White @Composable -internal fun BudgetItem(budget: BudgetView) { +internal fun BudgetItem( + budget: BudgetView, + onItemClick: (BudgetView) -> Unit, + modifier: Modifier = Modifier, +) { Card( shape = MaterialTheme.shapes.medium, colors = CardDefaults.cardColors( @@ -39,10 +38,12 @@ internal fun BudgetItem(budget: BudgetView) { width = Dp.Hairline, color = Color.Black ), - modifier = Modifier + modifier = modifier + .clickable { + onItemClick(budget) + } .fillMaxWidth() ) { - Column( modifier = Modifier .padding(CompanionTheme.spacings.spacingD), @@ -62,101 +63,6 @@ internal fun BudgetItem(budget: BudgetView) { style = CompanionTheme.typography.bodyBold, ) } - - VerticalSpacer(height = CompanionTheme.spacings.spacingD) - - - if (budget.items.isNotEmpty()) { - - budget.items.forEach { - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - - Text( - stringResource(R.string.budget_transaction_label), - overflow = TextOverflow.Ellipsis, - maxLines = 2, - modifier = Modifier.weight(1f), - color = White, - style = CompanionTheme.typography.body, - ) - - Text( - text = it.totalTransactions.toString(), - color = White, - style = CompanionTheme.typography.bodyBold, - ) - - } - - VerticalSpacer(height = CompanionTheme.spacings.spacingD) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - - Text( - stringResource(R.string.budget_value_label), - overflow = TextOverflow.Ellipsis, - maxLines = 2, - modifier = Modifier.weight(1f), - color = White, - style = CompanionTheme.typography.body, - ) - - Text( - text = formatCurrency( - it.totalBudget, - it.currency - ), - color = SunburstGold, - style = CompanionTheme.typography.bodyBold, - ) - - } - - VerticalSpacer(height = CompanionTheme.spacings.spacingD) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - - Text( - stringResource(R.string.budget_spending_label), - overflow = TextOverflow.Ellipsis, - maxLines = 2, - modifier = Modifier.weight(1f), - color = White, - style = CompanionTheme.typography.body, - ) - - Text( - text = formatCurrency( - it.totalSpending, - it.currency - ), - color = if (it.totalSpending > it.totalBudget) FadedBlood else EmeraldSpring, - style = CompanionTheme.typography.bodyBold, - ) - - } - } - - } else { - - Text( - stringResource(R.string.budget_empty_budget_message), - overflow = TextOverflow.Ellipsis, - maxLines = 2, - color = White, - style = CompanionTheme.typography.body, - ) - } } } } @@ -166,12 +72,12 @@ internal fun BudgetItem(budget: BudgetView) { private fun BudgetItemPreview() { CompanionTheme { Column { - BudgetItem(fakeBudgetView()) + BudgetItem(fakeBudgetView(), {}) VerticalSpacer(height = CompanionTheme.spacings.spacingB) BudgetItem( fakeBudgetView( - items = emptyList() - ) + ), + {} ) } } diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetScreen.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetScreen.kt index c039def..9951674 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetScreen.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetScreen.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.launch internal fun BudgetScreen( uiModel: IBudgetUIModel = DummyIBudgetUIModel(), onError: (String, String) -> Unit = { _, _ -> }, + onItemClick: (BudgetView) -> Unit, ) { val viewState by uiModel.viewState.collectAsStateWithLifecycle() @@ -145,7 +146,12 @@ internal fun BudgetScreen( val item = budget[index] - BudgetItem(item) + BudgetItem( + item, + { +// onItemClick(it) + }, + ) } } } @@ -170,7 +176,7 @@ private fun getBudget( uiModel: IBudgetUIModel ) { val (start, end) = filterState.getFilter() - uiModel.getBudgetData(start, end) + uiModel.getBudgetList(start, end) } @Composable @@ -179,6 +185,6 @@ private fun BudgetScreenPreview( @PreviewParameter(BudgetUIModelProvider::class) uiModel: IBudgetUIModel ) { CompanionTheme { - BudgetScreen(uiModel) + BudgetScreen(uiModel, { _, _ -> }, {}) } } \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetView.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetView.kt index 89a73c6..eed754c 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetView.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetView.kt @@ -8,7 +8,7 @@ import java.util.UUID data class BudgetView( val category: String, val uuid: UUID = UUID.randomUUID(), - val items: List, + val items: Map, ) @Immutable @@ -20,27 +20,19 @@ data class BudgetItemView( ) fun fakeBudgetView( - items: List = listOf( - BudgetItemView( - totalTransactions = ValueGenerator.gen(), - totalSpending = ValueGenerator.gen(), - totalBudget = ValueGenerator.gen(), - currency = ValueGenerator.currency(), - ), - BudgetItemView( - totalTransactions = ValueGenerator.gen(), - totalSpending = ValueGenerator.gen(), - totalBudget = ValueGenerator.gen(), - currency = ValueGenerator.currency(), - ), - BudgetItemView( - totalTransactions = ValueGenerator.gen(), - totalSpending = ValueGenerator.gen(), - totalBudget = ValueGenerator.gen(), - currency = ValueGenerator.currency(), - ), + items: Map = mapOf( + ValueGenerator.gen() to fakeBudgetItemView(), + ValueGenerator.gen() to fakeBudgetItemView(), + ValueGenerator.gen() to fakeBudgetItemView(), ), ) = BudgetView( category = ValueGenerator.gen(), items = items, +) + +fun fakeBudgetItemView() = BudgetItemView( + totalTransactions = ValueGenerator.gen(), + totalSpending = ValueGenerator.gen(), + totalBudget = ValueGenerator.gen(), + currency = ValueGenerator.currency(), ) \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetViewModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetViewModel.kt index 49db714..67653b3 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetViewModel.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/BudgetViewModel.kt @@ -14,12 +14,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.Date -typealias GetBudgetData = suspend (start: Date, end: Date) -> Outcome, LunchError> +typealias GetBudgetList = suspend (start: Date, end: Date) -> Outcome, LunchError> internal abstract class IBudgetViewModel : ViewModel(), IBudgetUIModel internal class BudgetViewModel( - private val getBudget: GetBudgetData, + private val getBudgetLambda: GetBudgetList, ) : IBudgetViewModel() { private val _viewState = MutableStateFlow(BudgetUiState.Loading) @@ -27,13 +27,13 @@ internal class BudgetViewModel( init { val (start, end) = getCurrentMonthDates() - getBudgetData(start, end) + getBudgetList(start, end) } - override fun getBudgetData(start: Date, end: Date) { + override fun getBudgetList(start: Date, end: Date) { viewModelScope.launch { _viewState.update { BudgetUiState.Loading } - getBudget(start, end).onSuccess { result -> + getBudgetLambda(start, end).onSuccess { result -> _viewState.update { BudgetUiState.Success(result.toImmutableList()) } diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/DummyIBudgetUIModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/DummyIBudgetUIModel.kt index 3b8000a..6521a39 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/DummyIBudgetUIModel.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/DummyIBudgetUIModel.kt @@ -9,7 +9,7 @@ import java.util.Date internal class DummyIBudgetUIModel(state: BudgetUiState = BudgetUiState.Loading) : IBudgetUIModel { override val viewState: StateFlow = MutableStateFlow(state) - override fun getBudgetData(start: Date, end: Date) { + override fun getBudgetList(start: Date, end: Date) { // no-op } } diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/IBudgetUIModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/IBudgetUIModel.kt index 19b1630..ed679d0 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/IBudgetUIModel.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/IBudgetUIModel.kt @@ -6,5 +6,5 @@ import java.util.Date internal interface IBudgetUIModel { val viewState: StateFlow - fun getBudgetData(start: Date, end: Date) + fun getBudgetList(start: Date, end: Date) } \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDataItem.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDataItem.kt new file mode 100644 index 0000000..b01fe34 --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDataItem.kt @@ -0,0 +1,154 @@ +package com.rodrigolmti.lunch.money.companion.features.budget.detail + +import androidx.compose.foundation.BorderStroke +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.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import com.rodrigolmti.lunch.money.companion.R +import com.rodrigolmti.lunch.money.companion.core.utils.LunchMoneyPreview +import com.rodrigolmti.lunch.money.companion.core.utils.formatCurrency +import com.rodrigolmti.lunch.money.companion.features.budget.BudgetItemView +import com.rodrigolmti.lunch.money.companion.features.budget.fakeBudgetItemView +import com.rodrigolmti.lunch.money.companion.uikit.components.HorizontalSpacer +import com.rodrigolmti.lunch.money.companion.uikit.components.VerticalSpacer +import com.rodrigolmti.lunch.money.companion.uikit.theme.CharcoalMist +import com.rodrigolmti.lunch.money.companion.uikit.theme.CompanionTheme +import com.rodrigolmti.lunch.money.companion.uikit.theme.SunburstGold +import com.rodrigolmti.lunch.money.companion.uikit.theme.White + +@Composable +internal fun BudgetDataItem( + modifier: Modifier = Modifier, + item: BudgetItemView? = null, +) { + Card( + shape = MaterialTheme.shapes.medium, + colors = CardDefaults.cardColors( + containerColor = CharcoalMist + ), + modifier = modifier + .fillMaxWidth(), + border = BorderStroke( + width = Dp.Hairline, + color = Color.Black + ), + ) { + Column( + modifier = Modifier.padding( + top = CompanionTheme.spacings.spacingD, + start = CompanionTheme.spacings.spacingD, + end = CompanionTheme.spacings.spacingD, + bottom = CompanionTheme.spacings.spacingD + ) + ) { + Text( + text = "Period overview", + overflow = TextOverflow.Ellipsis, + color = SunburstGold, + style = CompanionTheme.typography.bodyBold, + ) + + item?.let { + VerticalSpacer(CompanionTheme.spacings.spacingD) + + Row { + Text( + text = stringResource(R.string.budget_transaction_label), + style = CompanionTheme.typography.body, + color = White, + modifier = Modifier.weight(1f) + ) + HorizontalSpacer(CompanionTheme.spacings.spacingA) + Text( + text = item.totalTransactions.toString(), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + color = White, + style = CompanionTheme.typography.bodyBold, + ) + } + + VerticalSpacer(CompanionTheme.spacings.spacingD) + + Row { + Text( + stringResource(R.string.budget_value_label), + style = CompanionTheme.typography.body, + color = White, + modifier = Modifier.weight(1f) + ) + HorizontalSpacer(CompanionTheme.spacings.spacingA) + Text( + text = formatCurrency( + item.totalBudget, + item.currency + ), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + color = White, + style = CompanionTheme.typography.bodyBold, + ) + } + + VerticalSpacer(CompanionTheme.spacings.spacingD) + + Row { + Text( + stringResource(R.string.budget_spending_label), + style = CompanionTheme.typography.body, + color = White, + modifier = Modifier.weight(1f) + ) + HorizontalSpacer(CompanionTheme.spacings.spacingA) + Text( + text = formatCurrency( + item.totalSpending, + item.currency + ), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + color = White, + style = CompanionTheme.typography.bodyBold, + ) + } + } ?: run { + VerticalSpacer(CompanionTheme.spacings.spacingD) + + Text( + stringResource(R.string.budget_empty_budget_message), + overflow = TextOverflow.Ellipsis, + maxLines = 2, + color = White, + style = CompanionTheme.typography.body, + ) + } + } + } +} + +@Composable +@LunchMoneyPreview +fun BudgetDataItemPreview() { + CompanionTheme { + Column { + BudgetDataItem( + item = fakeBudgetItemView() + ) + + VerticalSpacer(CompanionTheme.spacings.spacingD) + + BudgetDataItem() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailScreen.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailScreen.kt new file mode 100644 index 0000000..999c767 --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailScreen.kt @@ -0,0 +1,172 @@ +package com.rodrigolmti.lunch.money.companion.features.budget.detail + +import android.annotation.SuppressLint +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.rodrigolmti.lunch.money.companion.R +import com.rodrigolmti.lunch.money.companion.core.utils.LunchMoneyPreview +import com.rodrigolmti.lunch.money.companion.features.budget.BudgetView +import com.rodrigolmti.lunch.money.companion.features.transactions.ui.FilterState +import com.rodrigolmti.lunch.money.companion.uikit.components.Center +import com.rodrigolmti.lunch.money.companion.uikit.components.EmptyState +import com.rodrigolmti.lunch.money.companion.uikit.components.LunchAppBar +import com.rodrigolmti.lunch.money.companion.uikit.components.LunchButton +import com.rodrigolmti.lunch.money.companion.uikit.components.LunchLoading +import com.rodrigolmti.lunch.money.companion.uikit.components.LunchTextField +import com.rodrigolmti.lunch.money.companion.uikit.components.MonthSelector +import com.rodrigolmti.lunch.money.companion.uikit.theme.CompanionTheme +import com.rodrigolmti.lunch.money.companion.uikit.theme.White + +@Composable +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +internal fun BudgetDetailScreen( + uiModel: IBudgetDetailUIModel = DummyIBudgetDetailUIModel(), + budgetId: Int, + onBackClick: () -> Unit = {}, +) { + val viewState by uiModel.viewState.collectAsStateWithLifecycle() + var filterState by remember { mutableStateOf(FilterState()) } + var value by remember { mutableLongStateOf(0) } + val scrollState = rememberScrollState() + + LaunchedEffect(Unit) { + uiModel.getBudget(budgetId) + } + + Scaffold( + topBar = { + LunchAppBar( + title = "Budget Detail", + onBackClick = onBackClick, + ) + }, + modifier = Modifier, + ) { padding -> + when (viewState) { + is BudgetDetailUiState.Error -> { + EmptyState( + stringResource(R.string.budget_empty_content_message), + ) + } + is BudgetDetailUiState.Loading -> { + Center { + LunchLoading() + } + } + + is BudgetDetailUiState.Success -> { + val budget = (viewState as BudgetDetailUiState.Success).budget + + BuildBody( + budget = budget, + scrollState = scrollState, + filterState = filterState, + value = value, + onPreviousMonthClick = { + filterState = filterState.decrease() + }, + onNextMonthClick = { + filterState = filterState.increase() + }, + ) + } + } + } +} + +@Composable +private fun BuildBody( + budget: BudgetView, + scrollState: ScrollState, + filterState: FilterState, + value: Long, + onPreviousMonthClick: () -> Unit = {}, + onNextMonthClick: () -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxHeight() + .verticalScroll(scrollState) + .padding( + top = CompanionTheme.spacings.spacingJ, + start = CompanionTheme.spacings.spacingD, + end = CompanionTheme.spacings.spacingD, + bottom = CompanionTheme.spacings.spacingD, + ), + verticalArrangement = Arrangement.spacedBy(CompanionTheme.spacings.spacingD) + ) { + + MonthSelector( + label = filterState.getDisplay(), + onPreviousMonthClick = { + onPreviousMonthClick() + }, + onNextMonthClick = { + onNextMonthClick() + }, + ) + + with(budget.items[filterState.getStartDateAsString()]) { + BudgetDataItem(item = this) + } + + LunchTextField( + label = "Budget Value", + text = value.toString(), + enabled = false, + disabledTextColor = White, + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + ), + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { + + } + ) + + Spacer(modifier = Modifier.weight(1f)) + + LunchButton( + label = "Update", + isLoading = false, + isEnabled = false, + ) { + + } + } +} + +@Composable +@LunchMoneyPreview +private fun BudgetDetailScreenPreview( + @PreviewParameter(BudgetDetailUIModelProvider::class) uiModel: IBudgetDetailUIModel +) { + CompanionTheme { + BudgetDetailScreen(uiModel, 1) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailUiState.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailUiState.kt new file mode 100644 index 0000000..3511c2d --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailUiState.kt @@ -0,0 +1,9 @@ +package com.rodrigolmti.lunch.money.companion.features.budget.detail + +import com.rodrigolmti.lunch.money.companion.features.budget.BudgetView + +sealed class BudgetDetailUiState { + data object Loading : BudgetDetailUiState() + data object Error : BudgetDetailUiState() + data class Success(val budget: BudgetView) : BudgetDetailUiState() +} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailViewModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailViewModel.kt new file mode 100644 index 0000000..20dffed --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/BudgetDetailViewModel.kt @@ -0,0 +1,31 @@ +package com.rodrigolmti.lunch.money.companion.features.budget.detail + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.rodrigolmti.lunch.money.companion.features.budget.BudgetView +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +typealias GetBudget = (Int) -> BudgetView? + +internal abstract class IBudgetDetailViewModel : ViewModel(), IBudgetDetailUIModel + +internal class BudgetDetailViewModel( + private val getBudgetLambda: GetBudget, +) : IBudgetDetailViewModel() { + + private val _viewState = MutableStateFlow(BudgetDetailUiState.Loading) + override val viewState: StateFlow = _viewState + + override fun getBudget(budgetId: Int) { + viewModelScope.launch { + _viewState.value = BudgetDetailUiState.Loading + getBudgetLambda(budgetId)?.let { + _viewState.value = BudgetDetailUiState.Success(it) + } ?: run { + _viewState.value = BudgetDetailUiState.Error + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/DummyIBudgetDetailUIModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/DummyIBudgetDetailUIModel.kt new file mode 100644 index 0000000..65d5d88 --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/DummyIBudgetDetailUIModel.kt @@ -0,0 +1,28 @@ +package com.rodrigolmti.lunch.money.companion.features.budget.detail + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.rodrigolmti.lunch.money.companion.features.budget.fakeBudgetView +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal class DummyIBudgetDetailUIModel(state: BudgetDetailUiState = BudgetDetailUiState.Loading) : + IBudgetDetailUIModel { + override val viewState: StateFlow = MutableStateFlow(state) + + override fun getBudget(budgetId: Int) { + // no-op + } +} + +internal class BudgetDetailUIModelProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + DummyIBudgetDetailUIModel(BudgetDetailUiState.Loading), + DummyIBudgetDetailUIModel(BudgetDetailUiState.Error), + DummyIBudgetDetailUIModel( + BudgetDetailUiState.Success( + fakeBudgetView(), + ) + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/IBudgetDetailUIModel.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/IBudgetDetailUIModel.kt new file mode 100644 index 0000000..2b801e2 --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/budget/detail/IBudgetDetailUIModel.kt @@ -0,0 +1,18 @@ +package com.rodrigolmti.lunch.money.companion.features.budget.detail + +import kotlinx.coroutines.flow.StateFlow + +/** + * Add a component to control which month the budget should be set to; + * Add a list of recurring items related to this budget on a horizontal list; + * Add a list of months that contains this budget on a horizontal list; + * With budget value and total spending; + * Remove from the list of budgets the fields budget value and total spending; + * Cache the budget item on the repository, so the detail screen have access to it; + */ + +internal interface IBudgetDetailUIModel { + val viewState: StateFlow + + fun getBudget(budgetId: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/BottomNavigation.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/BottomNavigation.kt index a387e53..d809911 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/BottomNavigation.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/BottomNavigation.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.rodrigolmti.lunch.money.companion.R import com.rodrigolmti.lunch.money.companion.features.budget.BudgetScreen +import com.rodrigolmti.lunch.money.companion.features.budget.BudgetView import com.rodrigolmti.lunch.money.companion.features.budget.IBudgetViewModel import com.rodrigolmti.lunch.money.companion.features.home.ui.HomeScreen import com.rodrigolmti.lunch.money.companion.features.home.ui.IHomeViewModel @@ -71,6 +72,7 @@ internal fun BottomNavigation( onAnalyzeClick: () -> Unit = {}, onBreakdownClick: () -> Unit = {}, onWhatsNewClick: () -> Unit = {}, + onBudgetItemClick: (BudgetView) -> Unit, ) { val sheetState = rememberModalBottomSheetState( @@ -231,14 +233,16 @@ internal fun BottomNavigation( BottomNavigationRouter.BUDGET -> { val uiModel = koinViewModel() - BudgetScreen(uiModel) { title, description -> + BudgetScreen(uiModel, { title, description -> updateBottomSheetState( state, BottomNavigationUiState.ShowInformationBottomSheet(title, description), sheetState, scope ) - } + }, { + onBudgetItemClick(it) + },) } BottomNavigationRouter.RECURRING -> { diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/NavigationGraph.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/NavigationGraph.kt index 5c6a8a6..40535e5 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/NavigationGraph.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/navigation/NavigationGraph.kt @@ -18,6 +18,8 @@ import com.rodrigolmti.lunch.money.companion.features.analyze.AnalyzeScreen import com.rodrigolmti.lunch.money.companion.features.analyze.IAnalyzeViewModel import com.rodrigolmti.lunch.money.companion.features.authentication.ui.AuthenticationScreen import com.rodrigolmti.lunch.money.companion.features.authentication.ui.IAuthenticationViewModel +import com.rodrigolmti.lunch.money.companion.features.budget.detail.BudgetDetailScreen +import com.rodrigolmti.lunch.money.companion.features.budget.detail.IBudgetDetailViewModel import com.rodrigolmti.lunch.money.companion.features.webView.WebViewScreen import com.rodrigolmti.lunch.money.companion.features.transactions.ui.detail.ITransactionDetailViewModel import com.rodrigolmti.lunch.money.companion.features.transactions.ui.detail.TransactionsDetailScreen @@ -29,6 +31,7 @@ internal const val authenticationRoute = "/authentication" internal const val dashboardRoute = "/dashboard" internal const val webViewRouter = "/webView?url={url}" internal const val transactionDetailRouter = "/transaction?id={id}" +internal const val budgetDetailRouter = "/budget?id={id}" internal const val transactionSummaryRouter = "/transaction/summary" internal const val analyzeRouter = "/analyze" @@ -81,6 +84,9 @@ internal fun NavigationGraph( }, onWhatsNewClick = { navController.navigate(webViewRouter.replace("{url}", GITHUB_RELEASES_URL)) + }, + onBudgetItemClick = { + navController.navigate(budgetDetailRouter.replace("{id}", it.toString())) } ) } @@ -100,6 +106,26 @@ internal fun NavigationGraph( } } } + composable( + route = budgetDetailRouter, + arguments = listOf( + navArgument("id") { + type = NavType.IntType + } + ) + ) { navBackStackEntry -> + val uiModel = koinViewModel() + + navBackStackEntry.arguments?.getInt("id")?.let { + BudgetDetailScreen( + budgetId = it, + uiModel = uiModel, + + ) { + navController.navigateUp() + } + } + } composable( route = analyzeRouter, ) { diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/transactions/ui/FilterState.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/transactions/ui/FilterState.kt index 8bdcf7d..c9467c6 100644 --- a/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/transactions/ui/FilterState.kt +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/features/transactions/ui/FilterState.kt @@ -79,6 +79,11 @@ data class FilterState( } } + fun getStartDateAsString(): String { + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + return dateFormat.format(getFilter().first) + } + fun getDisplay(): String { val dateFormat = SimpleDateFormat("MMMM yyyy", Locale.getDefault()) return dateFormat.format(calendar.time) diff --git a/app/src/main/java/com/rodrigolmti/lunch/money/companion/uikit/components/MonthSelector.kt b/app/src/main/java/com/rodrigolmti/lunch/money/companion/uikit/components/MonthSelector.kt new file mode 100644 index 0000000..911e875 --- /dev/null +++ b/app/src/main/java/com/rodrigolmti/lunch/money/companion/uikit/components/MonthSelector.kt @@ -0,0 +1,71 @@ +package com.rodrigolmti.lunch.money.companion.uikit.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import com.rodrigolmti.lunch.money.companion.core.utils.LunchMoneyPreview +import com.rodrigolmti.lunch.money.companion.uikit.theme.CompanionTheme +import com.rodrigolmti.lunch.money.companion.uikit.theme.SilverLining +import com.rodrigolmti.lunch.money.companion.uikit.theme.White + +@Composable +internal fun MonthSelector( + label: String, + onPreviousMonthClick: () -> Unit = {}, + onNextMonthClick: () -> Unit = {}, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = onPreviousMonthClick) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = null, + tint = SilverLining, + ) + } + + HorizontalSpacer(width = CompanionTheme.spacings.spacingB) + + Text( + text = label, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + textAlign = TextAlign.Center, + modifier = Modifier.weight(1f), + color = White, + style = CompanionTheme.typography.header, + ) + + HorizontalSpacer(width = CompanionTheme.spacings.spacingB) + + IconButton(onClick = onNextMonthClick) { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = SilverLining, + ) + } + } +} + +@Composable +@LunchMoneyPreview +fun MonthSelectorPreview() { + CompanionTheme { + MonthSelector( + label = "January 2024", + onPreviousMonthClick = {}, + onNextMonthClick = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicTest.kt b/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicTest.kt index 6ddebab..97443d7 100644 --- a/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicTest.kt +++ b/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/ExecuteStartupLogicTest.kt @@ -1,6 +1,6 @@ package com.rodrigolmti.lunch.money.companion.composition.domain.usecase -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import com.rodrigolmti.lunch.money.companion.core.ConnectionChecker import io.mockk.coEvery import io.mockk.coVerify @@ -14,7 +14,7 @@ import kotlin.test.Test class ExecuteStartupLogicTest { - private val lunchRepository: ILunchRepository = mockk() + private val lunchRepository: IAppRepository = mockk() private val connectionChecker: ConnectionChecker = mockk(relaxed = true) private val sut = ExecuteStartupLogic(lunchRepository, connectionChecker) diff --git a/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticatedTest.kt b/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticatedTest.kt index 271974f..c5b57a2 100644 --- a/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticatedTest.kt +++ b/app/src/test/java/com/rodrigolmti/lunch/money/companion/composition/domain/usecase/IsUserAuthenticatedTest.kt @@ -1,7 +1,7 @@ package com.rodrigolmti.lunch.money.companion.composition.domain.usecase import com.rodrigolmti.lunch.money.companion.composition.domain.model.UserModel -import com.rodrigolmti.lunch.money.companion.composition.domain.repository.ILunchRepository +import com.rodrigolmti.lunch.money.companion.composition.domain.repository.IAppRepository import io.mockk.every import io.mockk.mockk import kotlin.test.Test @@ -10,7 +10,7 @@ import kotlin.test.assertTrue class IsUserAuthenticatedTest { - private val lunchRepository: ILunchRepository = mockk() + private val lunchRepository: IAppRepository = mockk() private val sut = IsUserAuthenticated(lunchRepository) private val user = mockk(relaxed = true)