From e8bd7d475b4065e9e4004eb4523916c193fc4da9 Mon Sep 17 00:00:00 2001 From: ithillad Date: Thu, 19 Dec 2024 14:02:38 +0100 Subject: [PATCH] Initial commit: Add financial viewer --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 18 + .idea/gradle.xml | 19 + .idea/kotlinc.xml | 6 + .idea/migrations.xml | 10 + .idea/misc.xml | 9 + .idea/other.xml | 329 ++++++++++++++++++ app/.gitignore | 1 + app/build.gradle.kts | 54 +++ app/proguard-rules.pro | 21 ++ .../ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 49 +++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 32645 bytes app/src/main/java/com/financialviewer/App.kt | 25 ++ .../constants/BankTransactionRef.kt | 29 ++ .../com/financialviewer/constants/Category.kt | 64 ++++ .../financialviewer/constants/Comdirect.kt | 88 +++++ .../com/financialviewer/constants/Const.kt | 19 + .../com/financialviewer/constants/LoanList.kt | 49 +++ .../com/financialviewer/data/CategoryItem.kt | 6 + .../data/LoanMonthlyDetails.kt | 8 + .../com/financialviewer/data/YearMonthDay.kt | 7 + .../com/financialviewer/db/AppDatabase.kt | 37 ++ .../com/financialviewer/db/BankTransaction.kt | 17 + .../financialviewer/db/BankTransactionDao.kt | 45 +++ .../java/com/financialviewer/db/FixedCost.kt | 13 + .../com/financialviewer/db/FixedCostDao.kt | 33 ++ .../main/java/com/financialviewer/db/Loan.kt | 17 + .../java/com/financialviewer/db/LoanDao.kt | 30 ++ .../com/financialviewer/db/MonthlySummary.kt | 14 + .../financialviewer/db/MonthlySummaryDao.kt | 27 ++ .../com/financialviewer/db/YearlySummary.kt | 13 + .../financialviewer/db/YearlySummaryDao.kt | 27 ++ .../financialviewer/network/ConnectionData.kt | 10 + .../network/SmbClientHelper.kt | 123 +++++++ .../repository/BankTransactionRepository.kt | 62 ++++ .../repository/FixedCostRepository.kt | 37 ++ .../repository/LoanRepository.kt | 44 +++ .../repository/MonthlySummaryRepository.kt | 30 ++ .../repository/YearlySummaryRepository.kt | 29 ++ .../ui/BankTransactionDetailsActivity.kt | 88 +++++ .../ui/BankTransactionsActivity.kt | 184 ++++++++++ .../ui/FixedCostDetailsActivity.kt | 68 ++++ .../financialviewer/ui/FixedCostsActivity.kt | 147 ++++++++ .../com/financialviewer/ui/HomeActivity.kt | 98 ++++++ .../financialviewer/ui/LoanDetailsActivity.kt | 165 +++++++++ .../ui/LoanOverviewActivity.kt | 162 +++++++++ .../com/financialviewer/ui/LoginActivity.kt | 42 +++ .../com/financialviewer/ui/MainActivity.kt | 21 ++ .../ui/MonthlySummaryActivity.kt | 109 ++++++ .../ui/YearlySummaryActivity.kt | 110 ++++++ .../ui/adapter/BankTransactionAdapter.kt | 62 ++++ .../ui/adapter/FixedCostAdapter.kt | 52 +++ .../ui/adapter/FixedCostDetailsAdapter.kt | 59 ++++ .../ui/adapter/LoanDetailsAdapter.kt | 46 +++ .../ui/adapter/LoanOverviewAdapter.kt | 69 ++++ .../ui/adapter/MonthlySummaryAdapter.kt | 65 ++++ .../ui/adapter/YearlySummaryAdapter.kt | 58 +++ .../financialviewer/utils/BankRefParser.kt | 33 ++ .../java/com/financialviewer/utils/Common.kt | 66 ++++ .../financialviewer/utils/FormatConverter.kt | 63 ++++ .../financialviewer/utils/LoanCalculator.kt | 44 +++ .../utils/SharedPreferencesHelper.kt | 94 +++++ .../com/financialviewer/work/StartupWorker.kt | 28 ++ .../work/UpdateBankTransactions.kt | 142 ++++++++ .../financialviewer/work/UpdateFixedCosts.kt | 120 +++++++ .../com/financialviewer/work/UpdateLoans.kt | 56 +++ .../com/financialviewer/work/UpdateSummary.kt | 84 +++++ app/src/main/res/drawable/fixed_costs_24.xml | 1 + .../res/drawable/ic_launcher_background.xml | 74 ++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ .../main/res/drawable/loan_overview_24.xml | 1 + .../main/res/drawable/monthly_summary_24.xml | 1 + app/src/main/res/drawable/settings_24.xml | 1 + app/src/main/res/drawable/transaction_24.xml | 1 + .../res/drawable/update_transaction_24.xml | 1 + .../main/res/drawable/yearly_summary_24.xml | 1 + .../activity_bank_transaction_details.xml | 145 ++++++++ .../res/layout/activity_bank_transactions.xml | 141 ++++++++ .../layout/activity_fixed_cost_details.xml | 59 ++++ .../main/res/layout/activity_fixed_costs.xml | 154 ++++++++ app/src/main/res/layout/activity_home.xml | 105 ++++++ .../main/res/layout/activity_loan_details.xml | 142 ++++++++ .../res/layout/activity_loan_overview.xml | 64 ++++ app/src/main/res/layout/activity_login.xml | 36 ++ .../res/layout/activity_monthly_summary.xml | 128 +++++++ .../res/layout/activity_yearly_summary.xml | 93 +++++ .../main/res/layout/item_four_equal_texts.xml | 49 +++ .../main/res/layout/item_loan_overview.xml | 121 +++++++ .../res/layout/item_three_equal_texts.xml | 45 +++ .../main/res/layout/item_two_equal_texts.xml | 26 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 2636 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 1122 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 4424 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 2212 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 730 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 3014 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 3490 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 1484 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 6002 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 4946 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 2250 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 9192 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 6652 bytes .../ic_launcher_foreground.webp | Bin 0 -> 3118 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 12554 bytes app/src/main/res/values-night/themes.xml | 9 + app/src/main/res/values/colors.xml | 12 + app/src/main/res/values/strings.xml | 56 +++ app/src/main/res/values/themes.xml | 12 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../com/financialviewer/ExampleUnitTest.kt | 17 + build.gradle.kts | 5 + gradle.properties | 23 ++ gradle/libs.versions.toml | 35 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++++ gradlew.bat | 89 +++++ settings.gradle.kts | 24 ++ 125 files changed, 5736 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/other.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/financialviewer/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/financialviewer/App.kt create mode 100644 app/src/main/java/com/financialviewer/constants/BankTransactionRef.kt create mode 100644 app/src/main/java/com/financialviewer/constants/Category.kt create mode 100644 app/src/main/java/com/financialviewer/constants/Comdirect.kt create mode 100644 app/src/main/java/com/financialviewer/constants/Const.kt create mode 100644 app/src/main/java/com/financialviewer/constants/LoanList.kt create mode 100644 app/src/main/java/com/financialviewer/data/CategoryItem.kt create mode 100644 app/src/main/java/com/financialviewer/data/LoanMonthlyDetails.kt create mode 100644 app/src/main/java/com/financialviewer/data/YearMonthDay.kt create mode 100644 app/src/main/java/com/financialviewer/db/AppDatabase.kt create mode 100644 app/src/main/java/com/financialviewer/db/BankTransaction.kt create mode 100644 app/src/main/java/com/financialviewer/db/BankTransactionDao.kt create mode 100644 app/src/main/java/com/financialviewer/db/FixedCost.kt create mode 100644 app/src/main/java/com/financialviewer/db/FixedCostDao.kt create mode 100644 app/src/main/java/com/financialviewer/db/Loan.kt create mode 100644 app/src/main/java/com/financialviewer/db/LoanDao.kt create mode 100644 app/src/main/java/com/financialviewer/db/MonthlySummary.kt create mode 100644 app/src/main/java/com/financialviewer/db/MonthlySummaryDao.kt create mode 100644 app/src/main/java/com/financialviewer/db/YearlySummary.kt create mode 100644 app/src/main/java/com/financialviewer/db/YearlySummaryDao.kt create mode 100644 app/src/main/java/com/financialviewer/network/ConnectionData.kt create mode 100644 app/src/main/java/com/financialviewer/network/SmbClientHelper.kt create mode 100644 app/src/main/java/com/financialviewer/repository/BankTransactionRepository.kt create mode 100644 app/src/main/java/com/financialviewer/repository/FixedCostRepository.kt create mode 100644 app/src/main/java/com/financialviewer/repository/LoanRepository.kt create mode 100644 app/src/main/java/com/financialviewer/repository/MonthlySummaryRepository.kt create mode 100644 app/src/main/java/com/financialviewer/repository/YearlySummaryRepository.kt create mode 100644 app/src/main/java/com/financialviewer/ui/BankTransactionDetailsActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/BankTransactionsActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/FixedCostDetailsActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/FixedCostsActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/HomeActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/LoanDetailsActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/LoanOverviewActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/LoginActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/MainActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/MonthlySummaryActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/YearlySummaryActivity.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/BankTransactionAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/FixedCostAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/FixedCostDetailsAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/LoanDetailsAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/LoanOverviewAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/MonthlySummaryAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/ui/adapter/YearlySummaryAdapter.kt create mode 100644 app/src/main/java/com/financialviewer/utils/BankRefParser.kt create mode 100644 app/src/main/java/com/financialviewer/utils/Common.kt create mode 100644 app/src/main/java/com/financialviewer/utils/FormatConverter.kt create mode 100644 app/src/main/java/com/financialviewer/utils/LoanCalculator.kt create mode 100644 app/src/main/java/com/financialviewer/utils/SharedPreferencesHelper.kt create mode 100644 app/src/main/java/com/financialviewer/work/StartupWorker.kt create mode 100644 app/src/main/java/com/financialviewer/work/UpdateBankTransactions.kt create mode 100644 app/src/main/java/com/financialviewer/work/UpdateFixedCosts.kt create mode 100644 app/src/main/java/com/financialviewer/work/UpdateLoans.kt create mode 100644 app/src/main/java/com/financialviewer/work/UpdateSummary.kt create mode 100644 app/src/main/res/drawable/fixed_costs_24.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/loan_overview_24.xml create mode 100644 app/src/main/res/drawable/monthly_summary_24.xml create mode 100644 app/src/main/res/drawable/settings_24.xml create mode 100644 app/src/main/res/drawable/transaction_24.xml create mode 100644 app/src/main/res/drawable/update_transaction_24.xml create mode 100644 app/src/main/res/drawable/yearly_summary_24.xml create mode 100644 app/src/main/res/layout/activity_bank_transaction_details.xml create mode 100644 app/src/main/res/layout/activity_bank_transactions.xml create mode 100644 app/src/main/res/layout/activity_fixed_cost_details.xml create mode 100644 app/src/main/res/layout/activity_fixed_costs.xml create mode 100644 app/src/main/res/layout/activity_home.xml create mode 100644 app/src/main/res/layout/activity_loan_details.xml create mode 100644 app/src/main/res/layout/activity_loan_overview.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_monthly_summary.xml create mode 100644 app/src/main/res/layout/activity_yearly_summary.xml create mode 100644 app/src/main/res/layout/item_four_equal_texts.xml create mode 100644 app/src/main/res/layout/item_loan_overview.xml create mode 100644 app/src/main/res/layout/item_three_equal_texts.xml create mode 100644 app/src/main/res/layout/item_two_equal_texts.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/financialviewer/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..4f46cec --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..0897082 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..49481ad --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,329 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..b24598e --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + id("com.google.devtools.ksp") version "1.9.0-1.0.13" +} + +android { + namespace = "com.financialviewer" + compileSdk = 34 + + defaultConfig { + applicationId = "com.financialviewer" + minSdk = 31 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + implementation (libs.androidx.work.runtime.ktx) + ksp (libs.androidx.room.compiler) + implementation (libs.androidx.room.ktx) + implementation (libs.smbj) + implementation(libs.opencsv) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/financialviewer/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/financialviewer/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9b0930c --- /dev/null +++ b/app/src/androidTest/java/com/financialviewer/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.financialviewer + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.financialviewer", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5b69319 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..6ef9bbebf9130c08fa587c213bf1c8c67697180f GIT binary patch literal 32645 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 D1abDV literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/financialviewer/App.kt b/app/src/main/java/com/financialviewer/App.kt new file mode 100644 index 0000000..4ad67a6 --- /dev/null +++ b/app/src/main/java/com/financialviewer/App.kt @@ -0,0 +1,25 @@ +package com.financialviewer + +import android.app.Application +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.financialviewer.utils.SharedPreferencesHelper +import com.financialviewer.work.StartupWorker + +class App : Application() { + override fun onCreate() { + super.onCreate() + + val reset = false + + if (reset) { + this.deleteDatabase("app_database") + val appPreferences = SharedPreferencesHelper(this) + appPreferences.clearAllSharedPreferences() + } + + val workRequest = OneTimeWorkRequest.Builder(StartupWorker::class.java).build() + WorkManager.getInstance(this).enqueue(workRequest) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/constants/BankTransactionRef.kt b/app/src/main/java/com/financialviewer/constants/BankTransactionRef.kt new file mode 100644 index 0000000..b518699 --- /dev/null +++ b/app/src/main/java/com/financialviewer/constants/BankTransactionRef.kt @@ -0,0 +1,29 @@ +package com.financialviewer.constants + +val COUNTERPARTY_MAP = mapOf( + "1118 Reembroden 31-43" to Pair("Hausmann Hausverwaltung", "物业费"), + "AMERICAN EXPRESS EUROPE" to Pair("American Express", "信用卡帐"), + "Commerzbank AG" to Pair("Commerzbank AG", "房贷"), + "DKV KRANKENVERS. AG" to Pair("DKV Zahnzusatzversicherung", "牙医险"), + "Einkommensteuer" to Pair("Steuerkasse Hamburg", "补交所得税"), + "Grundsteuer" to Pair("Steuerkasse Hamburg", "房产税"), + "HAFEN UND." to Pair("HHLA", "工资"), + "Haftpflichtvers. SV95925167" to Pair("ERGO Haftpflichtversicherung", "第三责任险"), + "Hauptzollamt Hamburg" to Pair("Hauptzollamt Hamburg", "车税"), + "Hausratvers." to Pair("ERGO Hausratversicherung", "家财险"), + "KFZ-Versicherung" to Pair("HuK KFZ-Versicherung", "车险"), + "MIETE" to Pair("Reembroden 35 Miete", "房租"), + "Rechtsschutzversicherung" to Pair("ERGO Rechtsschutzversicherung", "法律险"), + "Rundfunk" to Pair("Rundfunk ARD,ZDF,DRadio", "广播电视费"), + "Stadtreinigung" to Pair("Stadtreinigung Hamburg", "垃圾费"), + "Stromnetz" to Pair("Stromnetz Hamburg", "电网"), + "Telefonica Germany GmbH" to Pair("O2 Germany", "手机费"), + "Unfallversicherung" to Pair("ERGO Unfallversicherung", "意外险"), + "VATTENFALL EUROPE SALES" to Pair("Vattenfall Europe", "电费"), + "Vodafone Deutschland GmbH" to Pair("Vodafone Kabel", "网费"), + "Vorsorge LV AG" to Pair("ERGO Lebensversicherung", "人寿险"), + "Wasserwerke" to Pair("Hamburger Wasserwerke", "水费"), + "Wohngebaeudevers." to Pair("ERGO Wohngebaeudeversicherung", "房屋险") +) + + diff --git a/app/src/main/java/com/financialviewer/constants/Category.kt b/app/src/main/java/com/financialviewer/constants/Category.kt new file mode 100644 index 0000000..94518f9 --- /dev/null +++ b/app/src/main/java/com/financialviewer/constants/Category.kt @@ -0,0 +1,64 @@ +package com.financialviewer.constants + +import com.financialviewer.data.CategoryItem + +val CATEGORY_LIST = listOf( + CategoryItem("1.收入", listOf("工资", "房租", "电网")), + CategoryItem("2.税", listOf("补交所得税", "房产税", "车税")), + CategoryItem("3.生活成本", listOf("房贷", "电费", "水费", "网费", "手机费", "垃圾费")), + CategoryItem("4.保险", listOf("第三责任险", "人寿险", "意外险", "车险", "房屋险", "家财险", "牙医险", "法律险")), + CategoryItem("5.其他", listOf("物业费", "信用卡帐", "广播电视费")), +) + +val CATEGORY_FIXED_COST_LIST = listOf( + CategoryItem("税", listOf("房产税", "车税")), + CategoryItem("生活成本", listOf("房贷", "电费", "水费", "网费", "手机费", "垃圾费")), + CategoryItem("保险", listOf("第三责任险", "人寿险", "意外险", "车险", "房屋险", "家财险", "牙医险", "法律险")), + CategoryItem("其他", listOf("物业费", "广播电视费")) +) + +const val MONTHLY = "monthly" +const val QUARTERLY = "quarterly" +const val YEARLY = "yearly" + +val FIXED_COST_TYPE = mapOf( + "房贷" to MONTHLY, + "电费" to MONTHLY, + "水费" to MONTHLY, + "网费" to MONTHLY, + "手机费" to MONTHLY, + "牙医险" to MONTHLY, + "物业费" to MONTHLY, + "房产税" to QUARTERLY, + "垃圾费" to QUARTERLY, + "广播电视费" to QUARTERLY, + "车税" to YEARLY, + "第三责任险" to YEARLY, + "人寿险" to YEARLY, + "意外险" to YEARLY, + "车险" to YEARLY, + "房屋险" to YEARLY, + "家财险" to YEARLY, + "法律险" to YEARLY +) + +val FIXED_COST_COUNT = mapOf( + "房贷" to 4, + "电费" to 2, + "水费" to 1, + "网费" to 1, + "手机费" to 1, + "牙医险" to 1, + "物业费" to 2, + "房产税" to 3, + "垃圾费" to 1, + "广播电视费" to 1, + "车税" to 1, + "第三责任险" to 1, + "人寿险" to 2, + "意外险" to 1, + "车险" to 1, + "房屋险" to 1, + "家财险" to 1, + "法律险" to 1 +) diff --git a/app/src/main/java/com/financialviewer/constants/Comdirect.kt b/app/src/main/java/com/financialviewer/constants/Comdirect.kt new file mode 100644 index 0000000..1da706a --- /dev/null +++ b/app/src/main/java/com/financialviewer/constants/Comdirect.kt @@ -0,0 +1,88 @@ +package com.financialviewer.constants + +import com.financialviewer.db.BankTransaction +import com.financialviewer.utils.convertStringToDouble + +val COMDIRECT_LIST = listOf( + BankTransaction( + "Comdirect1", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-310,83"), + "房贷", + "Commerzbank AG", + "SEPA-LASTSCHRIFT VON Commerzbank AG Comdirect1 IBAN DE79200400000504264302 BIC COBADEFF", + true), + BankTransaction( + "Comdirect2", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-852,5"), + "房贷", + "Commerzbank AG", + "SEPA-LASTSCHRIFT VON Commerzbank AG Comdirect2 IBAN DE09200400000504264301 BIC COBADEFF", + true), + BankTransaction( + "Comdirect3", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-756,25"), + "房贷", + "Commerzbank AG", + "SEPA-LASTSCHRIFT VON Commerzbank AG Comdirect3 IBAN DE09200400000504264301 BIC COBADEFF", + true), + BankTransaction( + "Comdirect4", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("9"), + "电网", + "Stromnetz Hamburg", + "ÜBERWEISUNG VON Stromnetz Hamburg GmbH Comdirect4 IBAN DE17500500000090085242 BIC HELADEFFXXX", + false), + BankTransaction( + "Comdirect5", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-65,44"), + "电费", + "Vattenfall Europe", + "SEPA-LASTSCHRIFT VON VATTENFALL EUROPE SALES Comdirect5 IBAN DE93500500000090085135 BIC HELADEFF", + true), + BankTransaction( + "Comdirect6", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-73,44"), + "电费", + "Vattenfall Europe", + "SEPA-LASTSCHRIFT VON VATTENFALL EUROPE SALES Comdirect6 IBAN DE93500500000090085135 BIC HELADEFF", + true), + BankTransaction( + "Comdirect7", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-776,87"), + "车险", + "ERGO KFZ-Versicherung", + "SEPA-LASTSCHRIFT VON ERGO VERSICHERUNG AG Comdirect7 IBAN DE67302201900004471610 BIC HYVEDEMM KFZ-Versicherung", + true), + BankTransaction( + "Comdirect8", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-20"), + "物业费", + "Hausmann Hausverwaltung", + "SEPA-LASTSCHRIFT VON WEG 1118 Reembroden 31-43 Hausmann Hausv Comdirect8 IBAN DE77217919060000773573 BIC GENODEFF", + true), + BankTransaction( + "Comdirect9", + "ComdirectAccount", + "2024-01-02", + convertStringToDouble("-325"), + "物业费", + "Hausmann Hausverwaltung", + "SEPA-LASTSCHRIFT VON WEG 1118 Reembroden 31-43 Hausmann Hausv Comdirec9 IBAN DE77217919060000773573 BIC GENODEFF", + true) +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/constants/Const.kt b/app/src/main/java/com/financialviewer/constants/Const.kt new file mode 100644 index 0000000..e07de4d --- /dev/null +++ b/app/src/main/java/com/financialviewer/constants/Const.kt @@ -0,0 +1,19 @@ +package com.financialviewer.constants + +import com.financialviewer.db.BankTransaction + +const val YEAR = 2024 +const val FIXED_COST_TIP = "该转账为固定支出: 是" +const val NON_FIXED_COST_TIP = "该转账为固定支出: 否" + +val BLANK_BANK_TRANSACTION = + BankTransaction( + "", + "", + "", + 0.0, + "", + "", + "", + false + ) diff --git a/app/src/main/java/com/financialviewer/constants/LoanList.kt b/app/src/main/java/com/financialviewer/constants/LoanList.kt new file mode 100644 index 0000000..96c0e09 --- /dev/null +++ b/app/src/main/java/com/financialviewer/constants/LoanList.kt @@ -0,0 +1,49 @@ +package com.financialviewer.constants + +import com.financialviewer.db.Loan +import java.util.Calendar + +val lastUpdate = Calendar.getInstance().timeInMillis + +val LoanList = mutableListOf( + Loan( + "R1", + "Reembroden 35 (1)", + "150.000,00", + "3,66%", + "1.168,59", + "30.08.2027", + "38.669,79", + lastUpdate + ), + Loan( + "R2", + "Reembroden 35 (2)", + "100.000,00", + "1,74%", + "310,83", + "30.11.2033", + "91.365,58", + lastUpdate + ), + Loan( + "V1", + "Voßstraat 24 (1)", + "300.000,00", + "1,42%", + "852,50", + "30.11.2028", + "274.266,59", + lastUpdate + ), + Loan( + "V2", + "Voßstraat 24 (2)", + "250.000,00", + "1,64%", + "756,25", + "30.11.2033", + "220.338,01", + lastUpdate + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/data/CategoryItem.kt b/app/src/main/java/com/financialviewer/data/CategoryItem.kt new file mode 100644 index 0000000..dee3114 --- /dev/null +++ b/app/src/main/java/com/financialviewer/data/CategoryItem.kt @@ -0,0 +1,6 @@ +package com.financialviewer.data + +data class CategoryItem ( + val title: String, + val subList: List +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/data/LoanMonthlyDetails.kt b/app/src/main/java/com/financialviewer/data/LoanMonthlyDetails.kt new file mode 100644 index 0000000..cc4541f --- /dev/null +++ b/app/src/main/java/com/financialviewer/data/LoanMonthlyDetails.kt @@ -0,0 +1,8 @@ +package com.financialviewer.data + +data class LoanMonthlyDetails( + val month: String, + val interest: String, + val principal: String, + val remaining: String +) diff --git a/app/src/main/java/com/financialviewer/data/YearMonthDay.kt b/app/src/main/java/com/financialviewer/data/YearMonthDay.kt new file mode 100644 index 0000000..1f7fa34 --- /dev/null +++ b/app/src/main/java/com/financialviewer/data/YearMonthDay.kt @@ -0,0 +1,7 @@ +package com.financialviewer.data + +data class YearMonthDay( + val year: Int, + val month: Int, + val day: Int +) diff --git a/app/src/main/java/com/financialviewer/db/AppDatabase.kt b/app/src/main/java/com/financialviewer/db/AppDatabase.kt new file mode 100644 index 0000000..1ec9500 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/AppDatabase.kt @@ -0,0 +1,37 @@ +package com.financialviewer.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [ + Loan::class, + FixedCost::class, + BankTransaction::class, + YearlySummary::class, + MonthlySummary::class], version = 1) +abstract class AppDatabase : RoomDatabase() { + abstract fun loanDao(): LoanDao + abstract fun fixedCostDao(): FixedCostDao + abstract fun bankTransactionDao(): BankTransactionDao + abstract fun yearlySummaryDao(): YearlySummaryDao + abstract fun monthlySummaryDao(): MonthlySummaryDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "app_database" + ).build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/BankTransaction.kt b/app/src/main/java/com/financialviewer/db/BankTransaction.kt new file mode 100644 index 0000000..60d627d --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/BankTransaction.kt @@ -0,0 +1,17 @@ +package com.financialviewer.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "bank_transactions") +data class BankTransaction( + @PrimaryKey val refId: String, + @ColumnInfo(name = "account") val account: String, + @ColumnInfo(name = "date") val date: String, + @ColumnInfo(name = "amount") val amount: Double, + @ColumnInfo(name = "category") val category: String, + @ColumnInfo(name = "counterparty") val counterparty: String, + @ColumnInfo(name = "reference") val reference: String, + @ColumnInfo(name = "is_fixed_cost") var isFixedCost: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/BankTransactionDao.kt b/app/src/main/java/com/financialviewer/db/BankTransactionDao.kt new file mode 100644 index 0000000..e335491 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/BankTransactionDao.kt @@ -0,0 +1,45 @@ +package com.financialviewer.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +@Dao +interface BankTransactionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertBankTransaction(bankTransaction: BankTransaction) + + @Update + suspend fun updateBankTransaction(bankTransaction: BankTransaction) + + @Query("SELECT COUNT(*) FROM bank_Transactions") + suspend fun getRowCount(): Int + + @Query("SELECT * FROM bank_Transactions WHERE refId = :refId") + suspend fun getBankTransactionById(refId: String): BankTransaction? + + @Query("SELECT * FROM bank_Transactions ORDER BY date DESC LIMIT 1") + suspend fun getLastBankTransaction(): BankTransaction? + + @Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-%' ORDER BY date DESC") + suspend fun getAllBankTransactionsDesc(year: String): List + + @Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-%' AND category = :category ORDER BY date DESC") + suspend fun getBankTransactionsByCategoryDesc(year: String, category: String): List + + @Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-%' AND category IN (:categoryList) ORDER BY date DESC") + suspend fun getBankTransactionsByCategoryListDesc(year: String, categoryList: MutableList): List + + @Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-' || :month || '%' ORDER BY date DESC") + suspend fun getBankTransactionsByMonthDesc(year: String, month: String): List + + @Query("SELECT * FROM bank_Transactions WHERE date LIKE :year || '-' || :month || '%' AND category IN (:categoryList) ORDER BY date DESC") + suspend fun getBankTransactionsByMonthAndCategoryListDesc(year: String, month: String, categoryList: MutableList): List + + @Delete + suspend fun deleteBankTransaction(bankTransaction: BankTransaction) +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/FixedCost.kt b/app/src/main/java/com/financialviewer/db/FixedCost.kt new file mode 100644 index 0000000..6985c10 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/FixedCost.kt @@ -0,0 +1,13 @@ +package com.financialviewer.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "fixed_costs") +data class FixedCost( + @PrimaryKey val category: String, + @ColumnInfo(name = "amount") var amount: Double, + @ColumnInfo(name = "type") val type: String, + @ColumnInfo(name = "refIds") var refIds: String +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/FixedCostDao.kt b/app/src/main/java/com/financialviewer/db/FixedCostDao.kt new file mode 100644 index 0000000..12635d5 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/FixedCostDao.kt @@ -0,0 +1,33 @@ +package com.financialviewer.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +@Dao +interface FixedCostDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertFixedCost(fixedCost: FixedCost) + + @Update + suspend fun updateFixedCost(fixedCost: FixedCost) + + @Query("SELECT COUNT(*) FROM fixed_costs") + suspend fun getRowCount(): Int + + @Query("SELECT * FROM fixed_costs WHERE category = :category") + suspend fun getFixedCostByCategory(category: String): FixedCost? + + @Query("SELECT * FROM fixed_costs") + suspend fun getAllFixedCosts(): List + + @Query("SELECT * FROM fixed_costs WHERE type = :type") + suspend fun getAllFixedCostsByType(type: String): List + + @Delete + suspend fun deleteFixedCost(fixedCost: FixedCost) +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/Loan.kt b/app/src/main/java/com/financialviewer/db/Loan.kt new file mode 100644 index 0000000..fac82e0 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/Loan.kt @@ -0,0 +1,17 @@ +package com.financialviewer.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "loans") +data class Loan( + @PrimaryKey val id: String, + @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "amount") val amount: String, + @ColumnInfo(name = "rate") var rate: String, + @ColumnInfo(name = "payment") var payment: String, + @ColumnInfo(name = "maturity") var maturity: String, + @ColumnInfo(name = "remaining_loan") var remainingLoan: String, + @ColumnInfo(name = "last_update") var lastUpdate: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/LoanDao.kt b/app/src/main/java/com/financialviewer/db/LoanDao.kt new file mode 100644 index 0000000..b395dd6 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/LoanDao.kt @@ -0,0 +1,30 @@ +package com.financialviewer.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +@Dao +interface LoanDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLoan(loan: Loan) + + @Update + suspend fun updateLoan(loan: Loan) + + @Query("SELECT COUNT(*) FROM loans") + suspend fun getRowCount(): Int + + @Query("SELECT * FROM loans WHERE id = :loanId") + suspend fun getLoanById(loanId: String): Loan? + + @Query("SELECT * FROM loans") + suspend fun getAllLoans(): List + + @Delete + suspend fun deleteLoan(loan: Loan) +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/MonthlySummary.kt b/app/src/main/java/com/financialviewer/db/MonthlySummary.kt new file mode 100644 index 0000000..90ef697 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/MonthlySummary.kt @@ -0,0 +1,14 @@ +package com.financialviewer.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "monthly_summary") +data class MonthlySummary( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "year") val year: Int, + @ColumnInfo(name = "month") val month: Int, + @ColumnInfo(name = "income") var income: Double, + @ColumnInfo(name = "cost") var cost: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/MonthlySummaryDao.kt b/app/src/main/java/com/financialviewer/db/MonthlySummaryDao.kt new file mode 100644 index 0000000..a677e39 --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/MonthlySummaryDao.kt @@ -0,0 +1,27 @@ +package com.financialviewer.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +@Dao +interface MonthlySummaryDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMonthlySummary(monthlySummary: MonthlySummary) + + @Update + suspend fun updateMonthlySummary(monthlySummary: MonthlySummary) + + @Query("SELECT * FROM monthly_summary WHERE year = :year") + suspend fun getMonthlySummary(year: Int): List + + @Query("SELECT * FROM monthly_summary WHERE year = :year AND month = :month") + suspend fun getMonthlySummaryByMonth(year: Int, month: Int): MonthlySummary? + + @Delete + suspend fun deleteMonthlySummary(monthlySummary: MonthlySummary) +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/YearlySummary.kt b/app/src/main/java/com/financialviewer/db/YearlySummary.kt new file mode 100644 index 0000000..ce510ad --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/YearlySummary.kt @@ -0,0 +1,13 @@ +package com.financialviewer.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "yearly_summary") +data class YearlySummary( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "year") val year: Int, + @ColumnInfo(name = "category") val category: String, + @ColumnInfo(name = "total") var total: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/db/YearlySummaryDao.kt b/app/src/main/java/com/financialviewer/db/YearlySummaryDao.kt new file mode 100644 index 0000000..1a8ce9f --- /dev/null +++ b/app/src/main/java/com/financialviewer/db/YearlySummaryDao.kt @@ -0,0 +1,27 @@ +package com.financialviewer.db + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update + +@Dao +interface YearlySummaryDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertYearlySummary(yearlySummary: YearlySummary) + + @Update + suspend fun updateYearlySummary(yearlySummary: YearlySummary) + + @Query("SELECT * FROM yearly_summary WHERE year = :year") + suspend fun getYearlySummary(year: Int): List + + @Query("SELECT * FROM yearly_summary WHERE year = :year AND category = :category") + suspend fun getYearlySummaryByCategory(year: Int, category: String): YearlySummary? + + @Delete + suspend fun deleteYearlySummary(yearlySummary: YearlySummary) +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/network/ConnectionData.kt b/app/src/main/java/com/financialviewer/network/ConnectionData.kt new file mode 100644 index 0000000..6bfafc8 --- /dev/null +++ b/app/src/main/java/com/financialviewer/network/ConnectionData.kt @@ -0,0 +1,10 @@ +package com.financialviewer.network + +object ConnectionData { + const val SHARED_FOLDER: String = "Working" + const val BANK_CSV_FOLDER: String = "Bank_CSV" + const val USERNAME: String = "halio" + const val PASSWORD: String = "zhao8888" + const val DOMAIN: String = "" + const val HOSTNAME: String = "192.168.0.10" +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/network/SmbClientHelper.kt b/app/src/main/java/com/financialviewer/network/SmbClientHelper.kt new file mode 100644 index 0000000..0751669 --- /dev/null +++ b/app/src/main/java/com/financialviewer/network/SmbClientHelper.kt @@ -0,0 +1,123 @@ +package com.financialviewer.network + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import com.financialviewer.utils.pathJoin +import com.hierynomus.msdtyp.AccessMask +import com.hierynomus.msfscc.fileinformation.FileIdBothDirectoryInformation +import com.hierynomus.mssmb2.SMB2CreateDisposition +import com.hierynomus.mssmb2.SMB2ShareAccess +import com.hierynomus.smbj.SMBClient +import com.hierynomus.smbj.auth.AuthenticationContext +import com.hierynomus.smbj.connection.Connection +import com.hierynomus.smbj.session.Session +import com.hierynomus.smbj.share.DiskShare +import com.hierynomus.smbj.share.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.io.InputStream + +class SmbClientHelper(private val context: Context) { + private lateinit var connection: Connection + private lateinit var session: Session + private lateinit var share: DiskShare + + suspend fun connect(): Boolean { + return try { + val client = SMBClient() + + // 使用 withContext 切换到 IO 线程执行连接操作 + withContext(Dispatchers.IO) { + connection = client.connect(ConnectionData.HOSTNAME) + Log.d("SmbClientHelper", "Connected to server: ${ConnectionData.HOSTNAME}") + + val ac = AuthenticationContext( + ConnectionData.USERNAME, ConnectionData.PASSWORD.toCharArray(), + ConnectionData.DOMAIN + ) + session = connection.authenticate(ac) + Log.d("SmbClientHelper", "Authenticated with username: ${ConnectionData.USERNAME}") + + share = session.connectShare(ConnectionData.SHARED_FOLDER) as DiskShare + Log.d("SmbClientHelper", "Connected to share: ${ConnectionData.SHARED_FOLDER}") + true + } + } catch (e: Exception) { + // 捕获 SMB 连接异常 + Log.e("SmbClientHelper", "Error connecting to share", e) + false + } + } + + fun renameFile(file: File, newName: String) { + + } + + fun getCSVFromShare(): List { + val allFiles = share.list(ConnectionData.BANK_CSV_FOLDER) + val csvFiles = allFiles.filter { file -> file.fileName.endsWith(".csv", ignoreCase = true) } + return csvFiles + } + + fun getCSVFile(filename: String): File { + return share.openFile( + pathJoin(ConnectionData.BANK_CSV_FOLDER, filename), + setOf(AccessMask.GENERIC_READ), + null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, + null + ) + } + + suspend fun getImagePathsFromShare(title: String, episode: String): MutableList { + val episodeFolderPath = pathJoin(ConnectionData.BANK_CSV_FOLDER, title, episode) + return withContext(Dispatchers.IO) { + var imageNames = mutableListOf() + for (item in share.list(episodeFolderPath)) { + val fileInfo = item as FileIdBothDirectoryInformation + if (fileInfo.fileName.endsWith(".jpg", true)) { + imageNames.add(fileInfo.fileName) + } + } + imageNames = imageNames.sortedBy { it.substringBefore('.').toInt() }.toMutableList() + imageNames.map { "$episodeFolderPath/$it" }.toMutableList() + } + } + + suspend fun loadImageFromShare(imagePath: String): Bitmap? { + return withContext(Dispatchers.IO) { + val file = share.openFile( + imagePath, + setOf(AccessMask.GENERIC_READ), + null, + SMB2ShareAccess.ALL, + SMB2CreateDisposition.FILE_OPEN, + null + ) + + val outputStream = ByteArrayOutputStream() + val inputStream: InputStream = file.inputStream + inputStream.copyTo(outputStream) + file.close() + + val imageData = outputStream.toByteArray() + BitmapFactory.decodeByteArray(imageData, 0, imageData.size) + } + } + + suspend fun disconnect() { + try { + withContext(Dispatchers.IO) { + share.close() + session.close() + connection.close() + } + } catch (e: Exception) { + Log.e("SmbClientHelper", "Error disconnecting", e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/repository/BankTransactionRepository.kt b/app/src/main/java/com/financialviewer/repository/BankTransactionRepository.kt new file mode 100644 index 0000000..3f9a40f --- /dev/null +++ b/app/src/main/java/com/financialviewer/repository/BankTransactionRepository.kt @@ -0,0 +1,62 @@ +package com.financialviewer.repository + +import android.content.Context +import com.financialviewer.R +import com.financialviewer.constants.YEAR +import com.financialviewer.db.AppDatabase +import com.financialviewer.db.BankTransaction + +class BankTransactionRepository(context: Context) { + private val bankTransactionDao = AppDatabase.getDatabase(context).bankTransactionDao() + private val showAllMonth: String = context.getString(R.string.show_all_month) + private val showAllCategory: String = context.getString(R.string.show_all_category) + + suspend fun insertBankTransaction(bankTransaction: BankTransaction) { + bankTransactionDao.insertBankTransaction(bankTransaction) + } + + suspend fun updateBankTransaction(bankTransaction: BankTransaction) { + bankTransactionDao.updateBankTransaction(bankTransaction) + } + + suspend fun getRowCount(): Int{ + return bankTransactionDao.getRowCount() + } + + suspend fun getBankTransactionById(refId: String): BankTransaction? { + return bankTransactionDao.getBankTransactionById(refId) + } + + suspend fun getLatestYearOfBankTransaction(): String { + val bankTransaction = bankTransactionDao.getLastBankTransaction() + return if (bankTransaction != null) { + bankTransaction.date.split("-")[0] + } else { + YEAR.toString() + } + } + + suspend fun getBankTransactionsByDateAndCategoryDesc(year: String, month: String, categoryList: MutableList): MutableList { + return if (categoryList[0].contains(showAllCategory) && month.contains(showAllMonth)) { + bankTransactionDao.getAllBankTransactionsDesc(year).toMutableList() + } else if (categoryList[0].contains(showAllCategory)) { + bankTransactionDao.getBankTransactionsByMonthDesc(year, month).toMutableList() + } else if (month.contains(showAllMonth)) { + bankTransactionDao.getBankTransactionsByCategoryListDesc(year, categoryList).toMutableList() + } else { + bankTransactionDao.getBankTransactionsByMonthAndCategoryListDesc(year, month, categoryList).toMutableList() + } + } + + suspend fun getBankTransactionsByMonth(year: String, month: String): MutableList { + return bankTransactionDao.getBankTransactionsByMonthDesc(year, month).toMutableList() + } + + suspend fun getBankTransactionsByCategory(year: String, category: String): MutableList { + return bankTransactionDao.getBankTransactionsByCategoryDesc(year, category).toMutableList() + } + + suspend fun deleteBankTransaction(bankTransaction: BankTransaction) { + bankTransactionDao.deleteBankTransaction(bankTransaction) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/repository/FixedCostRepository.kt b/app/src/main/java/com/financialviewer/repository/FixedCostRepository.kt new file mode 100644 index 0000000..d2c16ff --- /dev/null +++ b/app/src/main/java/com/financialviewer/repository/FixedCostRepository.kt @@ -0,0 +1,37 @@ +package com.financialviewer.repository + +import android.content.Context +import com.financialviewer.db.AppDatabase +import com.financialviewer.db.FixedCost + +class FixedCostRepository(context: Context) { + private val fixedCostDao = AppDatabase.getDatabase(context).fixedCostDao() + + suspend fun insertFixedCost(fixedCost: FixedCost) { + fixedCostDao.insertFixedCost(fixedCost) + } + + suspend fun updateFixedCost(fixedCost: FixedCost) { + fixedCostDao.updateFixedCost(fixedCost) + } + + suspend fun getRowCount(): Int { + return fixedCostDao.getRowCount() + } + + suspend fun getFixedCostByCategory(category: String): FixedCost? { + return fixedCostDao.getFixedCostByCategory(category) + } + + suspend fun getAllFixedCosts(): MutableList { + return fixedCostDao.getAllFixedCosts().toMutableList() + } + + suspend fun getAllFixedCostsByType(type: String): MutableList { + return fixedCostDao.getAllFixedCostsByType(type).toMutableList() + } + + suspend fun deleteFixedCost(fixedCost: FixedCost) { + fixedCostDao.deleteFixedCost(fixedCost) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/repository/LoanRepository.kt b/app/src/main/java/com/financialviewer/repository/LoanRepository.kt new file mode 100644 index 0000000..5ca1fe9 --- /dev/null +++ b/app/src/main/java/com/financialviewer/repository/LoanRepository.kt @@ -0,0 +1,44 @@ +package com.financialviewer.repository + +import android.content.Context +import com.financialviewer.constants.LoanList +import com.financialviewer.db.AppDatabase +import com.financialviewer.db.Loan +import com.financialviewer.utils.calculateRemainingLoan +import java.util.Calendar + +class LoanRepository(context: Context) { + private val loanDao = AppDatabase.getDatabase(context).loanDao() + + private suspend fun insertLoan(loan: Loan) { + loanDao.insertLoan(loan) + } + + suspend fun updateLoan(loan: Loan) { + loanDao.updateLoan(loan) + } + + suspend fun getRowCount(): Int { + return loanDao.getRowCount() + } + + suspend fun getLoanById(loanId: String): Loan? { + return loanDao.getLoanById(loanId) + } + + suspend fun getAllLoans(): MutableList { + return loanDao.getAllLoans().toMutableList() + } + + suspend fun initDB() { + for (loan in LoanList) { + insertLoan(loan) + } + } + + suspend fun updateRemainingLoan(loan: Loan, monthDiff: Int) { + loan.remainingLoan = calculateRemainingLoan(loan, monthDiff) + loan.lastUpdate = Calendar.getInstance().timeInMillis + updateLoan(loan) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/repository/MonthlySummaryRepository.kt b/app/src/main/java/com/financialviewer/repository/MonthlySummaryRepository.kt new file mode 100644 index 0000000..010da20 --- /dev/null +++ b/app/src/main/java/com/financialviewer/repository/MonthlySummaryRepository.kt @@ -0,0 +1,30 @@ +package com.financialviewer.repository + +import android.content.Context +import com.financialviewer.db.AppDatabase +import com.financialviewer.db.MonthlySummary + + +class MonthlySummaryRepository(context: Context) { + private val monthlySummaryDao = AppDatabase.getDatabase(context).monthlySummaryDao() + + suspend fun insertMonthlySummary(monthlySummary: MonthlySummary) { + monthlySummaryDao.insertMonthlySummary(monthlySummary) + } + + suspend fun updateMonthlySummary(monthlySummary: MonthlySummary) { + monthlySummaryDao.updateMonthlySummary(monthlySummary) + } + + suspend fun getMonthlySummary(year: Int): MutableList { + return monthlySummaryDao.getMonthlySummary(year).toMutableList() + } + + suspend fun getMonthlySummaryByMonth(year: Int, month: Int): MonthlySummary? { + return monthlySummaryDao.getMonthlySummaryByMonth(year, month) + } + + suspend fun deleteMonthlySummary(monthlySummary: MonthlySummary) { + monthlySummaryDao.deleteMonthlySummary(monthlySummary) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/repository/YearlySummaryRepository.kt b/app/src/main/java/com/financialviewer/repository/YearlySummaryRepository.kt new file mode 100644 index 0000000..362ffe5 --- /dev/null +++ b/app/src/main/java/com/financialviewer/repository/YearlySummaryRepository.kt @@ -0,0 +1,29 @@ +package com.financialviewer.repository + +import android.content.Context +import com.financialviewer.db.AppDatabase +import com.financialviewer.db.YearlySummary + +class YearlySummaryRepository(context: Context) { + private val yearlySummaryDao = AppDatabase.getDatabase(context).yearlySummaryDao() + + suspend fun insertYearlySummary(yearlySummary: YearlySummary) { + yearlySummaryDao.insertYearlySummary(yearlySummary) + } + + suspend fun updateYearlySummary(yearlySummary: YearlySummary) { + yearlySummaryDao.updateYearlySummary(yearlySummary) + } + + suspend fun getYearlySummary(year: Int): MutableList { + return yearlySummaryDao.getYearlySummary(year).toMutableList() + } + + suspend fun getYearlySummaryByCategory(year: Int, category: String): YearlySummary? { + return yearlySummaryDao.getYearlySummaryByCategory(year, category) + } + + suspend fun deleteYearlySummary(yearlySummary: YearlySummary) { + yearlySummaryDao.deleteYearlySummary(yearlySummary) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/BankTransactionDetailsActivity.kt b/app/src/main/java/com/financialviewer/ui/BankTransactionDetailsActivity.kt new file mode 100644 index 0000000..5e0527b --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/BankTransactionDetailsActivity.kt @@ -0,0 +1,88 @@ +package com.financialviewer.ui + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SwitchCompat +import androidx.lifecycle.lifecycleScope +import com.financialviewer.R +import com.financialviewer.constants.BLANK_BANK_TRANSACTION +import com.financialviewer.constants.FIXED_COST_TIP +import com.financialviewer.constants.FIXED_COST_TYPE +import com.financialviewer.constants.NON_FIXED_COST_TIP +import com.financialviewer.db.BankTransaction +import com.financialviewer.repository.BankTransactionRepository +import com.financialviewer.utils.convertDoubleToString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class BankTransactionDetailsActivity: AppCompatActivity() { + private lateinit var bankTransactionRepository: BankTransactionRepository + private lateinit var bankTransaction: BankTransaction + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bank_transaction_details) + + bankTransactionRepository = BankTransactionRepository(this) + + val refId = intent.getStringExtra("refId") ?: "" + val accountTextView: TextView = findViewById(R.id.account) + val dateTextView: TextView = findViewById(R.id.first) + val amountTextView: TextView = findViewById(R.id.second) + val counterpartyTextView: TextView = findViewById(R.id.third) + val referenceTextView: TextView = findViewById(R.id.reference) + val isFixedCostTextView: TextView = findViewById(R.id.isFixedCostTextView) + val switch: SwitchCompat = findViewById(R.id.isFixedCostSwitch) + + if (refId != "") { + lifecycleScope.launch { + + bankTransaction = withContext(Dispatchers.IO) { + bankTransactionRepository.getBankTransactionById(refId) ?: BLANK_BANK_TRANSACTION + } + withContext(Dispatchers.Main) { + accountTextView.text = bankTransaction.account + dateTextView.text = bankTransaction.date + amountTextView.text = convertDoubleToString(bankTransaction.amount) + counterpartyTextView.text = bankTransaction.counterparty + referenceTextView.text = bankTransaction.reference + if (FIXED_COST_TYPE.containsKey(bankTransaction.category)) { + isFixedCostTextView.visibility = View.VISIBLE + isFixedCostTextView.text = if (bankTransaction.isFixedCost) { + FIXED_COST_TIP + } else { + NON_FIXED_COST_TIP + } + switch.visibility = View.VISIBLE + switch.isChecked = bankTransaction.isFixedCost + switch.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + isFixedCostTextView.text = FIXED_COST_TIP + } else { + isFixedCostTextView.text = NON_FIXED_COST_TIP + } + } + } + + } + } + } + onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + bankTransaction.isFixedCost = switch.isChecked + bankTransactionRepository.updateBankTransaction(bankTransaction) + } + + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/BankTransactionsActivity.kt b/app/src/main/java/com/financialviewer/ui/BankTransactionsActivity.kt new file mode 100644 index 0000000..e66dd10 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/BankTransactionsActivity.kt @@ -0,0 +1,184 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Spinner +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.constants.YEAR +import com.financialviewer.db.BankTransaction +import com.financialviewer.repository.BankTransactionRepository +import com.financialviewer.ui.adapter.BankTransactionAdapter +import com.financialviewer.utils.SharedPreferencesHelper +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.utils.generateCategoryWithMainCategory +import com.financialviewer.utils.generateMonth +import com.financialviewer.utils.generateYears +import com.financialviewer.utils.getSelectedCategory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class BankTransactionsActivity: AppCompatActivity() { + private lateinit var bankTransactionRepository: BankTransactionRepository + private lateinit var bankTransactionAdapter: BankTransactionAdapter + private lateinit var bankTransactionList: MutableList + private lateinit var sumAmountTextView: TextView + private var sumAmount = 0.0 + private var yearSpinnerIsInitialized = false + private var monthSpinnerIsInitialized = false + private var categorySpinnerIsInitialized = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bank_transactions) + + bankTransactionRepository = BankTransactionRepository(this) + val appPreferences = SharedPreferencesHelper((this)) + + var year = intent.getIntExtra("year", 0).toString() + if (year == "0") { + year = appPreferences.readDefaultYear(YEAR).toString() + } + + var month = intent.getIntExtra("month", 0).toString().padStart(2, '0') + if (month == "00") { + month = getString(R.string.show_all_month) + } + + val category = intent.getStringExtra("category") ?: "" + var resultCategories: MutableList = mutableListOf() + if (category == "") { + resultCategories.add(getString(R.string.show_all_category)) + } else { + resultCategories.add(category) + } + + val recyclerView: RecyclerView = findViewById(R.id.recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + bankTransactionAdapter = BankTransactionAdapter(this, mutableListOf()) { selectedBankTransaction -> + onBankTransactionClick(selectedBankTransaction) + } + recyclerView.adapter = bankTransactionAdapter + sumAmountTextView = findViewById(R.id.sumAmountValue) + updateBankTransactionList(year, month, resultCategories) + + val yearSpinner: Spinner = findViewById(R.id.yearSpinner) + val latestYear = appPreferences.readDefaultYear(YEAR).toString() + val yearList = generateYears(latestYear) + val yearAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, yearList) + yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + yearSpinner.adapter = yearAdapter + val yearPosition = yearList.indexOf(year) + yearSpinner.setSelection(yearPosition) + yearSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val selectedItem = yearList[position] + year = selectedItem + + if (isInitialized()) { + updateBankTransactionList(year, month, resultCategories) + } + yearSpinnerIsInitialized = true + } + + override fun onNothingSelected(parent: AdapterView<*>) { + yearSpinnerIsInitialized = true + } + } + + val monthSpinner: Spinner = findViewById(R.id.monthSpinner) + val monthList = generateMonth().toMutableList() + monthList.add(0, getString(R.string.show_all_month)) + val monthAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, monthList) + monthAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + monthSpinner.adapter = monthAdapter + val monthPosition = monthList.indexOf(month) + monthSpinner.setSelection(monthPosition) + monthSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val selectedItem = monthList[position] + month = selectedItem + if (isInitialized()) { + updateBankTransactionList(year, month, resultCategories) + } + monthSpinnerIsInitialized = true + } + + override fun onNothingSelected(parent: AdapterView<*>) { + monthSpinnerIsInitialized = true + } + } + + val categorySpinner: Spinner = findViewById(R.id.categorySpinner) + val categoryList = generateCategoryWithMainCategory().toMutableList() + categoryList.add(0, getString(R.string.show_all_category)) + val categoryAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, categoryList) + categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + categorySpinner.adapter = categoryAdapter + val categoryPosition = categoryList.indexOf(category) + categorySpinner.setSelection(categoryPosition) + categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val selectedItem = categoryList[position] + resultCategories = getSelectedCategory(selectedItem) + if (isInitialized()) { + updateBankTransactionList(year, month, resultCategories) + } + categorySpinnerIsInitialized = true + } + + override fun onNothingSelected(parent: AdapterView<*>) { + categorySpinnerIsInitialized = true + } + } + + } + + private fun onBankTransactionClick(selectedBankTransaction: BankTransaction) { + val intent = Intent(this@BankTransactionsActivity, BankTransactionDetailsActivity::class.java) + intent.putExtra("refId", selectedBankTransaction.refId) + startActivity(intent) + } + + private fun updateBankTransactionList(year: String, month: String, categories: MutableList) { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + bankTransactionList = bankTransactionRepository.getBankTransactionsByDateAndCategoryDesc(year,month, + categories + ) + sumAmount = calculateSumAmount(bankTransactionList) + } + withContext(Dispatchers.Main) { + bankTransactionAdapter.updateBankTransactionList(bankTransactionList) + sumAmountTextView.text = convertDoubleToString(sumAmount) + val color = if (sumAmount > 0) { + ContextCompat.getColor(this@BankTransactionsActivity, R.color.green) + } else { + ContextCompat.getColor(this@BankTransactionsActivity, R.color.red) + } + sumAmountTextView.setTextColor(color) + } + } + } + + private fun calculateSumAmount(bankTransactionList: MutableList): Double { + var sum = 0.0 + for (bankTransaction in bankTransactionList) { + sum += bankTransaction.amount + } + return sum + } + + private fun isInitialized(): Boolean { + return yearSpinnerIsInitialized && monthSpinnerIsInitialized && categorySpinnerIsInitialized + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/FixedCostDetailsActivity.kt b/app/src/main/java/com/financialviewer/ui/FixedCostDetailsActivity.kt new file mode 100644 index 0000000..6a36fe5 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/FixedCostDetailsActivity.kt @@ -0,0 +1,68 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.constants.YEAR +import com.financialviewer.db.BankTransaction +import com.financialviewer.repository.BankTransactionRepository +import com.financialviewer.ui.adapter.FixedCostDetailsAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class FixedCostDetailsActivity : AppCompatActivity() { + private lateinit var bankTransactionRepository: BankTransactionRepository + private lateinit var sourceAdapter: FixedCostDetailsAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_fixed_cost_details) + + bankTransactionRepository = BankTransactionRepository(this) + + val refIds = intent.getStringExtra("refIds") ?: "" + val category = intent.getStringExtra("category") ?: "" + val refIdList = refIds.split(",") + + val sourceRecyclerView: RecyclerView = findViewById(R.id.sourceRecyclerView) + sourceRecyclerView.layoutManager = LinearLayoutManager(this) + + val currentSourceList: MutableList = mutableListOf() + val allSourceList: MutableList = mutableListOf() + + lifecycleScope.launch { + withContext(Dispatchers.IO) { + for (refId in refIdList) { + val bankTransaction = bankTransactionRepository.getBankTransactionById(refId) + if (bankTransaction != null) { + currentSourceList.add(bankTransaction) + } + } + allSourceList.addAll( + bankTransactionRepository.getBankTransactionsByCategory(YEAR.toString(), category)) + allSourceList.addAll( + bankTransactionRepository.getBankTransactionsByCategory((YEAR - 1).toString(), category)) + } + + withContext(Dispatchers.Main) { + sourceAdapter = + FixedCostDetailsAdapter(this@FixedCostDetailsActivity, allSourceList, currentSourceList) { selectedBankTransaction -> + onBankTransactionClick(selectedBankTransaction) + } + sourceRecyclerView.adapter = sourceAdapter + } + + } + } + + private fun onBankTransactionClick(selectedBankTransaction: BankTransaction) { + val intent = Intent(this@FixedCostDetailsActivity, BankTransactionDetailsActivity::class.java) + intent.putExtra("refId", selectedBankTransaction.refId) + startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/FixedCostsActivity.kt b/app/src/main/java/com/financialviewer/ui/FixedCostsActivity.kt new file mode 100644 index 0000000..2b63ec2 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/FixedCostsActivity.kt @@ -0,0 +1,147 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import android.widget.Button +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.constants.MONTHLY +import com.financialviewer.constants.QUARTERLY +import com.financialviewer.constants.YEAR +import com.financialviewer.db.FixedCost +import com.financialviewer.repository.FixedCostRepository +import com.financialviewer.ui.adapter.FixedCostAdapter +import com.financialviewer.utils.SharedPreferencesHelper +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.work.UpdateFixedCosts +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +class FixedCostsActivity : AppCompatActivity() { + private lateinit var fixedCostRepository: FixedCostRepository + private lateinit var monthlyAdapter: FixedCostAdapter + private lateinit var quarterlyAdapter: FixedCostAdapter + private lateinit var yearlyAdapter: FixedCostAdapter + + private var year = YEAR + private val monthlyList: MutableList = mutableListOf() + private val quarterlyList: MutableList = mutableListOf() + private val yearlyList: MutableList = mutableListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_fixed_costs) + + fixedCostRepository = FixedCostRepository(this) + + val appPreferences = SharedPreferencesHelper((this)) + year = appPreferences.readDefaultYear(YEAR) + + val lastUpdateTextView: TextView = findViewById(R.id.lastUpdateValue) + lastUpdateTextView.text = appPreferences.readCalculateFixedCostTime() + + val monthlyRecyclerView: RecyclerView = findViewById(R.id.monthlyRecyclerView) + monthlyRecyclerView.layoutManager = LinearLayoutManager(this) + + val quarterlyRecyclerView: RecyclerView = findViewById(R.id.quarterlyRecyclerView) + quarterlyRecyclerView.layoutManager = LinearLayoutManager(this) + + val yearlyRecyclerView: RecyclerView = findViewById(R.id.yearlyRecyclerView) + yearlyRecyclerView.layoutManager = LinearLayoutManager(this) + + val monthlyFixedCostTextView: TextView = findViewById(R.id.monthlyFixedCostValue) + val yearlyFixedCostTextView: TextView = findViewById(R.id.yearlyFixedCostValue) + + lifecycleScope.launch { + withContext(Dispatchers.IO) { + generateFixedCostList() + } + + withContext(Dispatchers.Main) { + monthlyAdapter = FixedCostAdapter(this@FixedCostsActivity, monthlyList) { monthlyFixedCost -> + onFixedCostClick(monthlyFixedCost) + } + monthlyRecyclerView.adapter = monthlyAdapter + + quarterlyAdapter = FixedCostAdapter(this@FixedCostsActivity, quarterlyList) { quarterlyFixedCost -> + onFixedCostClick(quarterlyFixedCost) + } + quarterlyRecyclerView.adapter = quarterlyAdapter + + yearlyAdapter = FixedCostAdapter(this@FixedCostsActivity, yearlyList) { yearlyFixedCost -> + onFixedCostClick(yearlyFixedCost) + } + yearlyRecyclerView.adapter = yearlyAdapter + + val monthlyFixedCost = calculateMonthlyFixedCost() + monthlyFixedCostTextView.text = convertDoubleToString(monthlyFixedCost) + yearlyFixedCostTextView.text = convertDoubleToString(monthlyFixedCost * 12) + } + } + + val recalculateButton: Button = findViewById(R.id.recalculateButton) + recalculateButton.setOnClickListener { + val updateFixedCost = UpdateFixedCosts(this) + lifecycleScope.launch { + withContext(Dispatchers.IO) { + updateFixedCost.updateDB() + } + } + } + } + + private suspend fun generateFixedCostList() { + val fixedCostList = fixedCostRepository.getAllFixedCosts() + for (fixedCost in fixedCostList) { + when (fixedCost.type) { + MONTHLY -> { + monthlyList.add(fixedCost) + } + QUARTERLY -> { + quarterlyList.add(fixedCost) + } + else -> { + yearlyList.add(fixedCost) + } + } + } + } + + private fun calculateMonthlyFixedCost(): Double { + var monthlySum = BigDecimal(0.0) + var quarterlySum = BigDecimal(0.0) + var yearlySum = BigDecimal(0.0) + + for (monthly in monthlyList) { + monthlySum += BigDecimal(monthly.amount) + } + + for (quarterly in quarterlyList) { + quarterlySum += BigDecimal(quarterly.amount) + } + + for (yearly in yearlyList) { + yearlySum += BigDecimal(yearly.amount) + } + + return (monthlySum + + (quarterlySum / BigDecimal("3")) + + (yearlySum / BigDecimal("12"))) + .toDouble() + } + + + private fun onFixedCostClick(fixedCost: FixedCost) { + val intent = Intent(this@FixedCostsActivity, FixedCostDetailsActivity::class.java) + intent.putExtra("refIds", fixedCost.refIds) + intent.putExtra("category", fixedCost.category) + startActivity(intent) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/HomeActivity.kt b/app/src/main/java/com/financialviewer/ui/HomeActivity.kt new file mode 100644 index 0000000..0b4fafc --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/HomeActivity.kt @@ -0,0 +1,98 @@ +package com.financialviewer.ui + +import android.app.AlertDialog +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.financialviewer.R +import com.financialviewer.work.UpdateBankTransactions +import kotlinx.coroutines.launch + +class HomeActivity: AppCompatActivity() { + private lateinit var progressBarContainer: LinearLayout + private var isProgressing = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_home) + + progressBarContainer = findViewById(R.id.progressBarContainer) + + val loanOverviewTextView : TextView =findViewById(R.id.loanOverview) + loanOverviewTextView.setOnClickListener { + if (!isProgressing) { + val intent = Intent(this@HomeActivity, LoanOverviewActivity::class.java) + startActivity(intent) + } + } + + val fixedCostsTextView : TextView =findViewById(R.id.fixedCosts) + fixedCostsTextView.setOnClickListener { + if (!isProgressing) { + val intent = Intent(this@HomeActivity, FixedCostsActivity::class.java) + startActivity(intent) + } + } + + val bankTransactionTextView : TextView =findViewById(R.id.bankTransaction) + bankTransactionTextView.setOnClickListener { + if (!isProgressing) { + val intent = Intent(this@HomeActivity, BankTransactionsActivity::class.java) + startActivity(intent) + } + } + + val monthlySummaryTextView : TextView =findViewById(R.id.monthlySummary) + monthlySummaryTextView.setOnClickListener { + if (!isProgressing) { + val intent = Intent(this@HomeActivity, MonthlySummaryActivity::class.java) + startActivity(intent) + } + } + + val yearlySummaryTextView : TextView =findViewById(R.id.yearlySummary) + yearlySummaryTextView.setOnClickListener { + if (!isProgressing) { + val intent = Intent(this@HomeActivity, YearlySummaryActivity::class.java) + startActivity(intent) + } + } + + val updateTransactionTextView : TextView =findViewById(R.id.updateTransaction) + updateTransactionTextView.setOnClickListener { + if (!isProgressing) { + AlertDialog.Builder(this) + .setTitle("确认更新") + .setMessage("是否确定更新银行流水?") + .setPositiveButton("确定") { dialog, _ -> + showProgressBar() + + lifecycleScope.launch { + val updateBankTransaction = UpdateBankTransactions(this@HomeActivity) + updateBankTransaction.updateDB() + hideProgressBar() + dialog.dismiss() + } + } + .setNegativeButton("取消") { dialog, _ -> + dialog.dismiss() + } + .show() + } + } + } + + private fun showProgressBar() { + progressBarContainer.visibility = View.VISIBLE + isProgressing = true + } + + private fun hideProgressBar() { + progressBarContainer.visibility = View.GONE + isProgressing = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/LoanDetailsActivity.kt b/app/src/main/java/com/financialviewer/ui/LoanDetailsActivity.kt new file mode 100644 index 0000000..48c4d46 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/LoanDetailsActivity.kt @@ -0,0 +1,165 @@ +package com.financialviewer.ui + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.icu.util.Calendar +import android.os.Bundle +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.data.LoanMonthlyDetails +import com.financialviewer.db.Loan +import com.financialviewer.repository.LoanRepository +import com.financialviewer.ui.adapter.LoanDetailsAdapter +import com.financialviewer.utils.calculateNextInterest +import com.financialviewer.utils.calculateNextPrincipal +import com.financialviewer.utils.calculateNextRemaining +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.utils.convertInputStringToDouble +import com.financialviewer.utils.convertStringToDouble +import com.financialviewer.utils.getNextMonth +import kotlinx.coroutines.launch + +class LoanDetailsActivity: AppCompatActivity() { + private lateinit var loanRepository: LoanRepository + private lateinit var loanDetailsAdapter: LoanDetailsAdapter + private var remainingUpdated: Boolean = false + private var sumInterest: Double = 0.0 + + @SuppressLint("SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_loan_details) + + loanRepository = LoanRepository(this) + + val loanId = intent.getStringExtra("loanId") ?: "R1" + + val titleTextView: TextView = findViewById(R.id.loanTitle) + val paymentTextView: TextView = findViewById(R.id.payment) + val remainingTextView: TextView = findViewById(R.id.remaining) + val rateTextView: TextView = findViewById(R.id.rate) + val sumInterestTextView: TextView = findViewById(R.id.sumInterest) + + lifecycleScope.launch { + val loan = loanRepository.getLoanById(loanId) + if (loan != null) { + titleTextView.text = loan.title + remainingTextView.text = "当前欠款:" + loan.remainingLoan + paymentTextView.text = "月供: " + loan.payment + rateTextView.text = "利率:" + loan.rate + + val recyclerView: RecyclerView = findViewById(R.id.recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this@LoanDetailsActivity) + loanDetailsAdapter = LoanDetailsAdapter(generateMonthlyDetails(loan)) + recyclerView.adapter = loanDetailsAdapter + + sumInterestTextView.text = "预计还需支付利息:" + convertDoubleToString(sumInterest) + } + } + + val refreshButton: Button = findViewById(R.id.refresh) + refreshButton.setOnClickListener { + lifecycleScope.launch { + val loan = loanRepository.getLoanById(loanId) + if (loan != null) { + loanDetailsAdapter.updateLoanDetailsList(generateMonthlyDetails(loan)) + sumInterestTextView.text = "预计还需支付利息:" + convertDoubleToString(sumInterest) + } + } + } + + val specialRepaymentButton: Button = findViewById(R.id.specialRepayment) + specialRepaymentButton.setOnClickListener { + // 创建一个EditText作为输入框 + val input = EditText(this@LoanDetailsActivity) + + // 创建并显示AlertDialog + val dialog = AlertDialog.Builder(this@LoanDetailsActivity) + .setTitle("提前还款金额") + .setView(input) // 将EditText添加到对话框 + .setPositiveButton("Confirm") { _, _ -> + // 获取用户输入并更新TextView的内容 + val inputString = input.text.toString() + try { + val inputDouble = convertInputStringToDouble(inputString) // 成功转换 + lifecycleScope.launch { + val loan = loanRepository.getLoanById(loanId) + + if (loan != null) { + val newRemainingLoan = convertStringToDouble(loan.remainingLoan) - inputDouble + remainingTextView.text = "当前欠款:" + convertDoubleToString(newRemainingLoan) + + loan.remainingLoan = convertDoubleToString(newRemainingLoan) + loanRepository.updateLoan(loan) + remainingUpdated = true + } + } + } catch (e: NumberFormatException) { + // 如果输入不是有效的数字,弹出错误提示 + Toast.makeText(this@LoanDetailsActivity, "Invalid input. Please enter a valid number.", Toast.LENGTH_SHORT).show() + } + + } + .setNegativeButton("Cancel", null) + .create() + + dialog.show() + } + + onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (remainingUpdated) { + val resultIntent = Intent().apply { + putExtra("loanId", loanId) + } + setResult(Activity.RESULT_OK, resultIntent) + } + finish() + } + }) + } + + private fun generateMonthlyDetails(loan: Loan): MutableList { + sumInterest = 0.0 + val monthlyDetails: MutableList = mutableListOf() + val now = Calendar.getInstance() + var currentYear = now.get(Calendar.YEAR) + var currentMonth = now.get(Calendar.MONTH) + 1 + var base = convertStringToDouble(loan.remainingLoan) + var remaining = 1.0 + while (remaining > 0) { + val month = getNextMonth(currentYear, currentMonth) + currentYear = month.split(".")[1].toInt() + currentMonth = month.split(".")[0].toInt() + + val interest = calculateNextInterest(loan, base) + var principal = calculateNextPrincipal(loan, base) + remaining = calculateNextRemaining(loan, base) + + if (remaining < 0) { + remaining = 0.0 + principal = base + } + base = remaining + + monthlyDetails.add(LoanMonthlyDetails( + month, + convertDoubleToString(interest), + convertDoubleToString(principal), + convertDoubleToString(remaining) + )) + sumInterest += interest + } + return monthlyDetails + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/LoanOverviewActivity.kt b/app/src/main/java/com/financialviewer/ui/LoanOverviewActivity.kt new file mode 100644 index 0000000..b681798 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/LoanOverviewActivity.kt @@ -0,0 +1,162 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.Loan +import com.financialviewer.repository.LoanRepository +import com.financialviewer.ui.adapter.LoanOverviewAdapter +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.utils.convertInputStringToDouble +import com.financialviewer.utils.convertPercentageStringToDouble +import com.financialviewer.utils.convertPercentageToString +import com.financialviewer.utils.convertStringToDouble +import com.financialviewer.utils.convertStringToGermanDate +import kotlinx.coroutines.launch +import java.time.format.DateTimeParseException + +class LoanOverviewActivity : AppCompatActivity() { + private lateinit var loanRepository: LoanRepository + private lateinit var recyclerView: RecyclerView + private var loansInDB: MutableList = mutableListOf() + + private val startForResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + val data: Intent? = result.data + val loanId = data?.getStringExtra("loanId") + loanId?.let { + // 在当前 Activity 的协程作用域中调用 refreshData + lifecycleScope.launch { + refresh(it) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_loan_overview) + + loanRepository = LoanRepository(this) + recyclerView = findViewById(R.id.recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + + var sumLoan = 0.0 + var sumPayment = 0.0 + + lifecycleScope.launch { + loansInDB = loanRepository.getAllLoans() + val adapter = LoanOverviewAdapter( + loansInDB, + onEditClick = { loan, field, value, position -> + // 处理编辑点击事件,例如弹出对话框 + showEditDialog(loan, field, value, position) + }, + onTitleClick = { loan -> + // 处理标题点击事件,例如显示详细信息 + showLoanDetails(loan) + } + ) + recyclerView.adapter = adapter + + for (loan in loansInDB) { + sumLoan += convertStringToDouble(loan.remainingLoan) + sumPayment += convertStringToDouble(loan.payment) + } + val sumLoanValueTextView: TextView = findViewById(R.id.sumLoanValue) + sumLoanValueTextView.text = convertDoubleToString(sumLoan) + + val sumPaymentValueTextView: TextView = findViewById(R.id.sumPaymentValue) + sumPaymentValueTextView.text = convertDoubleToString(sumPayment) + } + } + + private fun showLoanDetails(loan: Loan) { + val intent = Intent(this@LoanOverviewActivity, LoanDetailsActivity::class.java) + intent.putExtra("loanId", loan.id) + startForResult.launch(intent) + + } + + private fun showEditDialog(loan: Loan, field: String, value: String, position: Int) { + val builder = AlertDialog.Builder(this) + builder.setTitle("Edit $field") + + val input = EditText(this) + input.setText(value) + builder.setView(input) + + builder.setPositiveButton("OK") { dialog, _ -> + val inputString = input.text.toString() + try { + // 更新字段值 + when (field) { + "rate" -> { + val validValue = convertPercentageStringToDouble(inputString) + loan.rate = convertPercentageToString(validValue) + } + "payment" -> { + val validValue = convertInputStringToDouble(inputString) + loan.payment = convertDoubleToString(validValue) + } + "maturity" -> { + val validValue = convertStringToGermanDate(inputString) + loan.maturity = validValue + } + "remainingLoan" -> { + val validValue = convertInputStringToDouble(inputString) + loan.remainingLoan = convertDoubleToString(validValue) + } + } + + // 更新数据库中的数据 + lifecycleScope.launch { + loanRepository.updateLoan(loan) + runOnUiThread { + recyclerView.adapter?.notifyItemChanged(position) + } + } + } catch (e: NumberFormatException) { + // 如果输入不是有效的数字,弹出错误提示 + Toast.makeText(this@LoanOverviewActivity, "Invalid input. Please enter a valid number.", Toast.LENGTH_SHORT).show() + } catch (e: DateTimeParseException) { + // 如果输入不是有效的数字,弹出错误提示 + Toast.makeText(this@LoanOverviewActivity, "Invalid input. Please enter a valid date.", Toast.LENGTH_SHORT).show() + } + + dialog.dismiss() + } + + builder.setNegativeButton("Cancel") { dialog, _ -> + dialog.cancel() + } + + builder.show() + } + + private fun refresh(loanId: String) { + val position = loansInDB.indexOfFirst { it.id == loanId } + if (position != -1) { + lifecycleScope.launch { + val newLoan = loanRepository.getLoanById(loanId) + if (newLoan != null) { + loansInDB[position] = newLoan + runOnUiThread { + recyclerView.adapter?.notifyItemChanged(position) + } + } + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/LoginActivity.kt b/app/src/main/java/com/financialviewer/ui/LoginActivity.kt new file mode 100644 index 0000000..67bad9b --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/LoginActivity.kt @@ -0,0 +1,42 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import android.widget.Button +import android.widget.EditText +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.financialviewer.R +import com.financialviewer.utils.SharedPreferencesHelper + +class LoginActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_login) + + val password: EditText = findViewById(R.id.password) + + // 处理登录逻辑 + val loginButton: Button = findViewById(R.id.button) + loginButton.setOnClickListener { + val enteredPassword = password.text.toString() + if (enteredPassword.isEmpty()) { + Toast.makeText(this, "请输入密码", Toast.LENGTH_SHORT).show() + } else { + // 处理登录逻辑,例如验证密码 + if (enteredPassword == "3713") { + val appPreferences = SharedPreferencesHelper((this)) + appPreferences.saveAuthStatus("is_authenticated", true) + + // 跳转到主页面 + val intent = Intent(this, HomeActivity::class.java) + startActivity(intent) + finish() + } else { + Toast.makeText(this, "密码错误", Toast.LENGTH_SHORT).show() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/MainActivity.kt b/app/src/main/java/com/financialviewer/ui/MainActivity.kt new file mode 100644 index 0000000..d755187 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/MainActivity.kt @@ -0,0 +1,21 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.financialviewer.utils.SharedPreferencesHelper + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val appPreferences = SharedPreferencesHelper((this)) + appPreferences.saveAuthStatus("is_authenticated", false) + + val intent = Intent(this, LoginActivity::class.java) + startActivity(intent) + finish() + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/MonthlySummaryActivity.kt b/app/src/main/java/com/financialviewer/ui/MonthlySummaryActivity.kt new file mode 100644 index 0000000..6517e7c --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/MonthlySummaryActivity.kt @@ -0,0 +1,109 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Spinner +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.constants.YEAR +import com.financialviewer.db.MonthlySummary +import com.financialviewer.repository.MonthlySummaryRepository +import com.financialviewer.ui.adapter.MonthlySummaryAdapter +import com.financialviewer.utils.SharedPreferencesHelper +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.utils.generateYears +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +class MonthlySummaryActivity : AppCompatActivity() { + private lateinit var monthlySummaryRepository: MonthlySummaryRepository + private lateinit var monthlySummaryAdapter: MonthlySummaryAdapter + private lateinit var monthlySummaryList: MutableList + + private lateinit var incomeSumTextView: TextView + private lateinit var costSumTextView: TextView + + private var year = YEAR + private var yearSpinnerIsInitialized = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_monthly_summary) + + monthlySummaryRepository = MonthlySummaryRepository(this) + val appPreferences = SharedPreferencesHelper((this)) + + val recyclerView: RecyclerView = findViewById(R.id.recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + + monthlySummaryAdapter = MonthlySummaryAdapter(this, mutableListOf()) { selectedMonthlySummary -> + onMonthlySummaryClick(selectedMonthlySummary) + } + recyclerView.adapter = monthlySummaryAdapter + + incomeSumTextView = findViewById(R.id.sumIncomeValue) + costSumTextView = findViewById(R.id.sumCostValue) + + updateYearlySummaryList() + + year = appPreferences.readDefaultYear(YEAR) + val yearSpinner: Spinner = findViewById(R.id.yearSpinner) + val yearList = generateYears(year.toString()) + val yearAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, yearList) + yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + yearSpinner.adapter = yearAdapter + yearSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val selectedItem = yearList[position] + year = selectedItem.toInt() + if (isInitialized()) { + updateYearlySummaryList() + } + yearSpinnerIsInitialized = true + } + + override fun onNothingSelected(parent: AdapterView<*>) { + yearSpinnerIsInitialized = true + } + } + } + + private fun updateYearlySummaryList() { + lifecycleScope.launch { + var incomeSum = BigDecimal("0.0") + var costSum = BigDecimal("0.0") + withContext(Dispatchers.IO) { + monthlySummaryList = monthlySummaryRepository.getMonthlySummary(year) + for (monthlySummary in monthlySummaryList) { + incomeSum += BigDecimal(monthlySummary.income.toString()) + costSum += BigDecimal(monthlySummary.cost.toString()) + } + } + withContext(Dispatchers.Main) { + monthlySummaryAdapter.updateMonthlySummaryList(monthlySummaryList) + incomeSumTextView.text = convertDoubleToString(incomeSum.toDouble()) + costSumTextView.text = convertDoubleToString(costSum.toDouble()) + } + } + } + + private fun onMonthlySummaryClick(monthlySummary: MonthlySummary) { + val intent = Intent(this@MonthlySummaryActivity, BankTransactionsActivity::class.java) + intent.putExtra("year", monthlySummary.year) + intent.putExtra("month", monthlySummary.month) + startActivity(intent) + } + + private fun isInitialized(): Boolean { + return yearSpinnerIsInitialized + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/YearlySummaryActivity.kt b/app/src/main/java/com/financialviewer/ui/YearlySummaryActivity.kt new file mode 100644 index 0000000..1a44628 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/YearlySummaryActivity.kt @@ -0,0 +1,110 @@ +package com.financialviewer.ui + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Spinner +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.constants.YEAR +import com.financialviewer.db.YearlySummary +import com.financialviewer.repository.YearlySummaryRepository +import com.financialviewer.ui.adapter.YearlySummaryAdapter +import com.financialviewer.utils.SharedPreferencesHelper +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.utils.generateYears +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +class YearlySummaryActivity : AppCompatActivity() { + private lateinit var yearlySummaryRepository: YearlySummaryRepository + private lateinit var yearlySummaryAdapter: YearlySummaryAdapter + private lateinit var yearlySummaryList: MutableList + + private lateinit var sumAmountTextView: TextView + private var year = YEAR + private var yearSpinnerIsInitialized = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_yearly_summary) + + yearlySummaryRepository = YearlySummaryRepository(this) + val appPreferences = SharedPreferencesHelper((this)) + + val recyclerView: RecyclerView = findViewById(R.id.recyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + + yearlySummaryAdapter = YearlySummaryAdapter(this, mutableListOf()) { selectedYearlySummary -> + onYearlySummaryClick(selectedYearlySummary) + } + recyclerView.adapter = yearlySummaryAdapter + + sumAmountTextView = findViewById(R.id.sumAmountValue) + updateYearlySummaryList() + + year = appPreferences.readDefaultYear(YEAR) + val yearSpinner: Spinner = findViewById(R.id.yearSpinner) + val yearList = generateYears(year.toString()) + val yearAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, yearList) + yearAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + yearSpinner.adapter = yearAdapter + yearSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + val selectedItem = yearList[position] + year = selectedItem.toInt() + if (isInitialized()) { + updateYearlySummaryList() + } + yearSpinnerIsInitialized = true + } + + override fun onNothingSelected(parent: AdapterView<*>) { + yearSpinnerIsInitialized = true + } + } + } + + private fun updateYearlySummaryList() { + lifecycleScope.launch { + var sum = BigDecimal("0.0") + withContext(Dispatchers.IO) { + yearlySummaryList = yearlySummaryRepository.getYearlySummary(year) + for (yearlySummary in yearlySummaryList) { + sum += BigDecimal(yearlySummary.total.toString()) + } + } + withContext(Dispatchers.Main) { + yearlySummaryAdapter.updateYearlySummaryList(yearlySummaryList) + sumAmountTextView.text = convertDoubleToString(sum.toDouble()) + + val color = if (sum.toDouble() > 0) { + ContextCompat.getColor(this@YearlySummaryActivity, R.color.green) + } else { + ContextCompat.getColor(this@YearlySummaryActivity, R.color.red) + } + sumAmountTextView.setTextColor(color) + } + } + } + + private fun onYearlySummaryClick(yearlySummary: YearlySummary) { + val intent = Intent(this@YearlySummaryActivity, BankTransactionsActivity::class.java) + intent.putExtra("year", yearlySummary.year) + intent.putExtra("category", yearlySummary.category) + startActivity(intent) + } + + private fun isInitialized(): Boolean { + return yearSpinnerIsInitialized + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/BankTransactionAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/BankTransactionAdapter.kt new file mode 100644 index 0000000..631e406 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/BankTransactionAdapter.kt @@ -0,0 +1,62 @@ +package com.financialviewer.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.BankTransaction +import com.financialviewer.utils.convertDoubleToString +import com.financialviewer.utils.convertStandardToGermanDate + +class BankTransactionAdapter ( + private val context: Context, + private val bankTransactionList: MutableList, + private val onItemClick: (BankTransaction) -> Unit, +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val date: TextView = itemView.findViewById(R.id.first) + val category: TextView = itemView.findViewById(R.id.second) + val amount: TextView = itemView.findViewById(R.id.third) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_three_equal_texts, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val bankTransaction = bankTransactionList[position] + val dateParts = convertStandardToGermanDate(bankTransaction.date).split(".") + holder.date.text = "${dateParts[0]}.${dateParts[1]}" + holder.category.text = bankTransaction.category + holder.amount.text = convertDoubleToString(bankTransaction.amount) + + val color = if (bankTransaction.amount > 0) { + ContextCompat.getColor(context, R.color.green) + } else { + ContextCompat.getColor(context, R.color.red) + } + holder.amount.setTextColor(color) + + holder.itemView.setOnClickListener { + onItemClick(bankTransaction) + } + } + + override fun getItemCount() = bankTransactionList.size + + private fun initBankTransactionList() { + bankTransactionList.clear() + } + + fun updateBankTransactionList(newList: List) { + initBankTransactionList() + bankTransactionList.addAll(newList) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/FixedCostAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/FixedCostAdapter.kt new file mode 100644 index 0000000..4bf8e12 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/FixedCostAdapter.kt @@ -0,0 +1,52 @@ +package com.financialviewer.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.FixedCost +import com.financialviewer.utils.convertDoubleToString + +class FixedCostAdapter ( + private val context: Context, + private val fixedCostList: MutableList, + private val onItemClick: (FixedCost) -> Unit, +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val category: TextView = itemView.findViewById(R.id.first) + val amount: TextView = itemView.findViewById(R.id.second) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_two_equal_texts, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val fixedCost = fixedCostList[position] + holder.category.text = fixedCost.category + holder.amount.text = convertDoubleToString(fixedCost.amount) + holder.amount.setTextColor(ContextCompat.getColor(context, R.color.red)) + + holder.itemView.setOnClickListener { + onItemClick(fixedCost) + } + } + + override fun getItemCount() = fixedCostList.size + + private fun initBankTransactionList() { + fixedCostList.clear() + } + + fun updateFixedCostList(newList: List) { + initBankTransactionList() + fixedCostList.addAll(newList) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/FixedCostDetailsAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/FixedCostDetailsAdapter.kt new file mode 100644 index 0000000..91a8b0e --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/FixedCostDetailsAdapter.kt @@ -0,0 +1,59 @@ +package com.financialviewer.ui.adapter + +import android.content.Context +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.BankTransaction +import com.financialviewer.utils.convertDoubleToString + +class FixedCostDetailsAdapter ( + private val context: Context, + private val allSourceList: MutableList, + private val currentSourceList: MutableList, + private val onItemClick: (BankTransaction) -> Unit, +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val date: TextView = itemView.findViewById(R.id.first) + val category: TextView = itemView.findViewById(R.id.second) + val amount: TextView = itemView.findViewById(R.id.third) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_three_equal_texts, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val bankTransaction = allSourceList[position] + holder.date.text = bankTransaction.date + holder.category.text = bankTransaction.category + holder.amount.text = convertDoubleToString(bankTransaction.amount) + holder.amount.setTextColor(ContextCompat.getColor(context, R.color.red)) + + if (currentSourceList.contains(bankTransaction)) { + holder.date.setTypeface(null, Typeface.BOLD) + holder.category.setTypeface(null, Typeface.BOLD) + holder.amount.setTypeface(null, Typeface.BOLD) + } + + if (!bankTransaction.isFixedCost) { + holder.date.setTypeface(null, Typeface.ITALIC) + holder.category.setTypeface(null, Typeface.ITALIC) + holder.amount.setTypeface(null, Typeface.ITALIC) + } + + holder.itemView.setOnClickListener { + onItemClick(bankTransaction) + } + } + + override fun getItemCount() = allSourceList.size + +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/LoanDetailsAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/LoanDetailsAdapter.kt new file mode 100644 index 0000000..a0d75dd --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/LoanDetailsAdapter.kt @@ -0,0 +1,46 @@ +package com.financialviewer.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.data.LoanMonthlyDetails + +class LoanDetailsAdapter ( + private val loanDetailsList: MutableList +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val monthText: TextView = itemView.findViewById(R.id.first) + val interestText: TextView = itemView.findViewById(R.id.second) + val principalText: TextView = itemView.findViewById(R.id.third) + val remainingDebtText: TextView = itemView.findViewById(R.id.fourth) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_four_equal_texts, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val loanDetails = loanDetailsList[position] + holder.monthText.text = loanDetails.month + holder.interestText.text = loanDetails.interest + holder.principalText.text = loanDetails.principal + holder.remainingDebtText.text = loanDetails.remaining + } + + override fun getItemCount() = loanDetailsList.size + + private fun initLoanDetailsList() { + loanDetailsList.clear() + } + + fun updateLoanDetailsList(newList: List) { + initLoanDetailsList() + loanDetailsList.addAll(newList) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/LoanOverviewAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/LoanOverviewAdapter.kt new file mode 100644 index 0000000..1a35873 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/LoanOverviewAdapter.kt @@ -0,0 +1,69 @@ +package com.financialviewer.ui.adapter + +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.Loan + +class LoanOverviewAdapter ( + private val loans: MutableList, + private val onTitleClick: (Loan) -> Unit, + private val onEditClick: (Loan, String, String, Int) -> Unit + ) : RecyclerView.Adapter() { + + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val title: TextView = itemView.findViewById(R.id.loanTitle) + val amount: TextView = itemView.findViewById(R.id.loanAmount) + val rate: TextView = itemView.findViewById(R.id.loanRate) + val payment: TextView = itemView.findViewById(R.id.loanPayment) + val maturity: TextView = itemView.findViewById(R.id.loanMaturity) + val remainingLoan: TextView = itemView.findViewById(R.id.loanRemainingLoan) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_loan_overview, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val loan = loans[position] + holder.title.text = loan.title + holder.amount.text = loan.amount + holder.rate.text = loan.rate + holder.payment.text = loan.payment + holder.maturity.text = loan.maturity + holder.remainingLoan.text = loan.remainingLoan + + // 设置 title 为加粗大号字体 + holder.title.setTypeface(null, Typeface.BOLD) + holder.title.textSize = 20f + + holder.title.setOnClickListener { + onTitleClick(loan) + } + + // 为可编辑字段设置点击事件 + holder.rate.setOnClickListener { + onEditClick(loan, "rate", loan.rate, position) + } + holder.payment.setOnClickListener { + onEditClick(loan, "payment", loan.payment, position) + } + holder.maturity.setOnClickListener { + onEditClick(loan, "maturity", loan.maturity, position) + } + holder.remainingLoan.setOnClickListener { + onEditClick(loan, "remainingLoan", loan.remainingLoan, position) + } + } + + override fun getItemCount(): Int = loans.size + + +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/MonthlySummaryAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/MonthlySummaryAdapter.kt new file mode 100644 index 0000000..dea9d3a --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/MonthlySummaryAdapter.kt @@ -0,0 +1,65 @@ +package com.financialviewer.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.MonthlySummary +import com.financialviewer.utils.convertDoubleToString +import java.text.DateFormatSymbols +import java.util.Locale + +class MonthlySummaryAdapter ( + private val context: Context, + private val monthlySummaryList: MutableList, + private val onItemClick: (MonthlySummary) -> Unit, +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val month: TextView = itemView.findViewById(R.id.first) + val income: TextView = itemView.findViewById(R.id.second) + val cost: TextView = itemView.findViewById(R.id.third) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_three_equal_texts, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val monthlySummary = monthlySummaryList[position] + holder.month.text = getMonthNameInGerman(monthlySummary.month) + holder.income.text = convertDoubleToString(monthlySummary.income) + holder.cost.text = convertDoubleToString(monthlySummary.cost) + + holder.itemView.setOnClickListener { + onItemClick(monthlySummary) + } + + holder.income.setTextColor(ContextCompat.getColor(context, R.color.green)) + holder.cost.setTextColor(ContextCompat.getColor(context, R.color.red)) + } + + override fun getItemCount() = monthlySummaryList.size + + private fun initMonthlySummaryList() { + monthlySummaryList.clear() + } + + fun updateMonthlySummaryList(newList: List) { + initMonthlySummaryList() + monthlySummaryList.addAll(newList) + notifyDataSetChanged() + } + + fun getMonthNameInGerman(month: Int): String { + // 获取德文的月份名称数组 + val monthsInGerman = DateFormatSymbols(Locale.GERMAN).months + // 检查输入是否在有效范围内 + return if (month in 1..12) monthsInGerman[month - 1] else "Ungültiger Monat" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/ui/adapter/YearlySummaryAdapter.kt b/app/src/main/java/com/financialviewer/ui/adapter/YearlySummaryAdapter.kt new file mode 100644 index 0000000..6be03c6 --- /dev/null +++ b/app/src/main/java/com/financialviewer/ui/adapter/YearlySummaryAdapter.kt @@ -0,0 +1,58 @@ +package com.financialviewer.ui.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.financialviewer.R +import com.financialviewer.db.YearlySummary +import com.financialviewer.utils.convertDoubleToString + +class YearlySummaryAdapter ( + private val context: Context, + private val yearlySummaryList: MutableList, + private val onItemClick: (YearlySummary) -> Unit, +) : RecyclerView.Adapter() { + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val category: TextView = itemView.findViewById(R.id.first) + val amount: TextView = itemView.findViewById(R.id.second) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_two_equal_texts, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val yearlySummary = yearlySummaryList[position] + holder.category.text = yearlySummary.category + holder.amount.text = convertDoubleToString(yearlySummary.total) + + val color = if (yearlySummary.total > 0) { + ContextCompat.getColor(context, R.color.green) + } else { + ContextCompat.getColor(context, R.color.red) + } + holder.amount.setTextColor(color) + + holder.itemView.setOnClickListener { + onItemClick(yearlySummary) + } + } + + override fun getItemCount() = yearlySummaryList.size + + private fun initYearlySummaryList() { + yearlySummaryList.clear() + } + + fun updateYearlySummaryList(newList: List) { + initYearlySummaryList() + yearlySummaryList.addAll(newList) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/utils/BankRefParser.kt b/app/src/main/java/com/financialviewer/utils/BankRefParser.kt new file mode 100644 index 0000000..b02d93e --- /dev/null +++ b/app/src/main/java/com/financialviewer/utils/BankRefParser.kt @@ -0,0 +1,33 @@ +package com.financialviewer.utils + +import com.financialviewer.constants.COUNTERPARTY_MAP + +fun getCounterpartyKey(ref: String): String? { + return COUNTERPARTY_MAP.keys.find { key -> ref.contains(key) } +} + +fun getCounterparty(ref: String): String { + var counterparty = "" + val counterpartyOriginal = getCounterpartyKey(ref) + if (counterpartyOriginal != null) { + counterparty = COUNTERPARTY_MAP[counterpartyOriginal]?.first ?: "" + } + return counterparty +} + +fun getCategory(ref: String): String { + var category = "" + val counterpartyOriginal = getCounterpartyKey(ref) + if (counterpartyOriginal != null) { + category = COUNTERPARTY_MAP[counterpartyOriginal]?.second ?: "" + } + return category +} + +fun getBankTransactionRefId(ref: String): String { + val regex = """\b(\w+)\s+IBAN\b""".toRegex() // 匹配 string1 和 "IBAN" + val matchResult = regex.find(ref) + val refId = matchResult?.groupValues?.get(1) ?: "" + return refId +} + diff --git a/app/src/main/java/com/financialviewer/utils/Common.kt b/app/src/main/java/com/financialviewer/utils/Common.kt new file mode 100644 index 0000000..0b356ef --- /dev/null +++ b/app/src/main/java/com/financialviewer/utils/Common.kt @@ -0,0 +1,66 @@ +package com.financialviewer.utils + +import com.financialviewer.constants.CATEGORY_LIST +import java.nio.file.Paths + + +fun pathJoin(vararg parts: String): String { + return Paths.get(parts[0], *parts.sliceArray(1 until parts.size)).toString() +} + +fun generateYears(latestYearOfBankTransaction: String): List { + return (2024..latestYearOfBankTransaction.toInt()).map { it.toString() } +} + +fun insertItemIntoList(list: List, item: String, index: Int): List { + val mutableList = list.toMutableList() + mutableList.add(index, item) + return mutableList.toList() +} + +fun generateMonth(): List { + return (1..12).map { it.toString().padStart(2, '0') } +} + +fun generateCategory(): List { + val mutableList: MutableList = mutableListOf() + for (firstLevel in CATEGORY_LIST) { + for (secondLevel in firstLevel.subList) { + mutableList.add(secondLevel) + } + } + return mutableList +} + +fun generateCategoryWithMainCategory(): List { + val mutableList: MutableList = mutableListOf() + for (firstLevel in CATEGORY_LIST) { + mutableList.add(firstLevel.title) + for (secondLevel in firstLevel.subList) { + mutableList.add(secondLevel) + } + } + return mutableList +} + +fun generateOnlyMainCategory(): List { + val mutableList: MutableList = mutableListOf() + for (firstLevel in CATEGORY_LIST) { + mutableList.add(firstLevel.title) + } + return mutableList +} + +fun getSelectedCategory(selectedItem: String): MutableList { + val categoryList: MutableList = mutableListOf() + if (selectedItem.contains(".")) { + val firstLevelItem = CATEGORY_LIST.find { it.title == selectedItem } + if (firstLevelItem != null) { + categoryList.addAll(firstLevelItem.subList) + } + } else { + categoryList.add(selectedItem) + } + + return categoryList +} diff --git a/app/src/main/java/com/financialviewer/utils/FormatConverter.kt b/app/src/main/java/com/financialviewer/utils/FormatConverter.kt new file mode 100644 index 0000000..dd4123b --- /dev/null +++ b/app/src/main/java/com/financialviewer/utils/FormatConverter.kt @@ -0,0 +1,63 @@ +package com.financialviewer.utils + +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +fun convertStringToDouble(value: String): Double { + val cleanedValue = value.replace(".", "").replace(",", ".") + return cleanedValue.toDouble() +} + +fun convertInputStringToDouble(value: String): Double { + return if (value.contains(".") && value.contains(",")) { + convertStringToDouble(value) + } else { + value.replace(",", ".").toDouble() + } +} + +fun convertDoubleToString(value: Double): String { + val symbols = DecimalFormatSymbols(Locale.GERMAN) // 使用德语区域符号 + val decimalFormat = DecimalFormat("#,##0.00", symbols) // 格式化规则 + return decimalFormat.format(value) // 返回格式化后的字符串 +} + +fun convertPercentageStringToDouble(value: String): Double { + val cleanedValue = value.replace("%", "") + val normalizedValue = cleanedValue.replace(",", ".") + return normalizedValue.toDouble() / 100 +} + +fun convertPercentageToString(value: Double): String { + val symbols = DecimalFormatSymbols(Locale.GERMAN) // 使用德语区域符号 + val decimalFormat = DecimalFormat("#0.00%", symbols) + return decimalFormat.format(value) // 返回格式化后的字符串 +} + +fun convertStringToGermanDate(value: String): String { + val inputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN) + val outputFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN) + val date = LocalDate.parse(value, inputFormatter) + return outputFormatter.format(date) +} + +fun convertGermanDateToStandard(dateString: String): String { + val germanFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMAN) + val standardFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val localDate = LocalDate.parse(dateString, germanFormatter) + return localDate.format(standardFormatter) +} + +fun convertStandardToGermanDate(dateString: String): String { + val standardFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val germanFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.GERMANY) + val localDate = LocalDate.parse(dateString, standardFormatter) + return localDate.format(germanFormatter) +} + +fun windowsFileTimeToMillis(windowsFileTime: Long): Long { + return (windowsFileTime / 10000) - 11644473600000 +} diff --git a/app/src/main/java/com/financialviewer/utils/LoanCalculator.kt b/app/src/main/java/com/financialviewer/utils/LoanCalculator.kt new file mode 100644 index 0000000..8f0561b --- /dev/null +++ b/app/src/main/java/com/financialviewer/utils/LoanCalculator.kt @@ -0,0 +1,44 @@ +package com.financialviewer.utils + +import com.financialviewer.db.Loan + +fun calculateRemainingLoan(loan: Loan, monthDiff: Int): String { + var remainingLoan = convertStringToDouble(loan.remainingLoan) + val payment = convertStringToDouble(loan.payment) + val rate = convertPercentageStringToDouble(loan.rate) + + for (i in 0 until monthDiff) { + val interest = (remainingLoan * rate) / 12 + val calResult = remainingLoan - payment + interest + remainingLoan = calResult + } + return convertDoubleToString(remainingLoan) +} + +fun calculateNextInterest(loan: Loan, base: Double): Double { + val rate = convertPercentageStringToDouble(loan.rate) + return (base * rate) / 12 +} + +fun calculateNextPrincipal(loan: Loan, base: Double): Double { + val payment = convertStringToDouble(loan.payment) + val interest = calculateNextInterest(loan, base) + return payment - interest +} + +fun calculateNextRemaining(loan: Loan, base: Double): Double { + val principal = calculateNextPrincipal(loan, base) + return base - principal +} + +fun getNextMonth(year: Int, month: Int): String { + var newYear = year + var newMonth = month + if (month == 12) { + newYear += 1 + newMonth = 1 + } else { + newMonth += 1 + } + return newMonth.toString().padStart(2, '0') + "." + newYear.toString() +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/utils/SharedPreferencesHelper.kt b/app/src/main/java/com/financialviewer/utils/SharedPreferencesHelper.kt new file mode 100644 index 0000000..5c6c6ad --- /dev/null +++ b/app/src/main/java/com/financialviewer/utils/SharedPreferencesHelper.kt @@ -0,0 +1,94 @@ +package com.financialviewer.utils + +import android.content.Context +import android.util.Log +import java.io.File +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class SharedPreferencesHelper( + private val context: Context +) { + + private val fileName = "app_prefs" + private val sharedPreferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE) + + private fun saveData(key: String, value: String) { + val editor = sharedPreferences.edit() + editor.putString(key, value) + editor.apply() + } + + fun saveDefaultYear(value: Int) { + val editor = sharedPreferences.edit() + editor.putInt("default_year_of_bank_Transaction", value) + editor.apply() + } + + fun saveAuthStatus(key: String, value: Boolean) { + val editor = sharedPreferences.edit() + editor.putBoolean(key, value) + editor.apply() + } + + fun saveCurrentUpdateTimeToSharedPreferences() { + val currentDateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val formattedDateTime = currentDateTime.format(formatter) + + saveData("bank_transaction_last_Update_date", formattedDateTime) + } + + fun saveCalculateFixedCostTime() { + val currentDateTime = LocalDateTime.now() + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val formattedDateTime = currentDateTime.format(formatter) + + saveData("calculate_fixed_cost_time", formattedDateTime) + } + + private fun readData(key: String): String { + return sharedPreferences.getString(key, "") ?: "" + } + + fun readDefaultYear(defaultValue: Int): Int { + return sharedPreferences.getInt("default_year_of_bank_Transaction", defaultValue) + } + + fun readLastUpdateTimeToSharedPreferences(): String { + return readData("bank_transaction_last_Update_date") + } + + fun readCalculateFixedCostTime(): String { + return readData("calculate_fixed_cost_time") + } + + fun getAllSharedPreferences(): MutableList { + val allEntries: Map = sharedPreferences.all + for ((key, value) in allEntries) { + Log.d("SharedPreferences", "$key: $value") + + } + return sharedPreferences.all.keys.toMutableList() + } + + fun removeSpecificSharedPreference(key: String) { + val editor = sharedPreferences.edit() + editor.remove(key) + editor.apply() // or editor.commit() + } + + fun clearAllSharedPreferences() { + val editor = sharedPreferences.edit() + editor.clear() + editor.apply() // or editor.commit() + } + + private fun deleteSharedPreferencesFile(fileName: String) { + val dir = context.filesDir.parentFile + val sharedPrefsFile = File(dir, "shared_prefs/$fileName.xml") + if (sharedPrefsFile.exists()) { + sharedPrefsFile.delete() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/work/StartupWorker.kt b/app/src/main/java/com/financialviewer/work/StartupWorker.kt new file mode 100644 index 0000000..9344eb4 --- /dev/null +++ b/app/src/main/java/com/financialviewer/work/StartupWorker.kt @@ -0,0 +1,28 @@ +package com.financialviewer.work + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.financialviewer.repository.FixedCostRepository + +class StartupWorker (context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result { + + val updateLoans = UpdateLoans(context = applicationContext) + updateLoans.initDB() + updateLoans.updateDB() + + val updateBankTransactions = UpdateBankTransactions(context = applicationContext) + updateBankTransactions.initDB() + + val fixedCostRepository = FixedCostRepository(context = applicationContext) + val updateFixedCosts = UpdateFixedCosts(context = applicationContext) + if (fixedCostRepository.getRowCount() == 0) { + updateFixedCosts.updateDB() + } + + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/work/UpdateBankTransactions.kt b/app/src/main/java/com/financialviewer/work/UpdateBankTransactions.kt new file mode 100644 index 0000000..e6ed6c1 --- /dev/null +++ b/app/src/main/java/com/financialviewer/work/UpdateBankTransactions.kt @@ -0,0 +1,142 @@ +package com.financialviewer.work + +import android.content.Context +import com.financialviewer.constants.COMDIRECT_LIST +import com.financialviewer.constants.FIXED_COST_TYPE +import com.financialviewer.db.BankTransaction +import com.financialviewer.network.SmbClientHelper +import com.financialviewer.repository.BankTransactionRepository +import com.financialviewer.utils.SharedPreferencesHelper +import com.financialviewer.utils.convertGermanDateToStandard +import com.financialviewer.utils.convertStringToDouble +import com.financialviewer.utils.getBankTransactionRefId +import com.financialviewer.utils.getCategory +import com.financialviewer.utils.getCounterparty +import com.financialviewer.utils.windowsFileTimeToMillis +import com.hierynomus.smbj.share.File +import com.opencsv.CSVReaderBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.ByteArrayInputStream +import java.io.InputStreamReader +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +class UpdateBankTransactions (context: Context) { + private val bankTransactionRepository = BankTransactionRepository(context) + private val appPreferences = SharedPreferencesHelper(context) + private var smbClientHelper = SmbClientHelper(context) + private val appContext = context.applicationContext + + suspend fun initDB() { + if (bankTransactionRepository.getRowCount() == 0) { + for (comdirect in COMDIRECT_LIST) { + bankTransactionRepository.insertBankTransaction(comdirect) + } + } + } + + suspend fun updateDB() { + var isUpdated = false + var isConnect = false + + try { + isConnect = smbClientHelper.connect() + if (isConnect) { + withContext(Dispatchers.IO) { + val csvFiles = smbClientHelper.getCSVFromShare() + if (csvFiles.isNotEmpty()) { + for (fileInfo in csvFiles) { + val file = smbClientHelper.getCSVFile(fileInfo.fileName) + + val fileTime = fileInfo.lastWriteTime.windowsTimeStamp + if (updateNecessary(fileTime)) { + insertBankTransactionsIntoDB(file) + isUpdated = true + } + } + } + } + } + } finally { + if (isConnect) { + smbClientHelper.disconnect() + } + } + + if (isUpdated) { + appPreferences.saveCurrentUpdateTimeToSharedPreferences() + val year = bankTransactionRepository.getLatestYearOfBankTransaction() + appPreferences.saveDefaultYear(year.toInt()) + + updateSummary(appContext) + UpdateFixedCosts(appContext).updateDB() + } + } + + private fun updateNecessary(fileTime: Long): Boolean { + var necessary = false + val dateString = appPreferences.readLastUpdateTimeToSharedPreferences() + + if (dateString != "") { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + val localDateTime = LocalDateTime.parse(dateString, formatter) + val instantFromSharedPref = localDateTime.atZone(ZoneId.systemDefault()).toInstant() + + val fileTimeMillis = windowsFileTimeToMillis(fileTime) + val instantFromFileTime = Instant.ofEpochMilli(fileTimeMillis) + + if (instantFromSharedPref.isBefore(instantFromFileTime)) { + necessary = true + } + } else { + necessary = true + } + return necessary + } + + private suspend fun insertBankTransactionsIntoDB(file: File) { + file.inputStream.use { inputStream -> + val content = inputStream.readBytes() + val byteArrayInputStream = ByteArrayInputStream(content) + + CSVReaderBuilder(InputStreamReader(byteArrayInputStream, Charsets.UTF_8)) + .withCSVParser(com.opencsv.CSVParserBuilder().withSeparator(';').build()) // 指定分隔符 + .build().use { csvReader -> + + val rows = csvReader.readAll() + var account = "" + if (rows.isNotEmpty()) { + account = rows[0][1] + } + rows.forEach { row -> + val reference = row.last() + val refId = getBankTransactionRefId(reference) + if (bankTransactionRepository.getBankTransactionById(refId) == null) { + val counterparty = getCounterparty(reference) + if (counterparty != "") { + val date = row.first() + val standardDate = convertGermanDateToStandard(date) + val amount = convertStringToDouble(row[2]) + val category = getCategory(reference) + bankTransactionRepository.insertBankTransaction( + BankTransaction( + refId, + account, + standardDate, + amount, + category, + counterparty, + reference, + FIXED_COST_TYPE.containsKey(category) + ) + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/work/UpdateFixedCosts.kt b/app/src/main/java/com/financialviewer/work/UpdateFixedCosts.kt new file mode 100644 index 0000000..6195d89 --- /dev/null +++ b/app/src/main/java/com/financialviewer/work/UpdateFixedCosts.kt @@ -0,0 +1,120 @@ +package com.financialviewer.work + +import android.content.Context +import com.financialviewer.constants.FIXED_COST_COUNT +import com.financialviewer.constants.FIXED_COST_TYPE +import com.financialviewer.constants.MONTHLY +import com.financialviewer.constants.QUARTERLY +import com.financialviewer.constants.YEAR +import com.financialviewer.constants.YEARLY +import com.financialviewer.db.BankTransaction +import com.financialviewer.db.FixedCost +import com.financialviewer.repository.BankTransactionRepository +import com.financialviewer.repository.FixedCostRepository +import com.financialviewer.utils.SharedPreferencesHelper +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class UpdateFixedCosts(context: Context) { + private val bankTransactionRepository = BankTransactionRepository(context) + private val fixedCostRepository = FixedCostRepository(context) + private val appPreferences = SharedPreferencesHelper(context) + + private var year = appPreferences.readDefaultYear(YEAR) + + suspend fun updateDB() { + for (item in FIXED_COST_TYPE) { + val category = item.key + when (item.value) { + MONTHLY -> { + val bankTransactionList = getBankTransactionList(category, MONTHLY) + var sum = calculateAmount(bankTransactionList, category) + if (category == "手机费") { + sum += -10.0 + } + val refIds = bankTransactionList.map(BankTransaction::refId) + updateFixedCost(category, sum, MONTHLY, refIds) + } + QUARTERLY -> { + val bankTransactionList = getBankTransactionList(category, QUARTERLY) + val sum = calculateAmount(bankTransactionList, category) + val refIds = bankTransactionList.map(BankTransaction::refId) + updateFixedCost(category, sum, QUARTERLY, refIds) + } + else -> { + val bankTransactionList = getBankTransactionList(category, YEARLY) + val sum = calculateAmount(bankTransactionList, category) + val refIds = bankTransactionList.map(BankTransaction::refId) + updateFixedCost(category, sum, YEARLY, refIds) + } + } + } + appPreferences.saveCalculateFixedCostTime() + + } + + private suspend fun getBankTransactionList(category: String, type: String): MutableList { + var resultList: MutableList + var bankTransactionList = + bankTransactionRepository.getBankTransactionsByCategory(year.toString(), category) + if (bankTransactionList.isEmpty()) { + bankTransactionList = + bankTransactionRepository.getBankTransactionsByCategory((year - 1).toString(), category) + } + + when (type) { + YEARLY -> { + resultList = bankTransactionList + } + else -> { + resultList = getBankTransactionsFromLastMonth(bankTransactionList) + while (resultList.size != FIXED_COST_COUNT[category] && bankTransactionList.size > 0) { + bankTransactionList.removeAll(resultList) + resultList = getBankTransactionsFromLastMonth(bankTransactionList) + } + } + } + return resultList + } + + private fun getBankTransactionsFromLastMonth(bankTransactionList: MutableList): MutableList { + val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") + + val latestDate = LocalDate.parse(bankTransactionList.first().date, dateFormat) + val latestYear = latestDate.year + val latestMonth = latestDate.month + + return bankTransactionList.filter { + val itemDate = LocalDate.parse(it.date, dateFormat) + itemDate.year == latestYear && itemDate.month == latestMonth && it.isFixedCost + }.toMutableList() + } + + private fun calculateAmount(bankTransactionList: MutableList, category: String): Double { + var sum = 0.0 + if (bankTransactionList.size == FIXED_COST_COUNT[category]) { + for (bankTransaction in bankTransactionList) { + sum += bankTransaction.amount + } + } + return sum + } + + private suspend fun updateFixedCost(category: String, sum: Double, type: String, refIds: List) { + val fixedCost = fixedCostRepository.getFixedCostByCategory(category) + if (fixedCost == null) { + fixedCostRepository.insertFixedCost( + FixedCost( + category = category, + amount = sum, + type = type, + refIds = refIds.joinToString(",") + ) + ) + } else { + fixedCost.amount = sum + fixedCost.refIds = refIds.joinToString(",") + fixedCostRepository.updateFixedCost(fixedCost) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/work/UpdateLoans.kt b/app/src/main/java/com/financialviewer/work/UpdateLoans.kt new file mode 100644 index 0000000..ff284ba --- /dev/null +++ b/app/src/main/java/com/financialviewer/work/UpdateLoans.kt @@ -0,0 +1,56 @@ +package com.financialviewer.work + +import android.content.Context +import com.financialviewer.data.YearMonthDay +import com.financialviewer.db.Loan +import com.financialviewer.repository.LoanRepository +import java.util.Calendar +import java.util.TimeZone + +class UpdateLoans(context: Context) { + private val germanyTimeZone = TimeZone.getTimeZone("Europe/Berlin") + private val loanRepository = LoanRepository(context) + + suspend fun initDB() { + if (loanRepository.getRowCount() == 0) { + loanRepository.initDB() + } + } + + suspend fun updateDB() { + val allLoansInDB = loanRepository.getAllLoans() + + val monthDiff = getMonthDiff(allLoansInDB[0]) + if ( monthDiff > 0) { + for (loan in allLoansInDB) { + loanRepository.updateRemainingLoan(loan, monthDiff) + } + } + } + + private fun getLastUpdateDate(loan: Loan): YearMonthDay { + val calendar = Calendar.getInstance() + calendar.timeInMillis = loan.lastUpdate + return YearMonthDay( + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH) + 1, + calendar.get(Calendar.DAY_OF_MONTH) + ) + } + + private fun getMonthDiff(loan: Loan): Int { + val calendar = Calendar.getInstance(germanyTimeZone) + val currentDate = YearMonthDay( + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH) + 1, + calendar.get(Calendar.DAY_OF_MONTH) + ) + val lastUpdateDate = getLastUpdateDate(loan) + + val yearDiff = currentDate.year - lastUpdateDate.year + val monthDiff = currentDate.month - lastUpdateDate.month + + return yearDiff * 12 + monthDiff + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/financialviewer/work/UpdateSummary.kt b/app/src/main/java/com/financialviewer/work/UpdateSummary.kt new file mode 100644 index 0000000..b719bed --- /dev/null +++ b/app/src/main/java/com/financialviewer/work/UpdateSummary.kt @@ -0,0 +1,84 @@ +package com.financialviewer.work + +import android.content.Context +import com.financialviewer.constants.CATEGORY_LIST +import com.financialviewer.db.MonthlySummary +import com.financialviewer.db.YearlySummary +import com.financialviewer.repository.BankTransactionRepository +import com.financialviewer.repository.MonthlySummaryRepository +import com.financialviewer.repository.YearlySummaryRepository +import com.financialviewer.utils.generateYears +import java.math.BigDecimal + +suspend fun updateSummary(context: Context) { + val bankTransactionRepository = BankTransactionRepository(context) + val monthlySummaryRepository = MonthlySummaryRepository(context) + val yearlySummaryRepository = YearlySummaryRepository(context) + + + val latestYear = bankTransactionRepository.getLatestYearOfBankTransaction() + val yearList = generateYears(latestYear) + val categoryList = getCategoryList() + for (year in yearList) { + for (category in categoryList) { + var sum = BigDecimal("0.0") + val transactionList = bankTransactionRepository.getBankTransactionsByCategory(year, category) + for (transaction in transactionList) { + sum += BigDecimal(transaction.amount.toString()) + } + val yearlySummary = yearlySummaryRepository.getYearlySummaryByCategory(year.toInt(), category) + if (yearlySummary == null) { + yearlySummaryRepository.insertYearlySummary( + YearlySummary( + year = year.toInt(), + category = category, + total = sum.toDouble() + ) + ) + } else { + yearlySummary.total = sum.toDouble() + yearlySummaryRepository.updateYearlySummary(yearlySummary) + } + } + + for (i in 1..12) { + var incomeSum = BigDecimal("0.0") + var costSum = BigDecimal("0.0") + val transactionList = bankTransactionRepository.getBankTransactionsByMonth(year, i.toString().padStart(2, '0')) + for (transaction in transactionList) { + if (transaction.amount > 0) { + incomeSum += BigDecimal(transaction.amount.toString()) + } else { + costSum += BigDecimal(transaction.amount.toString()) + } + } + if (incomeSum.toDouble() > 0 || costSum.toDouble() > 0) { + val monthlySummary = monthlySummaryRepository.getMonthlySummaryByMonth(year.toInt(), i) + if (monthlySummary == null) { + monthlySummaryRepository.insertMonthlySummary( + MonthlySummary( + year = year.toInt(), + month = i, + income = incomeSum.toDouble(), + cost = costSum.toDouble() + ) + ) + } else { + monthlySummary.income = incomeSum.toDouble() + monthlySummary.cost = costSum.toDouble() + monthlySummaryRepository.updateMonthlySummary(monthlySummary) + } + } + } + } +} + +private fun getCategoryList(): MutableList { + val categoryList: MutableList = mutableListOf() + for (firstLevel in CATEGORY_LIST) { + for (secondLevel in firstLevel.subList) { + categoryList.add(secondLevel) + } + } + return categoryList +} diff --git a/app/src/main/res/drawable/fixed_costs_24.xml b/app/src/main/res/drawable/fixed_costs_24.xml new file mode 100644 index 0000000..9dde11d --- /dev/null +++ b/app/src/main/res/drawable/fixed_costs_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..b48f194 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/loan_overview_24.xml b/app/src/main/res/drawable/loan_overview_24.xml new file mode 100644 index 0000000..5871251 --- /dev/null +++ b/app/src/main/res/drawable/loan_overview_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/monthly_summary_24.xml b/app/src/main/res/drawable/monthly_summary_24.xml new file mode 100644 index 0000000..db81e26 --- /dev/null +++ b/app/src/main/res/drawable/monthly_summary_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/settings_24.xml b/app/src/main/res/drawable/settings_24.xml new file mode 100644 index 0000000..77b8202 --- /dev/null +++ b/app/src/main/res/drawable/settings_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/transaction_24.xml b/app/src/main/res/drawable/transaction_24.xml new file mode 100644 index 0000000..c4078b3 --- /dev/null +++ b/app/src/main/res/drawable/transaction_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/update_transaction_24.xml b/app/src/main/res/drawable/update_transaction_24.xml new file mode 100644 index 0000000..04389d7 --- /dev/null +++ b/app/src/main/res/drawable/update_transaction_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/drawable/yearly_summary_24.xml b/app/src/main/res/drawable/yearly_summary_24.xml new file mode 100644 index 0000000..f6a7112 --- /dev/null +++ b/app/src/main/res/drawable/yearly_summary_24.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_bank_transaction_details.xml b/app/src/main/res/layout/activity_bank_transaction_details.xml new file mode 100644 index 0000000..0c68f5f --- /dev/null +++ b/app/src/main/res/layout/activity_bank_transaction_details.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_bank_transactions.xml b/app/src/main/res/layout/activity_bank_transactions.xml new file mode 100644 index 0000000..03a45ad --- /dev/null +++ b/app/src/main/res/layout/activity_bank_transactions.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_fixed_cost_details.xml b/app/src/main/res/layout/activity_fixed_cost_details.xml new file mode 100644 index 0000000..3bacd88 --- /dev/null +++ b/app/src/main/res/layout/activity_fixed_cost_details.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_fixed_costs.xml b/app/src/main/res/layout/activity_fixed_costs.xml new file mode 100644 index 0000000..7f1a19b --- /dev/null +++ b/app/src/main/res/layout/activity_fixed_costs.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + +