commit e8bd7d475b4065e9e4004eb4523916c193fc4da9 Author: ithillad Date: Thu Dec 19 14:02:38 2024 +0100 Initial commit: Add financial viewer 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 0000000..6ef9bbe Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ 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 @@ + + + + + + + + + + + +