From b5ba334767cff2d00128c6f7ccde0602808ec6ab Mon Sep 17 00:00:00 2001 From: Karim Khusnutdinov <khusnkar@fel.cvut.cz> Date: Sat, 18 May 2024 18:53:00 +0200 Subject: [PATCH] Fix merge conflicts --- .gitignore | 346 +++++++++++++++++- .idea/deploymentTargetDropDown.xml | 6 - .idea/deploymentTargetSelector.xml | 10 + .idea/vcs.xml | 2 +- .../financemanagement/FinanceManagementApp.kt | 37 +- .../financemanagement/database/AppDatabase.kt | 6 +- .../database/account/Account.kt | 15 + .../database/account/AccountDao.kt | 45 +++ .../database/account/AccountRepository.kt | 18 + .../account/OfflineAccountRepository.kt | 24 ++ .../OfflineTransactionRepository.kt | 1 + .../operation/transaction/Transaction.kt | 1 + .../operation/transaction/TransactionDao.kt | 5 +- .../transaction/TransactionRepository.kt | 1 + .../pda/financemanagement/di/AppContainer.kt | 6 + .../ui/screens/AccountsScreen.kt | 33 -- .../screens/accounts/AccountDetailScreen.kt | 104 ++++++ .../accounts/AccountDetailViewModel.kt | 77 ++++ .../ui/screens/accounts/AccountsScreen.kt | 252 +++++++++++++ .../ui/screens/accounts/AccountsViewModel.kt | 57 +++ .../ui/screens/accounts/EditAccountScreen.kt | 146 ++++++++ .../ui/screens/transactions/CameraView.kt | 1 - .../transactions/EditTransactionScreen.kt | 345 +++++++++-------- .../TransactionDetailViewModel.kt | 54 ++- .../ui/utils/AppViewModelProvider.kt | 25 +- 25 files changed, 1355 insertions(+), 262 deletions(-) create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/Account.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountDao.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountRepository.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/OfflineAccountRepository.kt delete mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/AccountsScreen.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailScreen.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailViewModel.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsScreen.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsViewModel.kt create mode 100644 app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/EditAccountScreen.kt diff --git a/.gitignore b/.gitignore index aa724b7..fe9ad7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,337 @@ + +# Created by https://www.gitignore.io/api/java,linux,macos,windows,android,intellij,androidstudio +# Edit at https://www.gitignore.io/?templates=java,linux,macos,windows,android,intellij,androidstudio + +### Android ### +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ *.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +.idea/caches + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +### Android Patch ### +gen-external-apklibs + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files + +# Files for the ART/Dalvik VM + +# Java class files + +# Generated files + +# Gradle files .gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +*.ipr +*~ +*.swp + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# OS-specific files .DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format + +# IntelliJ + +# mpeltonen/sbt-idea plugin + +# JIRA plugin + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Java ### +# Compiled class file + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) + +# Package Files # +*.jar +*.nar +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.TemporaryItems +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/java,linux,macos,windows,android,intellij,androidstudio \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 51f9d01..0c0c338 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -2,12 +2,6 @@ <project version="4"> <component name="deploymentTargetDropDown"> <value> - <entry key="MainActivity"> - <State /> - </entry> - <entry key="MainActivity (1)"> - <State /> - </entry> <entry key="app"> <State /> </entry> diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="deploymentTargetSelector"> + <selectionStates> + <SelectionState runConfigName="app"> + <option name="selectionMode" value="DROPDOWN" /> + </SelectionState> + </selectionStates> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$" vcs="Git" /> + <mapping directory="" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/FinanceManagementApp.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/FinanceManagementApp.kt index b97d998..8c63490 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/FinanceManagementApp.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/FinanceManagementApp.kt @@ -8,7 +8,10 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import cz.cvut.fel.pda.financemanagement.ui.screens.AccountsScreen +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.AccountsScreen +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.AccountDetailScreen +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.EditAccountScreen +import cz.cvut.fel.pda.financemanagement.ui.screens.preferences.PreferencesScreen import cz.cvut.fel.pda.financemanagement.ui.screens.category.CategoriesScreen import cz.cvut.fel.pda.financemanagement.ui.screens.category.CategoryDetailScreen import cz.cvut.fel.pda.financemanagement.ui.screens.category.EditCategoryScreen @@ -32,6 +35,8 @@ enum class FinanceManagementScreens { EditCategory, Accounts, + AccountDetail, + EditAccount, Loans, LoansDetail, @@ -40,7 +45,6 @@ enum class FinanceManagementScreens { Preferences } - @Composable fun FinanceManagementApp() { val navController: NavHostController = rememberNavController() @@ -68,26 +72,28 @@ fun FinanceManagementApp() { EditLoansScreen(navController) } composable(route = FinanceManagementScreens.Categories.name) { - CategoriesScreen(navController) + CategoriesScreen(navController) } composable(route = "${FinanceManagementScreens.EditTransaction.name}/{Id}", - arguments = listOf(navArgument("Id") { type = NavType.LongType })){ - EditTransactionScreen(navController) + arguments = listOf(navArgument("Id") { type = NavType.LongType })) { + EditTransactionScreen(navController, + it.arguments?.getLong("Id") + ) } composable(route = "${FinanceManagementScreens.TransactionsDetail.name}/{Id}", - arguments = listOf(navArgument("Id") { type = NavType.LongType })){ + arguments = listOf(navArgument("Id") { type = NavType.LongType })) { TransactionDetailScreen(navController, transactionId = it.arguments?.getLong("Id") ) } composable(route = "${FinanceManagementScreens.EditCategory.name}/{Id}", - arguments = listOf(navArgument("Id") { type = NavType.LongType })){ + arguments = listOf(navArgument("Id") { type = NavType.LongType })) { EditCategoryScreen(navController, it.arguments?.getLong("Id") ) } composable(route = "${FinanceManagementScreens.CategoryDetail.name}/{Id}", - arguments = listOf(navArgument("Id") { type = NavType.LongType })){ + arguments = listOf(navArgument("Id") { type = NavType.LongType })) { CategoryDetailScreen(navController = navController, categoryId = it.arguments?.getLong("Id") ) @@ -95,5 +101,18 @@ fun FinanceManagementApp() { composable(route = FinanceManagementScreens.Preferences.name) { PreferencesScreen(navController) } + composable(route = "${FinanceManagementScreens.EditAccount.name}/{Id}", + arguments = listOf(navArgument("Id") { type = NavType.LongType })) { + EditAccountScreen( + navController, + it.arguments?.getLong("Id") + ) + } + composable(route = "${FinanceManagementScreens.AccountDetail.name}/{Id}", + arguments = listOf(navArgument("Id") { type = NavType.LongType })){ + AccountDetailScreen(navController = navController, + accountId = it.arguments?.getLong("Id") + ) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/AppDatabase.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/AppDatabase.kt index eff77a1..7aae915 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/AppDatabase.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/AppDatabase.kt @@ -5,6 +5,8 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import cz.cvut.fel.pda.financemanagement.database.account.Account +import cz.cvut.fel.pda.financemanagement.database.account.AccountDao import cz.cvut.fel.pda.financemanagement.database.category.Category import cz.cvut.fel.pda.financemanagement.database.category.CategoryDao import cz.cvut.fel.pda.financemanagement.database.operation.loan.Loan @@ -13,13 +15,13 @@ import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transact import cz.cvut.fel.pda.financemanagement.database.operation.transaction.TransactionDao import cz.cvut.fel.pda.financemanagement.database.typeConvertion.Converters -@Database(entities = [Transaction::class, Category::class, Loan::class], version = 9, exportSchema = false) +@Database(entities = [Transaction::class, Account::class, Loan::class, Category::class], version = 11, exportSchema = false) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase(){ abstract fun transactionDao(): TransactionDao abstract fun categoryDao(): CategoryDao abstract fun loanDao(): LoanDao - + abstract fun accountDao(): AccountDao companion object{ @Volatile private var INSTANCE: AppDatabase? = null diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/Account.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/Account.kt new file mode 100644 index 0000000..63de0fb --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/Account.kt @@ -0,0 +1,15 @@ +package cz.cvut.fel.pda.financemanagement.database.account + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.math.BigDecimal +import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency + +@Entity(tableName = "account") +data class Account( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val name: String, + val balance: BigDecimal, + val currency: OperationCurrency +) diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountDao.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountDao.kt new file mode 100644 index 0000000..ef4564e --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountDao.kt @@ -0,0 +1,45 @@ +package cz.cvut.fel.pda.financemanagement.database.account + +import androidx.room.* +import cz.cvut.fel.pda.financemanagement.database.category.Category +import cz.cvut.fel.pda.financemanagement.database.category.CategoryType +import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transaction +import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal + +@Dao +interface AccountDao { + + @Query("SELECT * FROM `account` ORDER BY id ASC") + fun getAllAccounts(): Flow<List<Account>> + + @Query("SELECT * FROM `account` WHERE id = :id") + fun getAccountForId(id: Long): Flow<Account?> + + @Query(""" + SELECT t.*, c.* + FROM `transaction` t + INNER JOIN `category` c ON t.categoryId = c.id + WHERE t.accountId = :accountId + """) + fun getTransactionsWithCategoryForAccount(accountId: Long): Flow<Map<Transaction, Category>> + + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAccounts(vararg accounts: Account) + + @Update + suspend fun updateAccounts(vararg accounts: Account) + + @Delete + suspend fun delete(item: Account) + + @Query("DELETE FROM `account`") + suspend fun deleteAll() + + @Query("DELETE FROM `transaction` WHERE accountId = :accountId") + suspend fun deleteTransactionsByAccountId(accountId: Long) + + @Query("UPDATE `account` SET balance = balance + :amount WHERE id = :accountId") + suspend fun updateAccountBalance(accountId: Long, amount: BigDecimal) +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountRepository.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountRepository.kt new file mode 100644 index 0000000..e66450a --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/AccountRepository.kt @@ -0,0 +1,18 @@ +package cz.cvut.fel.pda.financemanagement.database.account + +import cz.cvut.fel.pda.financemanagement.database.category.Category +import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transaction +import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal + +interface AccountRepository { + fun getAllAccountsStream(): Flow<List<Account>> + fun getAccountForIdStream(id: Long): Flow<Account?> + suspend fun insertAccounts(vararg accounts: Account) + suspend fun deleteAccount(account: Account) + suspend fun deleteAllAccounts() + suspend fun updateAccounts(vararg accounts: Account) + fun getTransactionsWithCategoryForAccount(accountId: Long): Flow<Map<Transaction, Category>> + suspend fun deleteAccountWithTransactions(account: Account) + suspend fun updateAccountBalance(accountId: Long, amount: BigDecimal) +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/OfflineAccountRepository.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/OfflineAccountRepository.kt new file mode 100644 index 0000000..64ae19b --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/account/OfflineAccountRepository.kt @@ -0,0 +1,24 @@ +package cz.cvut.fel.pda.financemanagement.database.account + +import cz.cvut.fel.pda.financemanagement.database.category.Category +import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transaction +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import java.math.BigDecimal + +class OfflineAccountRepository(private val accountDao: AccountDao) : AccountRepository { + override fun getAllAccountsStream(): Flow<List<Account>> = accountDao.getAllAccounts() + override fun getAccountForIdStream(id: Long): Flow<Account?> = accountDao.getAccountForId(id) + override suspend fun insertAccounts(vararg accounts: Account) = accountDao.insertAccounts(*accounts) + override suspend fun deleteAccount(account: Account) = accountDao.delete(account) + override suspend fun deleteAllAccounts() = accountDao.deleteAll() + override suspend fun updateAccounts(vararg accounts: Account) = accountDao.updateAccounts(*accounts) + override fun getTransactionsWithCategoryForAccount(accountId: Long): Flow<Map<Transaction, Category>> = accountDao.getTransactionsWithCategoryForAccount(accountId) + override suspend fun deleteAccountWithTransactions(account: Account) { + accountDao.deleteTransactionsByAccountId(account.id) + accountDao.delete(account) + } + override suspend fun updateAccountBalance(accountId: Long, amount: BigDecimal) { + accountDao.updateAccountBalance(accountId, amount) + } +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/OfflineTransactionRepository.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/OfflineTransactionRepository.kt index 0719631..bac7c21 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/OfflineTransactionRepository.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/OfflineTransactionRepository.kt @@ -15,5 +15,6 @@ class OfflineTransactionRepository(private val transactionDao: TransactionDao): override suspend fun insertTransactions(vararg transactions: Transaction) = transactionDao.insertTransactions(*transactions) override suspend fun deleteTransaction(transaction: Transaction) = transactionDao.delete(transaction) override suspend fun deleteAllTransactions() = transactionDao.deleteAll() + override suspend fun deleteTransactionsByAccountId(accountId: Long) = transactionDao.deleteTransactionsByAccountId(accountId) override suspend fun updateTransactions(vararg transactions: Transaction) = transactionDao.updateTransactions(*transactions) } \ No newline at end of file diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/Transaction.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/Transaction.kt index f88d3df..cdced6c 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/Transaction.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/Transaction.kt @@ -19,4 +19,5 @@ data class Transaction( override var currency: OperationCurrency = OperationCurrency.CZK, var imageUrl: Uri = Uri.EMPTY, var categoryId: Long, + var accountId: Long ) : Operation() diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionDao.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionDao.kt index b53fd22..5d56da6 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionDao.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionDao.kt @@ -52,4 +52,7 @@ interface TransactionDao { @Query("DELETE FROM `transaction`") suspend fun deleteAll() -} \ No newline at end of file + + @Query("DELETE FROM `transaction` WHERE accountId = :accountId") + suspend fun deleteTransactionsByAccountId(accountId: Long) +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionRepository.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionRepository.kt index 6093e05..c4058d5 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionRepository.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/database/operation/transaction/TransactionRepository.kt @@ -14,5 +14,6 @@ interface TransactionRepository { suspend fun insertTransactions(vararg transactions: Transaction) suspend fun deleteTransaction(transaction: Transaction) suspend fun deleteAllTransactions() + suspend fun deleteTransactionsByAccountId(accountId: Long) suspend fun updateTransactions(vararg transactions: Transaction) } \ No newline at end of file diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/di/AppContainer.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/di/AppContainer.kt index e597b26..1fd26c2 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/di/AppContainer.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/di/AppContainer.kt @@ -2,6 +2,8 @@ package cz.cvut.fel.pda.financemanagement.di import android.content.Context import cz.cvut.fel.pda.financemanagement.database.AppDatabase +import cz.cvut.fel.pda.financemanagement.database.account.AccountRepository +import cz.cvut.fel.pda.financemanagement.database.account.OfflineAccountRepository import cz.cvut.fel.pda.financemanagement.database.category.CategoryRepository import cz.cvut.fel.pda.financemanagement.database.category.OfflineCategoryRepository import cz.cvut.fel.pda.financemanagement.database.operation.loan.LoanRepository @@ -15,6 +17,7 @@ interface AppContainer { val categoryRepository: CategoryRepository val preferencesRepository: PreferencesRepository val loanRepository: LoanRepository + val accountRepository: AccountRepository } class AppDataContainer(private val context: Context) : AppContainer { @@ -30,4 +33,7 @@ class AppDataContainer(private val context: Context) : AppContainer { override val loanRepository: LoanRepository by lazy { OfflineLoanRepository(AppDatabase.getDatabase(context).loanDao()) } + override val accountRepository: AccountRepository by lazy { + OfflineAccountRepository(AppDatabase.getDatabase(context).accountDao()) + } } \ No newline at end of file diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/AccountsScreen.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/AccountsScreen.kt deleted file mode 100644 index e50e0dd..0000000 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/AccountsScreen.kt +++ /dev/null @@ -1,33 +0,0 @@ -package cz.cvut.fel.pda.financemanagement.ui.screens - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import cz.cvut.fel.pda.financemanagement.ui.components.BottomNavigation -import cz.cvut.fel.pda.financemanagement.ui.components.TopBar - -@Composable -fun AccountsScreen(navController: NavHostController){ - Scaffold( - topBar = { - TopBar(navController) - }, - bottomBar = { - BottomNavigation(navController = navController) - } - ) { - innerPadding -> AccountsScreenContent(innerPadding) - } -} - -@Composable -fun AccountsScreenContent(innerPadding: PaddingValues) { - Column(modifier = Modifier.padding(innerPadding)) { - Text(text = "Accounts") - } -} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailScreen.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailScreen.kt new file mode 100644 index 0000000..dd1001b --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailScreen.kt @@ -0,0 +1,104 @@ +package cz.cvut.fel.pda.financemanagement.ui.screens.accounts + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import cz.cvut.fel.pda.financemanagement.FinanceManagementScreens +import cz.cvut.fel.pda.financemanagement.database.account.Account +import cz.cvut.fel.pda.financemanagement.database.category.Category +import cz.cvut.fel.pda.financemanagement.database.category.CategoryType +import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transaction +import cz.cvut.fel.pda.financemanagement.ui.components.BottomNavigation +import cz.cvut.fel.pda.financemanagement.ui.components.ShowErrorScreen +import cz.cvut.fel.pda.financemanagement.ui.components.ShowLoadingScreen +import cz.cvut.fel.pda.financemanagement.ui.components.TopBar +import cz.cvut.fel.pda.financemanagement.ui.utils.AppViewModelProvider + +@Composable +fun AccountDetailScreen( + navController: NavHostController, + accountId: Long?, + accountDetailViewModel: AccountDetailViewModel = viewModel( + factory = AppViewModelProvider.Factory + ), +) { + accountDetailViewModel.setAccountId(accountId ?: 0L) + val uiState by accountDetailViewModel.uiState.collectAsStateWithLifecycle() + + Scaffold( + topBar = { TopBar(navController) }, + content = { innerPadding -> + when (val state = uiState) { + is AccountDetailUiState.Error -> { + ShowErrorScreen( + errorMessage = state.exception.message ?: "Error", + onClick = { navController.popBackStack() } + ) + } + AccountDetailUiState.Loading -> { + ShowLoadingScreen() + } + is AccountDetailUiState.Success -> { + AccountScreenContent( + state.account, + state.transactionsWithCategory, + innerPadding, + navController + ) + } + } + }, + floatingActionButton = { + FloatingActionButton(onClick = { + navController.navigate("${FinanceManagementScreens.EditAccount.name}/${accountId}") + }) { + Icon(Icons.Default.Edit, contentDescription = "Edit Account") + } + }, + bottomBar = { BottomNavigation(navController = navController) } + ) +} + +@Composable +fun AccountScreenContent( + account: Account, + transactionsWithCategory: Map<Transaction, Category>, + innerPadding: PaddingValues, + navController: NavHostController +) { + Column(modifier = Modifier.padding(innerPadding).padding(16.dp)) { + Text(text = "Account Details", style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = account.name, style = MaterialTheme.typography.titleMedium) + Text(text = account.balance.toString(), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleSmall) + Spacer(modifier = Modifier.height(16.dp)) + Text(text = "Transactions", style = MaterialTheme.typography.bodyLarge) + HorizontalDivider() + transactionsWithCategory.forEach { (transaction, category) -> + TransactionItem(transaction, category) + } + } +} + +@Composable +fun TransactionItem(transaction: Transaction, category: Category) { + Row(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween) { + Column { + Text(text = category.name, style = MaterialTheme.typography.bodyLarge) + Text( + text = transaction.amount.toString(), + color = if (category.type == CategoryType.INCOME) Color.Green else Color.Red, + style = MaterialTheme.typography.bodyLarge + ) + } + Text(text = transaction.dateCreated.toString(), style = MaterialTheme.typography.bodyMedium) + } +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailViewModel.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailViewModel.kt new file mode 100644 index 0000000..e73109f --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountDetailViewModel.kt @@ -0,0 +1,77 @@ +package cz.cvut.fel.pda.financemanagement.ui.screens.accounts + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cz.cvut.fel.pda.financemanagement.database.account.Account +import cz.cvut.fel.pda.financemanagement.database.account.AccountRepository +import cz.cvut.fel.pda.financemanagement.database.category.Category +import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency +import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transaction +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.math.BigDecimal + +class AccountDetailViewModel( + private val accountRepository: AccountRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + private var accountId: Long = savedStateHandle.get<Long>("Id")!! + + val uiState: StateFlow<AccountDetailUiState> = + accountRepository.getAccountForIdStream(accountId) + .combine(accountRepository.getTransactionsWithCategoryForAccount(accountId)) { account, transactionsWithCategory -> + if (account != null) { + AccountDetailUiState.Success(account, transactionsWithCategory) + } else if (accountId == 0L) { + AccountDetailUiState.Success( + Account( + id = 0, + name = "", + balance = BigDecimal.ZERO, + currency = OperationCurrency.CZK + ), + emptyMap() + ) + } else { + AccountDetailUiState.Error(Exception("Account not found")) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000L), + initialValue = AccountDetailUiState.Loading + ) + + fun setAccountId(id: Long) { + accountId = id + } + + fun saveAccount( + name: String, + balance: BigDecimal, + currency: OperationCurrency + ) { + viewModelScope.launch { + val account = Account( + id = accountId, + name = name, + balance = balance, + currency = currency + ) + if (accountId == 0L) { + accountRepository.insertAccounts(account) + } else { + accountRepository.updateAccounts(account) + } + } + } +} + +sealed interface AccountDetailUiState { + data object Loading : AccountDetailUiState + data class Success( + val account: Account, + val transactionsWithCategory: Map<Transaction, Category> + ) : AccountDetailUiState + data class Error(val exception: Exception) : AccountDetailUiState +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsScreen.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsScreen.kt new file mode 100644 index 0000000..7116484 --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsScreen.kt @@ -0,0 +1,252 @@ +package cz.cvut.fel.pda.financemanagement.ui.screens.accounts + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import cz.cvut.fel.pda.financemanagement.FinanceManagementScreens +import cz.cvut.fel.pda.financemanagement.database.account.Account +import cz.cvut.fel.pda.financemanagement.database.category.CategoryType +import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency +import cz.cvut.fel.pda.financemanagement.datastore.PreferencesRepository +import cz.cvut.fel.pda.financemanagement.model.TransactionWithCategory +import cz.cvut.fel.pda.financemanagement.ui.components.BottomNavigation +import cz.cvut.fel.pda.financemanagement.ui.components.TopBar +import cz.cvut.fel.pda.financemanagement.ui.screens.preferences.PreferencesUiState +import cz.cvut.fel.pda.financemanagement.ui.screens.preferences.PreferencesViewModel +import cz.cvut.fel.pda.financemanagement.ui.screens.preferences.PreferencesViewModelFactory +import cz.cvut.fel.pda.financemanagement.ui.screens.transactions.TransactionViewModel +import cz.cvut.fel.pda.financemanagement.ui.screens.transactions.TransactionsWithCategoryUiState +import cz.cvut.fel.pda.financemanagement.ui.utils.AppViewModelProvider +import cz.cvut.fel.pda.financemanagement.ui.utils.CurrencyExchangeProvider +import kotlinx.coroutines.runBlocking +import java.math.BigDecimal +import java.math.RoundingMode + +@Composable +fun AccountsScreen( + navController: NavHostController, + accountsViewModel: AccountsViewModel = viewModel( + factory = AppViewModelProvider.Factory + ), + transactionViewModel: TransactionViewModel = viewModel( + factory = AppViewModelProvider.Factory + ), + preferencesViewModel: PreferencesViewModel = viewModel( + factory = PreferencesViewModelFactory( + PreferencesRepository(LocalContext.current) + ) + ) +) { + val accountsUiState: AccountsUiState by accountsViewModel.accounts.collectAsStateWithLifecycle() + val transactionsUiState: TransactionsWithCategoryUiState by transactionViewModel.transactionsWithCategory.collectAsStateWithLifecycle() + val preferencesUiState: PreferencesUiState by preferencesViewModel.preferencesUiStateFlow.collectAsStateWithLifecycle() + + Scaffold( + topBar = { TopBar(navController) }, + content = { innerPadding -> + when (preferencesUiState) { + is PreferencesUiState.Loading -> CircularProgressIndicator() + is PreferencesUiState.Error -> Text("Error: ${(preferencesUiState as PreferencesUiState.Error).exception.message}") + is PreferencesUiState.Success -> { + val preferredCurrency = (preferencesUiState as PreferencesUiState.Success).preferences.preferredCurrency + Column(modifier = Modifier.padding(innerPadding)) { + when (transactionsUiState) { + is TransactionsWithCategoryUiState.Loading -> CircularProgressIndicator() + is TransactionsWithCategoryUiState.Error -> Text("Error: ${(transactionsUiState as TransactionsWithCategoryUiState.Error).exception.message}") + is TransactionsWithCategoryUiState.Success -> { + val transactions = (transactionsUiState as TransactionsWithCategoryUiState.Success).itemList + TotalsHeader(transactions, preferredCurrency) + } + } + AccountsContent(accountsUiState, navController, innerPadding, accountsViewModel) + } + } + } + }, + floatingActionButton = { + FloatingActionButton(onClick = { + navController.navigate("${FinanceManagementScreens.EditAccount.name}/0") + }) { + Icon(Icons.Filled.Add, contentDescription = "Add Account") + } + }, + bottomBar = { BottomNavigation(navController = navController) } + ) +} + +@Composable +fun TotalsHeader(transactions: List<TransactionWithCategory>, preferredCurrency: OperationCurrency) { + val repo = PreferencesRepository(LocalContext.current) + val totalIncome = transactions.filter { it.category.type == CategoryType.INCOME }.sumOf { it.transaction.amount * BigDecimal(CurrencyExchangeProvider.getExchangeRate(it.transaction.currency.name, repo)) } + val totalExpense = transactions.filter { it.category.type == CategoryType.EXPENSE }.sumOf { it.transaction.amount * BigDecimal(CurrencyExchangeProvider.getExchangeRate(it.transaction.currency.name, repo)) } + val totalBalance = totalIncome - totalExpense + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(horizontalAlignment = Alignment.Start) { + Text( + text = "TOTAL EXPENSE", + style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold), + color = Color.Black + ) + Text( + text = "-${totalExpense.setScale(2, RoundingMode.FLOOR)} ${preferredCurrency.name}", + style = TextStyle(fontSize = 16.sp), + color = Color.Red + ) + } + Column(horizontalAlignment = Alignment.End) { + Text( + text = "TOTAL INCOME", + style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold), + color = Color.Black + ) + Text( + text = "${totalIncome.setScale(2, RoundingMode.FLOOR)} ${preferredCurrency.name}", + style = TextStyle(fontSize = 16.sp), + color = Color.Green + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "TOTAL BALANCE", + style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold), + color = Color.Black + ) + Text( + text = "${totalBalance.setScale(2, RoundingMode.FLOOR)} ${preferredCurrency.name}", + style = TextStyle(fontSize = 16.sp), + color = Color.Black + ) + } + } + } +} + +@Composable +fun AccountsContent(accountsState: AccountsUiState, navController: NavHostController, innerPadding: PaddingValues, accountsViewModel: AccountsViewModel) { + when (accountsState) { + is AccountsUiState.Loading -> CircularProgressIndicator() + is AccountsUiState.Error -> Text("Error: ${accountsState.exception}") + is AccountsUiState.Success -> ShowAccountsList(accountsState.itemList, navController, innerPadding, accountsViewModel) + } +} + +@Composable +fun ShowAccountsList(accounts: List<Account>, navController: NavHostController, innerPadding: PaddingValues, accountsViewModel: AccountsViewModel) { + LazyColumn(modifier = Modifier.padding(innerPadding)) { + items(accounts) { account -> + AccountItem(account, navController, accountsViewModel) + } + } +} + +@Composable +fun AccountItem(account: Account, navController: NavHostController, accountsViewModel: AccountsViewModel) { + var showMenu by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } + + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { Text(text = "Delete this account?") }, + text = { Text("Deleting this account will also delete all transactions associated with it.") }, + confirmButton = { + TextButton( + onClick = { + accountsViewModel.deleteAccountWithTransactions(account) + showDialog = false + } + ) { + Text("Delete") + } + }, + dismissButton = { + TextButton( + onClick = { showDialog = false } + ) { + Text("Cancel") + } + } + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)) + .clickable { + navController.navigate("${FinanceManagementScreens.AccountDetail.name}/${account.id}") + } + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + Text(text = account.name, style = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.Bold)) + Text( + text = "${account.balance} ${account.currency}", + style = TextStyle(fontSize = 16.sp), + color = if (account.balance < BigDecimal.ZERO) Color.Red else Color.Black + ) + } + Box { + IconButton(onClick = { showMenu = true }) { + Icon(Icons.Filled.MoreVert, contentDescription = "More options") + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem(onClick = { + navController.navigate("${FinanceManagementScreens.EditAccount.name}/${account.id}") + showMenu = false + }, text = { + Text("Edit") + }) + DropdownMenuItem(onClick = { + showDialog = true + showMenu = false + }, text = { + Text("Delete") + }) + } + } + } +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsViewModel.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsViewModel.kt new file mode 100644 index 0000000..c9dbac8 --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/AccountsViewModel.kt @@ -0,0 +1,57 @@ +package cz.cvut.fel.pda.financemanagement.ui.screens.accounts + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cz.cvut.fel.pda.financemanagement.database.account.Account +import cz.cvut.fel.pda.financemanagement.database.account.AccountRepository +import cz.cvut.fel.pda.financemanagement.database.category.CategoryType +import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency +import cz.cvut.fel.pda.financemanagement.database.operation.transaction.TransactionRepository +import cz.cvut.fel.pda.financemanagement.datastore.PreferencesRepository +import cz.cvut.fel.pda.financemanagement.model.common.Result +import cz.cvut.fel.pda.financemanagement.model.common.asResult +import cz.cvut.fel.pda.financemanagement.ui.utils.CurrencyExchangeProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.math.BigDecimal + +class AccountsViewModel( + private val accountRepository: AccountRepository, + private val preferencesRepository: PreferencesRepository +) : ViewModel() { + val accounts: StateFlow<AccountsUiState> = + accountRepository + .getAllAccountsStream() + .asResult() + .map { allAccountsToResult -> + when (allAccountsToResult) { + is Result.Error -> AccountsUiState.Error(Exception(allAccountsToResult.exception)) + is Result.Loading -> AccountsUiState.Loading + is Result.Success -> AccountsUiState.Success(allAccountsToResult.data) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000L), + initialValue = AccountsUiState.Loading + ) + + fun deleteAccountWithTransactions(account: Account) { + viewModelScope.launch { + accountRepository.deleteAccountWithTransactions(account) + } + } +} + +sealed interface AccountsUiState { + data object Loading : AccountsUiState + data class Success(val itemList: List<Account> = listOf()) : AccountsUiState + data class Error(val exception: Exception) : AccountsUiState +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/EditAccountScreen.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/EditAccountScreen.kt new file mode 100644 index 0000000..457584d --- /dev/null +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/accounts/EditAccountScreen.kt @@ -0,0 +1,146 @@ +package cz.cvut.fel.pda.financemanagement.ui.screens.accounts + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import cz.cvut.fel.pda.financemanagement.database.account.Account +import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency +import cz.cvut.fel.pda.financemanagement.ui.components.BottomNavigation +import cz.cvut.fel.pda.financemanagement.ui.components.ShowErrorScreen +import cz.cvut.fel.pda.financemanagement.ui.components.ShowLoadingScreen +import cz.cvut.fel.pda.financemanagement.ui.components.TopBar +import cz.cvut.fel.pda.financemanagement.ui.utils.AppViewModelProvider +import java.math.BigDecimal + +@Composable +fun EditAccountScreen( + navController: NavHostController, + accountId: Long?, + accountDetailViewModel: AccountDetailViewModel = viewModel( + factory = AppViewModelProvider.Factory + ), +) { + val uiState by accountDetailViewModel.uiState.collectAsStateWithLifecycle() + + when (val state = uiState) { + is AccountDetailUiState.Error -> { + ShowErrorScreen( + errorMessage = state.exception.message ?: "Error", + onClick = { navController.popBackStack() } + ) + } + AccountDetailUiState.Loading -> { + ShowLoadingScreen() + } + is AccountDetailUiState.Success -> { + ShowAccountSuccessScreen( + state.account, + navController, + accountDetailViewModel + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShowAccountSuccessScreen( + account: Account, + navController: NavHostController, + accountDetailViewModel: AccountDetailViewModel +) { + val name = rememberSaveable { mutableStateOf(account.name) } + val balance = rememberSaveable { mutableStateOf(account.balance.toString()) } + val currency = rememberSaveable { mutableStateOf(account.currency) } + val currencies = OperationCurrency.entries + val expanded = remember { mutableStateOf(false) } + + Scaffold( + topBar = { TopBar(navController = navController) }, + content = { innerPadding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 4.dp) + .verticalScroll(rememberScrollState()) + .padding(innerPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val spaceModifier = Modifier + .fillMaxWidth() + .padding(8.dp) + Text( + text = if (account.id == 0L) "New Account" else "Edit Account", + style = MaterialTheme.typography.titleLarge + ) + OutlinedTextField( + value = name.value, + onValueChange = { name.value = it }, + label = { Text("Name") }, + modifier = spaceModifier + ) + Box(modifier = spaceModifier) { + OutlinedTextField( + value = currency.value.name, + onValueChange = {}, + label = { Text("Currency") }, + readOnly = true, + trailingIcon = { + IconButton(onClick = { expanded.value = true }) { + Icon(Icons.Outlined.Check, contentDescription = "Choose Currency") + } + } + ) + DropdownMenu( + expanded = expanded.value, + onDismissRequest = { expanded.value = false } + ) { + currencies.forEach { currencyOption -> + DropdownMenuItem( + onClick = { + currency.value = currencyOption + expanded.value = false + }, + text = { + Text(currencyOption.name) + } + ) + } + } + } + } + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + accountDetailViewModel.saveAccount( + name = name.value, + balance = if (account.id == 0L) BigDecimal.ZERO else BigDecimal(balance.value), + currency = currency.value + ) + navController.popBackStack() + } + ) { + Icon( + Icons.Outlined.Check, + contentDescription = "Save", + modifier = Modifier.size(24.dp) + ) + } + }, + bottomBar = { + BottomNavigation(navController = navController) + } + ) +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/CameraView.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/CameraView.kt index 8b56938..e5aef58 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/CameraView.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/CameraView.kt @@ -36,7 +36,6 @@ import java.util.concurrent.Executor import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CameraView( outputDirectory: File, diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/EditTransactionScreen.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/EditTransactionScreen.kt index 85382b3..08a03ac 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/EditTransactionScreen.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/EditTransactionScreen.kt @@ -1,43 +1,15 @@ package cz.cvut.fel.pda.financemanagement.ui.screens.transactions import android.net.Uri -import android.util.Log -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.outlined.Close -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -48,14 +20,15 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage -import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.rememberMultiplePermissionsState import cz.cvut.fel.pda.financemanagement.R +import cz.cvut.fel.pda.financemanagement.database.account.Account import cz.cvut.fel.pda.financemanagement.database.category.Category import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency import cz.cvut.fel.pda.financemanagement.model.TransactionWithCategory import cz.cvut.fel.pda.financemanagement.ui.components.ShowErrorScreen import cz.cvut.fel.pda.financemanagement.ui.components.ShowLoadingScreen +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.AccountsUiState +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.AccountsViewModel import cz.cvut.fel.pda.financemanagement.ui.screens.category.CategoriesUiState import cz.cvut.fel.pda.financemanagement.ui.screens.category.CategoryViewModel import cz.cvut.fel.pda.financemanagement.ui.utils.AppViewModelProvider @@ -67,16 +40,25 @@ import java.util.concurrent.Executors @Composable fun EditTransactionScreen( navController: NavHostController, - transactionDetailViewModel: TransactionDetailViewModel = viewModel(factory = AppViewModelProvider.Factory), - categoryViewModel: CategoryViewModel = viewModel(factory = AppViewModelProvider.Factory), + transactionId: Long?, + transactionDetailViewModel: TransactionDetailViewModel = viewModel( + factory = AppViewModelProvider.Factory + ), + categoryViewModel: CategoryViewModel = viewModel( + factory = AppViewModelProvider.Factory + ), + accountsViewModel: AccountsViewModel = viewModel( + factory = AppViewModelProvider.Factory + ) ) { - val transactionUiState by transactionDetailViewModel.uiState.collectAsStateWithLifecycle() + val uiState by transactionDetailViewModel.uiState.collectAsStateWithLifecycle() val categoryUiState by categoryViewModel.categories.collectAsStateWithLifecycle() + val accountsUiState by accountsViewModel.accounts.collectAsStateWithLifecycle() - when (transactionUiState) { + when (uiState) { is TransactionWithCategoryUiState.Error -> { ShowErrorScreen( - errorMessage = (transactionUiState as TransactionWithCategoryUiState.Error).exception.message ?: "Error", + errorMessage = (uiState as TransactionWithCategoryUiState.Error).exception.message ?: "Error", onClick = { navController.popBackStack() } ) } @@ -84,15 +66,28 @@ fun EditTransactionScreen( ShowLoadingScreen() } is TransactionWithCategoryUiState.Success -> { - val transaction = (transactionUiState as TransactionWithCategoryUiState.Success).transaction - when (categoryUiState) { + when (val categoryState = categoryUiState) { is CategoriesUiState.Success -> { - ShowSuccessScreen( - transaction = transaction, - categories = (categoryUiState as CategoriesUiState.Success).itemList, - navController = navController, - transactionViewModel = transactionDetailViewModel, - ) + when (val accountsState = accountsUiState) { + is AccountsUiState.Success -> { + ShowSuccessScreen( + (uiState as TransactionWithCategoryUiState.Success).transaction, + categoryState.itemList, + accountsState.itemList, + navController, + transactionDetailViewModel, + ) + } + is AccountsUiState.Loading -> { + ShowLoadingScreen() + } + is AccountsUiState.Error -> { + ShowErrorScreen( + errorMessage = accountsState.exception.message ?: "Error", + onClick = { navController.popBackStack() } + ) + } + } } is CategoriesUiState.Loading -> { ShowLoadingScreen() @@ -108,84 +103,92 @@ fun EditTransactionScreen( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ShowSuccessScreen( transaction: TransactionWithCategory, categories: List<Category>, + accounts: List<Account>, navController: NavHostController, transactionViewModel: TransactionDetailViewModel, ) { val amount = rememberSaveable { mutableStateOf(transaction.transaction.amount.toString()) } val currency = rememberSaveable { mutableStateOf(transaction.transaction.currency) } - val selectedCategory = remember { - mutableStateOf(categories.find { it.id == transaction.category.id } ?: categories[0]) - } + val selectedCategory = remember { mutableStateOf(categories.find { it.id == transaction.category.id } ?: categories.getOrNull(0)) } + val selectedAccount = remember { mutableStateOf(accounts.find { it.id == transaction.transaction.accountId } ?: accounts.getOrNull(0)) } val imageUri = rememberSaveable { mutableStateOf(transaction.transaction.imageUrl) } + val showCameraView = remember { mutableStateOf(false) } + val cameraExecutor = remember { mutableStateOf<ExecutorService?>(null) } Scaffold( topBar = { - SetupTopBar(navController, transactionViewModel, amount, selectedCategory, currency, imageUri) + TopAppBar( + title = { Text("Transaction edit") }, + colors = TopAppBarDefaults.mediumTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + modifier = Modifier, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "Close" + ) + } + }, + actions = { + TextButton( + onClick = { + transactionViewModel.saveTransaction( + amount = BigDecimal(amount.value), + category = selectedCategory.value ?: return@TextButton, + currency = currency.value, + accountId = selectedAccount.value?.id ?: return@TextButton, + imageUri = imageUri.value + ) + navController.popBackStack() + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = "Save") + } + } + ) }, content = { innerPadding -> - EditTransactionDetailContent( - innerPadding, amount, currency, categories, selectedCategory, imageUri + EditTransactionDetailSuccessContent( + innerPadding, + amount, + currency, + categories, + selectedCategory, + accounts, + selectedAccount, + showCameraView, + cameraExecutor, + imageUri ) } ) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SetupTopBar( - navController: NavHostController, - transactionViewModel: TransactionDetailViewModel, - amount: MutableState<String>, - selectedCategory: MutableState<Category>, - currency: MutableState<OperationCurrency>, - imageUri: MutableState<Uri> -) { - TopAppBar( - title = { Text("Edit Transaction") }, - colors = TopAppBarDefaults.mediumTopAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - navigationIcon = { - IconButton(onClick = { navController.popBackStack() }) { - Icon(imageVector = Icons.Outlined.Close, contentDescription = "Close") - } - }, - actions = { - TextButton( - onClick = { - transactionViewModel.saveTransaction( - amount = BigDecimal(amount.value), - category = selectedCategory.value, - currency = currency.value, - imageUri = imageUri.value - ) - navController.popBackStack() - }, - modifier = Modifier.padding(16.dp) - ) { - Text(text = "Save") - } - } - ) -} - -@OptIn(ExperimentalGlideComposeApi::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalGlideComposeApi::class) @Composable -fun EditTransactionDetailContent( +fun EditTransactionDetailSuccessContent( innerPadding: PaddingValues, amount: MutableState<String>, currency: MutableState<OperationCurrency>, categories: List<Category>, - selectedCategory: MutableState<Category>, + selectedCategory: MutableState<Category?>, + accounts: List<Account>, + selectedAccount: MutableState<Account?>, + showCameraView: MutableState<Boolean>, + cameraExecutor: MutableState<ExecutorService?>, imageUri: MutableState<Uri> ) { + val showDropdown = remember { mutableStateOf(false) } val showCurrencyDropdown = remember { mutableStateOf(false) } - val showCameraView = remember { mutableStateOf(false) } - val cameraExecutor = remember { mutableStateOf<ExecutorService?>(null) } + val showAccountDropdown = remember { mutableStateOf(false) } if (showCameraView.value) { Box( @@ -202,7 +205,9 @@ fun EditTransactionDetailContent( .padding(innerPadding), horizontalAlignment = Alignment.CenterHorizontally ) { - val spaceModifier = Modifier.padding(24.dp, 8.dp) + val spaceModifier = Modifier + .fillMaxWidth() + .padding(24.dp, 4.dp) Box(contentAlignment = Alignment.TopEnd) { OutlinedTextField( @@ -220,19 +225,18 @@ fun EditTransactionDetailContent( }, modifier = spaceModifier ) - DropdownMenu( + modifier = spaceModifier, expanded = showCurrencyDropdown.value, onDismissRequest = { showCurrencyDropdown.value = false } ) { OperationCurrency.entries.forEachIndexed { index, operationCurrency -> - DropdownMenuItem( - onClick = { - currency.value = operationCurrency - showCurrencyDropdown.value = false - }, - text = { Text(operationCurrency.toString()) } - ) + DropdownMenuItem(onClick = { + currency.value = operationCurrency + showCurrencyDropdown.value = false + }, text = { + Text(operationCurrency.toString()) + }) if (index < OperationCurrency.entries.size - 1) { HorizontalDivider() } @@ -240,11 +244,57 @@ fun EditTransactionDetailContent( } } - CategoryDropdown( - categories = categories, - selectedCategory = selectedCategory, - spaceModifier = spaceModifier + OutlinedTextField( + value = selectedCategory.value?.name.orEmpty(), + onValueChange = {}, + label = { Text("Category") }, + trailingIcon = { + IconButton(onClick = { showDropdown.value = true }) { + Icon(Icons.Filled.ArrowDropDown, contentDescription = null) + } + }, + readOnly = true, + modifier = spaceModifier ) + DropdownMenu( + expanded = showDropdown.value, + onDismissRequest = { showDropdown.value = false } + ) { + categories.forEach { category -> + DropdownMenuItem(onClick = { + selectedCategory.value = category + showDropdown.value = false + }, text = { + Text(category.name) + }) + } + } + + OutlinedTextField( + value = selectedAccount.value?.name.orEmpty(), + onValueChange = {}, + label = { Text("Account") }, + trailingIcon = { + IconButton(onClick = { showAccountDropdown.value = true }) { + Icon(Icons.Filled.ArrowDropDown, contentDescription = null) + } + }, + readOnly = true, + modifier = spaceModifier + ) + DropdownMenu( + expanded = showAccountDropdown.value, + onDismissRequest = { showAccountDropdown.value = false } + ) { + accounts.forEach { account -> + DropdownMenuItem(onClick = { + selectedAccount.value = account + showAccountDropdown.value = false + }, text = { + Text(account.name) + }) + } + } Button(onClick = { cameraExecutor.value = Executors.newSingleThreadExecutor() @@ -267,47 +317,6 @@ fun EditTransactionDetailContent( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CategoryDropdown( - categories: List<Category>, - selectedCategory: MutableState<Category>, - spaceModifier: Modifier -) { - var expanded by remember { mutableStateOf(false) } - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } - ) { - OutlinedTextField( - value = selectedCategory.value.name, - readOnly = true, - label = { Text("Category") }, - onValueChange = {}, - modifier = spaceModifier.menuAnchor(), - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) } - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - categories.forEachIndexed { index, category -> - DropdownMenuItem( - onClick = { - selectedCategory.value = category - expanded = false - }, - text = { Text(category.name) } - ) - if (index < categories.size - 1) { - HorizontalDivider() - } - } - } - } -} - -@OptIn(ExperimentalPermissionsApi::class) @Composable fun ShowCameraView( cameraExecutor: MutableState<ExecutorService?>, @@ -319,36 +328,12 @@ fun ShowCameraView( showCameraView.value = false } - val cameraPermissionsState = rememberMultiplePermissionsState( - permissions = listOf(android.Manifest.permission.CAMERA) + CameraView( + outputDirectory = getOutputDirectory(), + executor = cameraExecutor.value!!, + onImageCaptured = ::handleImageCapture, + onError = { /* Handle error */ } ) - var shouldShowRationaleDialog by remember { mutableStateOf(true) } - - if (cameraPermissionsState.shouldShowRationale && shouldShowRationaleDialog) { - AlertDialog( - onDismissRequest = {}, - title = { Text("Camera Permissions Request") }, - text = { Text("Camera permissions are necessary for taking photos.") }, - confirmButton = { - TextButton(onClick = { shouldShowRationaleDialog = false }) { - Text("OK") - } - } - ) - } else { - LaunchedEffect(Unit) { - cameraPermissionsState.launchMultiplePermissionRequest() - } - } - - if (cameraPermissionsState.allPermissionsGranted) { - CameraView( - outputDirectory = getOutputDirectory(), - executor = cameraExecutor.value!!, - onImageCaptured = ::handleImageCapture, - onError = { Log.e("Camera", "View error:", it) } - ) - } } @Composable diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/TransactionDetailViewModel.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/TransactionDetailViewModel.kt index fbb8cb2..c77d87d 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/TransactionDetailViewModel.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/screens/transactions/TransactionDetailViewModel.kt @@ -4,6 +4,7 @@ import android.net.Uri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import cz.cvut.fel.pda.financemanagement.database.account.AccountRepository import cz.cvut.fel.pda.financemanagement.database.category.Category import cz.cvut.fel.pda.financemanagement.database.category.CategoryType import cz.cvut.fel.pda.financemanagement.database.operation.OperationCurrency @@ -12,6 +13,7 @@ import cz.cvut.fel.pda.financemanagement.database.operation.transaction.Transact import cz.cvut.fel.pda.financemanagement.model.TransactionWithCategory import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -19,8 +21,9 @@ import java.math.BigDecimal class TransactionDetailViewModel( private val transactionRepository: TransactionRepository, + private val accountRepository: AccountRepository, savedStateHandle: SavedStateHandle -): ViewModel() { +) : ViewModel() { private var transactionId: Long = savedStateHandle.get<Long>("Id")!! val uiState: StateFlow<TransactionWithCategoryUiState> = @@ -32,9 +35,9 @@ class TransactionDetailViewModel( } else if (transactionId == 0L) { TransactionWithCategoryUiState.Success( TransactionWithCategory( - transaction = Transaction(categoryId = 0L), - category = Category(0, "Groceries",CategoryType.INCOME) - ) + transaction = Transaction(categoryId = 0L, accountId = 0L), + category = Category(0, "Groceries", CategoryType.INCOME) + ) ) } else { TransactionWithCategoryUiState.Error(Exception("Transaction not found")) @@ -44,31 +47,56 @@ class TransactionDetailViewModel( started = SharingStarted.WhileSubscribed(5000L), initialValue = TransactionWithCategoryUiState.Loading ) + fun setTransactionId(id: Long) { transactionId = id } + fun saveTransaction( amount: BigDecimal, category: Category, currency: OperationCurrency, + accountId: Long, imageUri: Uri ) { viewModelScope.launch { - transactionRepository.insertTransactions( - Transaction( - id = transactionId, - amount = amount, - categoryId = category.id, - currency = currency, - imageUrl = imageUri - ) + val transaction = Transaction( + id = transactionId, + amount = amount, + categoryId = category.id, + currency = currency, + accountId = accountId, + imageUrl = imageUri ) + + if (transactionId == 0L) { + transactionRepository.insertTransactions(transaction) + updateAccountBalance(accountId, amount, category.type) + } else { + // Get the old transaction details + val oldTransaction = transactionRepository.getTransactionForIdStream(transactionId).firstOrNull() + if (oldTransaction != null) { + transactionRepository.updateTransactions(transaction) + // Update account balance based on the difference + val difference = amount.subtract(oldTransaction.amount) + updateAccountBalance(accountId, difference, category.type) + } + } } } + + private suspend fun updateAccountBalance(accountId: Long, amount: BigDecimal, categoryType: CategoryType) { + val adjustment = if (categoryType == CategoryType.INCOME) { + amount + } else { + amount.negate() + } + accountRepository.updateAccountBalance(accountId, adjustment) + } } sealed interface TransactionWithCategoryUiState { data object Loading : TransactionWithCategoryUiState data class Success(val transaction: TransactionWithCategory) : TransactionWithCategoryUiState data class Error(val exception: Exception) : TransactionWithCategoryUiState -} \ No newline at end of file +} diff --git a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/utils/AppViewModelProvider.kt b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/utils/AppViewModelProvider.kt index 57d1295..3012184 100644 --- a/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/utils/AppViewModelProvider.kt +++ b/app/src/main/java/cz/cvut/fel/pda/financemanagement/ui/utils/AppViewModelProvider.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import cz.cvut.fel.pda.financemanagement.FinanceManagementApplication +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.AccountDetailViewModel +import cz.cvut.fel.pda.financemanagement.ui.screens.accounts.AccountsViewModel import cz.cvut.fel.pda.financemanagement.ui.screens.category.CategoryDetailViewModel import cz.cvut.fel.pda.financemanagement.ui.screens.category.CategoryViewModel import cz.cvut.fel.pda.financemanagement.ui.screens.loans.LoanDetailViewModel @@ -24,6 +26,7 @@ object AppViewModelProvider { initializer { TransactionDetailViewModel( financemanagementApplication().container.transactionRepository, + financemanagementApplication().container.accountRepository, this.createSavedStateHandle() ) } @@ -48,10 +51,24 @@ object AppViewModelProvider { financemanagementApplication().container.loanRepository ) } - initializer { LoanDetailViewModel( - financemanagementApplication().container.loanRepository, - this.createSavedStateHandle() - ) } + initializer { + LoanDetailViewModel( + financemanagementApplication().container.loanRepository, + this.createSavedStateHandle() + ) + } + initializer { + AccountsViewModel( + financemanagementApplication().container.accountRepository, + financemanagementApplication().container.preferencesRepository + ) + } + initializer { + AccountDetailViewModel( + financemanagementApplication().container.accountRepository, + this.createSavedStateHandle() + ) + } } } -- GitLab