diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8a..b589d56e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index fbd67c2d..00000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 61413d36..cb865f69 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,15 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..2b8a50fc --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 96869c6e..dc82b60f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - - + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b5ff13fc..5166153b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,13 +28,12 @@ String getManageYourRentersBaseUrl() { } android { - compileSdkVersion 33 - buildToolsVersion "33.0.0" + compileSdk 34 defaultConfig { applicationId "com.rohitthebest.manageyourrenters" - minSdkVersion 23 - targetSdkVersion 33 + minSdk 23 + targetSdk 34 versionCode 5 versionName "5.3.1" @@ -177,4 +176,6 @@ dependencies { //https://developer.android.com/studio/write/java8-support#groovy coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' // don't change the version until gradle plugin is updated to 7.4.0-alpha10 + + implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3314abf1..98b7b8d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,11 +9,11 @@ - - - - - - - - + + + + + + + + (DiffUtilCallback()) { + + private var mListener: OnClickListener? = null + + inner class BudgetViewHolder(val binding: AdapterBudgetBinding) : + RecyclerView.ViewHolder(binding.root) { + + init { + + binding.root.setOnClickListener { + + if (isPositionAndMlistenerValid()) { + mListener!!.onItemClick(getItem(absoluteAdapterPosition)) + } + } + + binding.baMenuBtn.setOnClickListener { + if (isPositionAndMlistenerValid()) { + mListener!!.onMenuBtnClick( + getItem(absoluteAdapterPosition), + binding.baMenuBtn, + absoluteAdapterPosition + ) + } + } + + binding.baDetailsBtn.setOnClickListener { + if (isPositionAndMlistenerValid()) { + + mListener!!.onDetailsButtonClicked(getItem(absoluteAdapterPosition)) + } + } + } + + private fun isPositionAndMlistenerValid(): Boolean { + return mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION + } + + fun setData(budget: Budget?) { + + budget?.let { myBudget -> + + binding.apply { + + baCategoryNameTV.text = myBudget.categoryName + + if (myBudget.categoryImageUrl.isValid()) { + + Functions.setImageToImageViewUsingGlide( + binding.root.context, + baCategoryImageIV, + myBudget.categoryImageUrl, + {}, + {} + ) + } else { + + Glide.with(binding.root) + .load(R.drawable.expense_shortcut_icon) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(baCategoryImageIV) + } + + baSpentVsLimitTV.text = binding.root.context.getString( + R.string.spentVsLimit, + myBudget.currentExpenseAmount.format(2), + myBudget.budgetLimit.format(2) + ) + + val numberOfDaysLeftInMonth = + WorkingWithDateAndTime.getNumberOfDaysLeftInAnyMonth( + myBudget.month, myBudget.year + ) + + Log.d(TAG, "setData: numberOfDaysLeftInMonth: $numberOfDaysLeftInMonth") + + var perDayExpense = if (numberOfDaysLeftInMonth != 0) { + (myBudget.budgetLimit - myBudget.currentExpenseAmount) / numberOfDaysLeftInMonth + } else { + 0.0 + } + + if (perDayExpense < 0) perDayExpense = 0.0 + + baPerDayExpenseTV.text = binding.root.context.getString( + R.string.budgetPerDay, perDayExpense.format(2), + numberOfDaysLeftInMonth.toString() + ) + + val progressInPercent = + ((myBudget.currentExpenseAmount / myBudget.budgetLimit) * 100).toInt() + + percentTV.text = if (progressInPercent > 100.0) { + binding.root.context.getString(R.string._100) + } else { + "$progressInPercent%" + } + + changeProgressColorsByProgressPercent(progressInPercent) + + baProgressBar.max = myBudget.budgetLimit.toInt() + + if (myBudget.currentExpenseAmount > myBudget.budgetLimit) { + baProgressBar.progress = myBudget.budgetLimit.toInt() + } else { + baProgressBar.progress = myBudget.currentExpenseAmount.toInt() + } + } + } + } + + private fun changeProgressColorsByProgressPercent(progressInPercent: Int) { + + when { + + (progressInPercent in 0..35) -> { + val colorGreen = ContextCompat.getColor( + binding.root.context, + R.color.color_green + ) + binding.baProgressBar.progressTintList = ColorStateList.valueOf( + colorGreen + ) + binding.percentMCV.strokeColor = colorGreen + + } + + (progressInPercent in 36..68) -> { + val colorYellow = ContextCompat.getColor( + binding.root.context, + R.color.color_yellow + ) + binding.baProgressBar.progressTintList = ColorStateList.valueOf( + colorYellow + ) + binding.percentMCV.strokeColor = colorYellow + + } + + else -> { + val colorRed = ContextCompat.getColor( + binding.root.context, + R.color.color_Red + ) + binding.baProgressBar.progressTintList = ColorStateList.valueOf( + colorRed + ) + + binding.percentMCV.strokeColor = colorRed + } + } + } + } + + companion object { + + class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Budget, newItem: Budget): Boolean = + oldItem.key == newItem.key + + override fun areContentsTheSame(oldItem: Budget, newItem: Budget): Boolean = + oldItem == newItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BudgetViewHolder { + + val binding = + AdapterBudgetBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + return BudgetViewHolder(binding) + } + + override fun onBindViewHolder(holder: BudgetViewHolder, position: Int) { + + holder.setData(getItem(position)) + } + + interface OnClickListener { + + fun onItemClick(budget: Budget) + fun onMenuBtnClick(budget: Budget, view: View, position: Int) + fun onDetailsButtonClicked(budget: Budget) + } + + fun setOnClickListener(listener: OnClickListener) { + mListener = listener + } +} + diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/IncomeRVAdapter.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/IncomeRVAdapter.kt new file mode 100644 index 00000000..a8b75283 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/IncomeRVAdapter.kt @@ -0,0 +1,113 @@ +package com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.databinding.AdapterIncomeListItemBinding +import com.rohitthebest.manageyourrenters.utils.format + +class IncomeRVAdapter( + val linkedPaymentMethodsMap: Map = emptyMap(), +) : ListAdapter(DiffUtilCallback()) { + + private var mListener: OnClickListener? = null + + inner class IncomeViewHolder(val binding: AdapterIncomeListItemBinding) : + RecyclerView.ViewHolder(binding.root) { + + init { + + binding.root.setOnClickListener { + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onItemClick(getItem(absoluteAdapterPosition)) + } + } + + binding.incomeMenuBtn.setOnClickListener { + + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onIncomeItemMenuBtnClicked( + getItem(absoluteAdapterPosition), + binding.incomeMenuBtn + ) + } + } + + binding.incomeSyncBtn.setOnClickListener { + + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onSyncBtnCLicked( + getItem(absoluteAdapterPosition), + absoluteAdapterPosition + ) + } + } + + } + + fun setData(income: Income?) { + + income?.let { myIncome -> + + binding.apply { + + incomeValueTV.text = myIncome.income.format(2) + incomeSourceValueTV.text = myIncome.source + incomeSyncBtn.isVisible = !myIncome.isSynced + + if (linkedPaymentMethodsMap.isNotEmpty()) { + lincomeLinkedPaymentMethodsTV.text = myIncome.getPaymentMethodString( + paymentMethodsMap = linkedPaymentMethodsMap + ) + } + + } + } + } + } + + companion object { + + class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Income, newItem: Income): Boolean = + oldItem.key == newItem.key + + override fun areContentsTheSame(oldItem: Income, newItem: Income): Boolean = + oldItem == newItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IncomeViewHolder { + + val binding = + AdapterIncomeListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + return IncomeViewHolder(binding) + } + + override fun onBindViewHolder(holder: IncomeViewHolder, position: Int) { + + holder.setData(getItem(position)) + } + + interface OnClickListener { + + fun onItemClick(income: Income) + fun onIncomeItemMenuBtnClicked(income: Income, view: View) + fun onSyncBtnCLicked(income: Income, position: Int) + } + + fun setOnClickListener(listener: OnClickListener) { + mListener = listener + } +} + diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/SelectMonthAndYearAdapter.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/SelectMonthAndYearAdapter.kt new file mode 100644 index 00000000..32d699c3 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/SelectMonthAndYearAdapter.kt @@ -0,0 +1,98 @@ +package com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.databinding.AdapterSelectMonthAndYearBinding +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime + +class SelectMonthAndYearAdapter : + ListAdapter(DiffUtilCallback()) { + + private var mListener: OnClickListener? = null + + inner class SelectMonthAndYearViewHolder(val binding: AdapterSelectMonthAndYearBinding) : + RecyclerView.ViewHolder(binding.root) { + + private var monthList: List = emptyList() + + init { + + monthList = binding.root.context.resources.getStringArray(R.array.months).toList() + + binding.monthYearRB.setOnClickListener { + + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onMonthAndYearClicked(getItem(absoluteAdapterPosition)) + } + } + } + + fun setData(monthAndYearString: String?) { + + monthAndYearString?.let { monthYearString -> + + binding.apply { + + val monthAndYear = + WorkingWithDateAndTime.extractMonthAndYearFromMonthAndYearString( + monthYearString + ) + + val month = monthList[monthAndYear.first] + + monthYearRB.text = binding.root.context.getString( + R.string.month_and_year, + month, + monthAndYear.second.toString() + ) + } + } + } + } + + companion object { + + class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = + oldItem == newItem + + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = + oldItem == newItem + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): SelectMonthAndYearViewHolder { + + val binding = AdapterSelectMonthAndYearBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + return SelectMonthAndYearViewHolder(binding) + } + + override fun onBindViewHolder(holder: SelectMonthAndYearViewHolder, position: Int) { + + holder.setData(getItem(position)) + } + + interface OnClickListener { + + fun onMonthAndYearClicked(monthAndYear: String) + } + + fun setOnClickListener(listener: OnClickListener) { + mListener = listener + } +} + diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/SetBudgetExpenseCategoryAdapter.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/SetBudgetExpenseCategoryAdapter.kt new file mode 100644 index 00000000..ebe62718 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/adapters/trackMoneyAdapters/expenseAdapters/budgetAndIncome/SetBudgetExpenseCategoryAdapter.kt @@ -0,0 +1,221 @@ +package com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome + +import android.content.res.ColorStateList +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.databinding.AdapterSetBudgetExpenseCategoryBinding +import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.changeTextColor +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.isValid +import com.rohitthebest.manageyourrenters.utils.show + +class SetBudgetExpenseCategoryAdapter : + ListAdapter(DiffUtilCallback()) { + + private var mListener: OnClickListener? = null + + inner class SetBudgetViewHolder(val binding: AdapterSetBudgetExpenseCategoryBinding) : + RecyclerView.ViewHolder(binding.root) { + + init { + + binding.budgetAddLimitBtn.setOnClickListener { + + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onAddBudgetClicked( + getItem(absoluteAdapterPosition), + absoluteAdapterPosition + ) + } + } + + binding.budgetMenuBtn.setOnClickListener { + + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onBudgetMenuBtnClicked( + getItem(absoluteAdapterPosition), + binding.budgetMenuBtn, + absoluteAdapterPosition + ) + } + } + + binding.budgetSyncBtn.setOnClickListener { + + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + + mListener!!.onBudgetSyncBtnClicked( + getItem(absoluteAdapterPosition), + absoluteAdapterPosition + ) + } + } + + binding.root.setOnClickListener { + if (mListener != null && absoluteAdapterPosition != RecyclerView.NO_POSITION) { + mListener!!.onItemClicked( + getItem(absoluteAdapterPosition).expenseCategoryKey, + getItem(absoluteAdapterPosition).budgetLimit != 0.0 + ) + } + } + } + + fun setData(budget: Budget?) { + + budget?.let { myBudget -> + + binding.apply { + + budgetCategoryNameTV.text = myBudget.categoryName + budgetCurrent.text = myBudget.currentExpenseAmount.format(2) + + if (myBudget.categoryImageUrl.isValid()) { + + Functions.setImageToImageViewUsingGlide( + binding.root.context, + categoryImage, + myBudget.categoryImageUrl, + {}, + {} + ) + } else { + + Glide.with(binding.root) + .load(R.drawable.expense_shortcut_icon) + .transition(DrawableTransitionOptions.withCrossFade()) + .into(categoryImage) + } + + if (myBudget.budgetLimit == 0.0) { + + // no budget is set + budgetAddLimitBtn.show() + budgetLimitTV.hide() + budgetMenuBtn.hide() + budgetSyncBtn.hide() + } else { + budgetAddLimitBtn.hide() + budgetLimitTV.show() + budgetMenuBtn.show() + + budgetSyncBtn.isVisible = !myBudget.isSynced + + budgetLimitTV.text = myBudget.budgetLimit.format(2) + + budgetProgressBar.max = myBudget.budgetLimit.toInt() + + } + + val progressInPercent = + ((myBudget.currentExpenseAmount / myBudget.budgetLimit) * 100).toInt() + + when { + + (progressInPercent in 0..35) -> { + + budgetCurrent.changeTextColor(binding.root.context, R.color.color_green) + budgetProgressBar.progressTintList = ColorStateList.valueOf( + ContextCompat.getColor( + binding.root.context, + R.color.color_green + ) + ) + } + + (progressInPercent in 36..68) -> { + + budgetCurrent.changeTextColor( + binding.root.context, + R.color.color_yellow + ) + budgetProgressBar.progressTintList = ColorStateList.valueOf( + ContextCompat.getColor( + binding.root.context, + R.color.color_yellow + ) + ) + } + + progressInPercent > 68 -> { + + budgetCurrent.changeTextColor( + binding.root.context, + R.color.color_Red + ) + budgetProgressBar.progressTintList = ColorStateList.valueOf( + ContextCompat.getColor( + binding.root.context, + R.color.color_Red + ) + ) + } + } + + if (myBudget.currentExpenseAmount > myBudget.budgetLimit) { + budgetProgressBar.progress = myBudget.budgetLimit.toInt() + } else { + budgetProgressBar.progress = myBudget.currentExpenseAmount.toInt() + } + + } + } + } + } + + companion object { + + class DiffUtilCallback : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: Budget, newItem: Budget): Boolean = + oldItem.key == newItem.key + + override fun areContentsTheSame(oldItem: Budget, newItem: Budget): Boolean = + oldItem == newItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetBudgetViewHolder { + + val binding = AdapterSetBudgetExpenseCategoryBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + return SetBudgetViewHolder(binding) + } + + override fun onBindViewHolder(holder: SetBudgetViewHolder, position: Int) { + + holder.setData(getItem(position)) + } + + interface OnClickListener { + + fun onItemClicked(expenseCategoryKey: String, isBudgetLimitAdded: Boolean) + fun onAddBudgetClicked(budget: Budget, position: Int) + fun onBudgetMenuBtnClicked(budget: Budget, view: View, position: Int) + + fun onBudgetSyncBtnClicked(budget: Budget, position: Int) + } + + fun setOnClickListener(listener: OnClickListener) { + mListener = listener + } +} + diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/data/ExpenseCategoryAndTheirTotalExpenseAmounts.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/data/ExpenseCategoryAndTheirTotalExpenseAmounts.kt new file mode 100644 index 00000000..ddcb70c4 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/data/ExpenseCategoryAndTheirTotalExpenseAmounts.kt @@ -0,0 +1,7 @@ +package com.rohitthebest.manageyourrenters.data + +data class ExpenseCategoryAndTheirTotalExpenseAmounts( + var expenseCategoryKey: String, + var categoryName: String, + var totalAmount: Double +) \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/data/MonthAndTotalSum.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/data/MonthAndTotalSum.kt new file mode 100644 index 00000000..c2f86d9a --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/data/MonthAndTotalSum.kt @@ -0,0 +1,6 @@ +package com.rohitthebest.manageyourrenters.data + +data class MonthAndTotalSum( + var month: Int, + var total: Double +) diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/data/ShowExpenseBottomSheetTagsEnum.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/data/ShowExpenseBottomSheetTagsEnum.kt index 71fba8cd..3130aebb 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/data/ShowExpenseBottomSheetTagsEnum.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/data/ShowExpenseBottomSheetTagsEnum.kt @@ -3,5 +3,6 @@ package com.rohitthebest.manageyourrenters.data enum class ShowExpenseBottomSheetTagsEnum { GRAPH_FRAGMENT, - PAYMENT_METHOD_FRAGMENT + PAYMENT_METHOD_FRAGMENT, + BUDGET_AND_INCOME_FRAGMENT } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/data/filter/BudgetAndIncomeExpenseFilter.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/data/filter/BudgetAndIncomeExpenseFilter.kt new file mode 100644 index 00000000..43a3807e --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/data/filter/BudgetAndIncomeExpenseFilter.kt @@ -0,0 +1,14 @@ +package com.rohitthebest.manageyourrenters.data.filter + +import java.io.Serializable + +data class BudgetAndIncomeExpenseFilter( + var categoryKeys: List, + var paymentMethods: List = emptyList() // if empty then show all the expenses +) : Serializable { + + constructor() : this( + emptyList(), + emptyList() + ) +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/BudgetDao.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/BudgetDao.kt new file mode 100644 index 00000000..a7f52720 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/BudgetDao.kt @@ -0,0 +1,76 @@ +package com.rohitthebest.manageyourrenters.database.dao + +import androidx.room.* +import com.rohitthebest.manageyourrenters.data.MonthAndTotalSum +import com.rohitthebest.manageyourrenters.database.model.Budget +import kotlinx.coroutines.flow.Flow + +@Dao +interface BudgetDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertBudget(budget: Budget) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAllBudget(budgets: List) + + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun updateBudget(budget: Budget) + + @Delete + suspend fun deleteBudget(budget: Budget) + + @Query("DELETE FROM budget_table") + suspend fun deleteAllBudgets() + + @Query("DELETE FROM budget_table WHERE expenseCategoryKey = :expenseCategoryKey") + suspend fun deleteBudgetsByExpenseCategoryKey(expenseCategoryKey: String) + + @Query("DELETE FROM budget_table WHERE month = :month AND year = :year") + suspend fun deleteBudgetsByMonthAndYear(month: Int, year: Int) + + @Query("DELETE FROM budget_table WHERE isSynced = :isSynced") + suspend fun deleteByIsSyncedValue(isSynced: Boolean) + + @Query("SELECT * FROM budget_table") + fun getAllBudgets(): Flow> + + @Query("SELECT * FROM budget_table WHERE `key` IN (:keyList)") + fun getAllBudgetsByKey(keyList: List): Flow> + + @Query("SELECT * FROM budget_table where month = :month and year = :year order by created DESC") + fun getAllBudgetsByMonthAndYear(month: Int, year: Int): Flow> + + @Query("SELECT * FROM budget_table where monthYearString = :monthYearString order by created DESC") + fun getAllBudgetsByMonthAndYearString(monthYearString: String): Flow> + + @Query("SELECT year FROM budget_table order by year ASC LIMIT 1") + fun getTheOldestSavedBudgetYear(): Flow + + @Query("SELECT * FROM budget_table where `key` = :budgetKey") + fun getBudgetBuyBudgetKey(budgetKey: String): Flow + + @Query("SELECT expenseCategoryKey FROM budget_table where month = :month and year = :year") + fun getExpenseCategoryKeysOfAllBudgetsByMonthAndYear(month: Int, year: Int): Flow> + + @Query("SELECT SUM(budgetLimit) FROM budget_table where month = :month and year = :year") + fun getTotalBudgetByMonthAndYear(month: Int, year: Int): Flow + + @Query("SELECT month, SUM(budgetLimit) AS total FROM budget_table where year = :year GROUP BY month, year") + fun getAllTotalBudgetByYear(year: Int): Flow> + + @Query("UPDATE budget_table SET isSynced = 0 WHERE `key` = :key") + suspend fun updateIsSyncedValueToFalse(key: String) + + @Query("SELECT `key` FROM budget_table WHERE expenseCategoryKey = :expenseCategoryKey") + suspend fun getKeysByExpenseCategoryKey(expenseCategoryKey: String): List + + @Query("SELECT `key` FROM budget_table WHERE month = :month AND year = :year") + suspend fun getKeysByMonthAndYear(month: Int, year: Int): List + + @Query("SELECT DISTINCT monthYearString FROM budget_table ORDER BY year DESC, month DESC") + fun getAllBudgetMonthAndYearForWhichBudgetIsAdded(): Flow> + + @Query("SELECT `key` FROM budget_table WHERE monthYearString = :monthYearString LIMIT 2") + fun isAnyBudgetAddedForThisMonthAndYear(monthYearString: String): Flow> +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseCategoryDAO.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseCategoryDAO.kt index 34e58ad4..4596af6c 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseCategoryDAO.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseCategoryDAO.kt @@ -28,9 +28,15 @@ interface ExpenseCategoryDAO { @Query("SELECT * FROM expense_category_table ORDER BY modified DESC") fun getAllExpenseCategories(): Flow> + @Query("SELECT * FROM expense_category_table limit :limit") + fun getAllExpenseCategoriesByLimit(limit: Int): Flow> + @Query("SELECT * FROM expense_category_table WHERE `key` = :key") fun getExpenseCategoryByKey(key: String): Flow + @Query("SELECT * FROM expense_category_table WHERE `key` in (:expenseCategoryKeys)") + fun getExpenseCategoriesByKey(expenseCategoryKeys: List): Flow> + @Query("UPDATE expense_category_table SET isSynced = 0 WHERE `key` = :key") suspend fun updateIsSyncedValueToFalse(key: String) } diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseDAO.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseDAO.kt index d3754afe..8ff5e7ba 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseDAO.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/ExpenseDAO.kt @@ -1,6 +1,7 @@ package com.rohitthebest.manageyourrenters.database.dao import androidx.room.* +import com.rohitthebest.manageyourrenters.data.ExpenseCategoryAndTheirTotalExpenseAmounts import com.rohitthebest.manageyourrenters.database.model.Expense import kotlinx.coroutines.flow.Flow @@ -60,6 +61,26 @@ interface ExpenseDAO { date2: Long ): Flow + @Query("SELECT SUM(amount) FROM expense_table WHERE categoryKey IN (:expenseCategoryKeys)") + fun getTotalExpenseByCategoryKeys( + expenseCategoryKeys: List + ): Flow + + + @Query("SELECT SUM(amount) FROM expense_table WHERE categoryKey IN (:expenseCategoryKeys) AND created BETWEEN :date1 AND :date2") + fun getTotalExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys: List, + date1: Long, + date2: Long + ): Flow + + @Query("SELECT * FROM expense_table WHERE categoryKey IN (:expenseCategoryKeys) AND created BETWEEN :date1 AND :date2 order by created DESC") + fun getExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys: List, + date1: Long, + date2: Long + ): Flow> + @Query("SELECT * FROM expense_table WHERE `key` = :expenseKey") fun getExpenseByKey(expenseKey: String): Flow @@ -89,4 +110,98 @@ interface ExpenseDAO { @Query("UPDATE expense_table SET isSynced = 0 WHERE `key` = :key") suspend fun updateIsSyncedValueToFalse(key: String) + + @Query( + "SELECT expense_category_table.`key` as expenseCategoryKey, " + + "expense_category_table.categoryName AS categoryName, " + + "SUM(expense_table.amount) AS totalAmount " + + "FROM expense_table " + + "INNER JOIN expense_category_table " + + "ON expense_table.categoryKey = expense_category_table.`key` " + + "GROUP BY expense_category_table.`key`" + ) + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeys(): Flow> + + @Query( + "SELECT expense_category_table.`key` as expenseCategoryKey, " + + "expense_category_table.categoryName AS categoryName, " + + "SUM(expense_table.amount) AS totalAmount " + + "FROM expense_table " + + "INNER JOIN expense_category_table " + + "ON expense_table.categoryKey = expense_category_table.`key` " + + "WHERE expense_table.`key` in (:expenseKeys) " + + "GROUP BY expense_category_table.`key`" + ) + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByListOfExpenseKeys( + expenseKeys: List + ): Flow> + + + @Query( + "SELECT expense_category_table.`key` AS expenseCategoryKey, " + + "expense_category_table.categoryName AS categoryName, " + + "SUM(expense_table.amount) AS totalAmount " + + "FROM expense_table " + + "INNER JOIN expense_category_table " + + "ON expense_table.categoryKey = expense_category_table.`key` " + + "WHERE expense_table.created BETWEEN :date1 AND :date2 " + + "GROUP BY expense_category_table.`key`" + ) + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRange( + date1: Long, + date2: Long + ): Flow> + + @Query( + "SELECT expense_category_table.`key` AS expenseCategoryKey, " + + "expense_category_table.categoryName AS categoryName, " + + "SUM(expense_table.amount) AS totalAmount " + + "FROM expense_table " + + "INNER JOIN expense_category_table " + + "ON expense_table.categoryKey = expense_category_table.`key` " + + "WHERE expense_table.created BETWEEN :date1 AND :date2 " + + "AND expense_table.`key` IN (:expenseKeys) " + + "GROUP BY expense_category_table.`key`" + ) + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRangeAndByListOfExpenseKeys( + date1: Long, + date2: Long, + expenseKeys: List + ): Flow> + + + @Query( + "SELECT expense_category_table.`key` AS expenseCategoryKey, " + + "expense_category_table.categoryName AS categoryName, " + + "SUM(expense_table.amount) AS totalAmount " + + "FROM expense_table " + + "INNER JOIN expense_category_table " + + "ON expense_table.categoryKey = expense_category_table.`key` " + + "WHERE expense_table.categoryKey IN (:selectedExpenseCategories) " + + "GROUP BY expense_category_table.`key`" + ) + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategories( + selectedExpenseCategories: List + ): Flow> + + @Query( + "SELECT expense_category_table.`key` AS expenseCategoryKey, " + + "expense_category_table.categoryName AS categoryName, " + + "SUM(expense_table.amount) AS totalAmount " + + "FROM expense_table " + + "INNER JOIN expense_category_table " + + "ON expense_table.categoryKey = expense_category_table.`key` " + + "WHERE expense_table.categoryKey IN (:selectedExpenseCategories) " + + "AND expense_table.created BETWEEN :date1 AND :date2 " + + "GROUP BY expense_category_table.`key`" + ) + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategoriesByDateRange( + selectedExpenseCategories: List, + date1: Long, + date2: Long + ): Flow> + + + @Query("SELECT EXISTS(select `key` from expense_table limit 1)") + fun isAnyExpenseAdded(): Flow } diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/IncomeDao.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/IncomeDao.kt new file mode 100644 index 00000000..7723c579 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/IncomeDao.kt @@ -0,0 +1,50 @@ +package com.rohitthebest.manageyourrenters.database.dao + +import androidx.room.* +import com.rohitthebest.manageyourrenters.data.MonthAndTotalSum +import com.rohitthebest.manageyourrenters.database.model.Income +import kotlinx.coroutines.flow.Flow + +@Dao +interface IncomeDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertIncome(income: Income) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAllIncome(incomes: List) + + @Update + suspend fun updateIncome(income: Income) + + @Delete + suspend fun deleteIncome(income: Income) + + @Query("DELETE FROM income_table WHERE isSynced = :isSynced") + suspend fun deleteByIsSyncedValue(isSynced: Boolean) + + @Query("DELETE FROM income_table") + suspend fun deleteAllIncomes() + + @Query("SELECT * FROM income_table") + fun getAllIncomes(): Flow> + + @Query("SELECT * FROM income_table where month = :month AND year = :year") + fun getAllIncomesByMonthAndYear(month: Int, year: Int): Flow> + + @Query("SELECT * FROM income_table where `key` = :incomeKey") + fun getIncomeByKey(incomeKey: String): Flow + + @Query("SELECT SUM(income) FROM income_table where month = :month AND year = :year") + fun getTotalIncomeAddedByMonthAndYear(month: Int, year: Int): Flow + + @Query("SELECT month, SUM(income) AS total FROM income_table where year = :year GROUP BY month, year") + fun getAllTotalIncomeByYear(year: Int): Flow> + + + @Query("SELECT DISTINCT source FROM income_table") + fun getAllIncomeSources(): Flow> + + @Query("UPDATE income_table SET isSynced = 0 WHERE `key` = :key") + suspend fun updateIsSyncedValueToFalse(key: String) +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/RenterDao.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/RenterDao.kt index 21abb6a3..4c378a57 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/RenterDao.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/dao/RenterDao.kt @@ -55,6 +55,10 @@ interface RenterDao { endDate: Long ): Flow>> + @MapInfo(keyColumn = "renterName", valueColumn = "dues") + @Query("SELECT renter_table.name AS renterName, renter_table.dueOrAdvanceAmount AS dues FROM renter_table WHERE dues < 0") + fun getRentersWithTheirDues(): Flow> + @Query("SELECT DISTINCT address FROM renter_table") fun getAllDistinctAddress(): Flow> diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/databases/BudgetAndIncomeDatabase.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/databases/BudgetAndIncomeDatabase.kt new file mode 100644 index 00000000..2581bf94 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/databases/BudgetAndIncomeDatabase.kt @@ -0,0 +1,22 @@ +package com.rohitthebest.manageyourrenters.database.databases + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.rohitthebest.manageyourrenters.database.dao.BudgetDao +import com.rohitthebest.manageyourrenters.database.dao.IncomeDao +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.database.typeConverters.TypeConvertersForDatabase + +@Database( + entities = [Budget::class, Income::class], + version = 1, + exportSchema = false +) +@TypeConverters(TypeConvertersForDatabase::class) +abstract class BudgetAndIncomeDatabase : RoomDatabase() { + + abstract fun getBudgetDao(): BudgetDao + abstract fun getIncomeDao(): IncomeDao +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/model/Budget.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/model/Budget.kt new file mode 100644 index 00000000..a4b7d5a9 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/model/Budget.kt @@ -0,0 +1,53 @@ +package com.rohitthebest.manageyourrenters.database.model + +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey +import com.google.firebase.firestore.Exclude +import com.google.firebase.firestore.IgnoreExtraProperties + +@Entity(tableName = "budget_table") +@IgnoreExtraProperties +data class Budget( + @PrimaryKey(autoGenerate = false) var key: String, + var created: Long, + var modified: Long, + var budgetLimit: Double, + var month: Int, + var year: Int, + var expenseCategoryKey: String, + var monthYearString: String, + var isSynced: Boolean, + var uid: String +) { + + @Exclude + @Ignore + var categoryName: String = "" + + @Exclude + @Ignore + var categoryImageUrl: String = "" + + @Exclude + @Ignore + var currentExpenseAmount: Double = 0.0 + + constructor() : this( + "", + System.currentTimeMillis(), + System.currentTimeMillis(), + 0.0, + 0, + 0, + "", + "", + false, + "" + ) + + fun generateMonthYearString(): String { + return "${this.month}_${this.year}" + } + +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/database/model/Income.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/database/model/Income.kt new file mode 100644 index 00000000..13914c70 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/database/model/Income.kt @@ -0,0 +1,67 @@ +package com.rohitthebest.manageyourrenters.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.firebase.firestore.IgnoreExtraProperties +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.utils.isValid + +@Entity(tableName = "income_table") +@IgnoreExtraProperties +data class Income( + @PrimaryKey(autoGenerate = false) var key: String, + var created: Long, + var modified: Long, + var source: String, + var income: Double, + var month: Int, + var year: Int, + var monthYearString: String, + var isSynced: Boolean, + var uid: String, + var linkedPaymentMethods: List? = null +) { + constructor() : this( + "", + System.currentTimeMillis(), + System.currentTimeMillis(), + "", + 0.0, + 0, + 0, + "", + false, + "", + null + ) + + fun generateMonthYearString(): String { + return "${this.month}_${this.year}" + } + + fun getPaymentMethodString(paymentMethodsMap: Map): String { + + return if (this.linkedPaymentMethods == null) { + Constants.OTHER_PAYMENT_METHOD + } else { + var paymentMethods = "" + this.linkedPaymentMethods!!.forEachIndexed { index, pm -> + + if (paymentMethodsMap[pm] != null) { + paymentMethods += paymentMethodsMap[pm] + } + + if (index != this.linkedPaymentMethods!!.size - 1) { + paymentMethods += " | " + } + } + + if (!paymentMethods.isValid()) { + paymentMethods = Constants.OTHER_PAYMENT_METHOD + } + + paymentMethods + } + } + +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/module/Module.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/module/Module.kt index 11fe3a52..78d2a02d 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/module/Module.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/module/Module.kt @@ -8,6 +8,7 @@ import com.rohitthebest.manageyourrenters.api.unsplash.UnsplashAPI import com.rohitthebest.manageyourrenters.database.databases.* import com.rohitthebest.manageyourrenters.others.Constants.BORROWER_DATABASE_NAME import com.rohitthebest.manageyourrenters.others.Constants.BORROWER_PAYMENT_DATABASE_NAME +import com.rohitthebest.manageyourrenters.others.Constants.BUDGET_AND_INCOME_DATABASE import com.rohitthebest.manageyourrenters.others.Constants.EMI_DATABASE_NAME import com.rohitthebest.manageyourrenters.others.Constants.EXPENSE_DATABASE_NAME import com.rohitthebest.manageyourrenters.others.Constants.MONTHLY_PAYMENT_DATABASE_NAME @@ -283,6 +284,29 @@ object Module { // ================================================================================== + // ----------------------------- Income and Budget ---------------------------------------- + + @Singleton + @Provides + fun getBudgetAndIncomeDatabase( + @ApplicationContext context: Context + ) = Room.databaseBuilder( + context, + BudgetAndIncomeDatabase::class.java, + BUDGET_AND_INCOME_DATABASE + ).build() + + @Provides + @Singleton + fun getBudgetDao(db: BudgetAndIncomeDatabase) = db.getBudgetDao() + + @Provides + @Singleton + fun getIncomeDao(db: BudgetAndIncomeDatabase) = db.getIncomeDao() + + // ================================================================================== + + // ------------------------------ Expense Category API ----------------------------------- @Provides diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/others/Constants.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/others/Constants.kt index c4f2129a..f939ee48 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/others/Constants.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/others/Constants.kt @@ -7,7 +7,7 @@ object Constants { const val UNSPLASH_BASE_URL = "https://api.unsplash.com/" const val NO_INTERNET_MESSAGE = "Please check your Internet connection!!!" - const val EDIT_TEXT_EMPTY_MESSAGE = "*This is a mandatory field...!!" + const val EDIT_TEXT_EMPTY_MESSAGE = "*This is a mandatory field..." // ----- databases name RELATED--------- const val RENTER_AND_PAYMENT_DATABASE_NAME = "renter_and_payment_database.db" @@ -18,10 +18,14 @@ object Constants { const val EXPENSE_DATABASE_NAME = "expense_database.db" const val MONTHLY_PAYMENT_DATABASE_NAME = "monthly_payment_db" const val PAYMENT_METHOD_DATABASE_NAME = "payment_method_db" + const val BUDGET_AND_INCOME_DATABASE = "budget_income_database.db" //---------------------------- + //------------ const val NOTIFICATION_CHANNEL_ID = "NotificationChannelID" + //------------ + //-------- Firestore---------- const val COLLECTION_KEY = "Colection_key_dskjsadaaddhadkjhbskjbvjhb" const val DOCUMENT_KEY = "Document_key_dskjshfjksadadbskjbvjhb" const val UPDATE_DOCUMENT_MAP_KEY = "UPDATE_DOCUMENT_MAP_KEYadsadsaDF_dskjshfdfsdadkjhbskjbvjhb" @@ -29,16 +33,19 @@ object Constants { const val RANDOM_ID_KEY = "RANDOM_ID_KEY_sdjhdsdvjkbvbavbhbvhjbhjbvdb" const val KEY_LIST_KEY = "KEY_LIST_KEY_fdhvjkhjkvkjvkbvk" const val DELETE_FILE_FROM_FIREBASE_KEY = "DELETE_FILE_FROM_FIREBASE_KEY_sjvvbibisbvbaib" + //----------------------------- + //--------- Shared Preference----------- const val IS_SYNCED_SHARED_PREF_NAME = "IS_SYNCED_SHARED_PREF_NAME_fdkdnf" const val IS_SYNCED_SHARED_PREF_KEY = "IS_SYNCED_SHARED_PREF_KEY_fkjdvkjdbdnf" const val CUSTOM_DATE_RANGE_FOR_GRAPH_FRAGMENT_SHARED_PREF_NAME = "djbbbwebhbBKJBJBSHBHBJbbsjb" const val CUSTOM_DATE_RANGE_FOR_GRAPH_FRAGMENT_SHARED_PREF_KEY = "sbjsbbhjsbhbyguqyJBABBHJBkbnd" + // -------------------------------------- const val SUPPORTING_DOCUMENT_HELPER_MODEL_KEY = "dsbjsbjhbuyavvu" const val RENTER_PAYMENT_CONFIRMATION_BILL_KEY = "BNZBNJBHVDLK&hjbxjhbskxknxB" - // Menu BottomSheet related + //--- Menu BottomSheet related--------- const val SHOW_EDIT_MENU = "SHOW_EDIT_MENU_sjcjwncjkn" const val SHOW_DOCUMENTS_MENU = "SHOW_DOCUMENTS_MENU_sjcjwncacacn" const val SHOW_DELETE_MENU = "DELETE_MENUbkjbskjc" @@ -46,6 +53,7 @@ object Constants { const val SHOW_MOVE_MENU = "MOVE_SAdhbhfbABJnbhsb" const val SHOW_COPY_MENU = "COPY_SANJABABJnbhhsb" const val COPY_MENU_TEXT = "COPY-TEXT_SwwAdsdrBAbuh=hsb" + //--------------------------- const val NETWORK_PAGE_SIZE_UNSPLASH = 30 const val SHORTCUT_FRAGMENT_NAME_KEY = "sdcjnsknkanckbajcbabacjk" @@ -54,17 +62,20 @@ object Constants { const val FILE_PROVIDER_AUTHORITY = "com.rohitthebest.manageyourrenters.provider" + // -------- Shortcuts --------- const val SHORTCUT_EXPENSE = "com.rohitthebest.manageyourrenters.expense" const val SHORTCUT_BORROWERS = "com.rohitthebest.manageyourrenters.borrowers" const val SHORTCUT_HOUSE_RENTERS = "com.rohitthebest.manageyourrenters.house_renters" const val SHORTCUT_MONTHLY_PAYMENTS = "com.rohitthebest.manageyourrenters.monthly_payments" const val SHORTCUT_EMI = "com.rohitthebest.manageyourrenters.emis" + // ---------------------------- - // app update related + //--- app update related------ const val APP_VERSION = BuildConfig.VERSION_NAME const val APP_UPDATE_FIRESTORE_DOCUMENT_KEY = "latest_app_version" + //------------------------------ - // Default payment method keys + //---- Default payment method keys------- const val PAYMENT_METHOD_CASH_KEY = "payment_method_cash_1316797_rrrrr" const val PAYMENT_METHOD_DEBIT_CARD_KEY = "payment_method_debit_card_5765763_rrrrr" const val PAYMENT_METHOD_CREDIT_CARD_KEY = "payment_method_credit_card_974673_rrrrr" @@ -74,6 +85,7 @@ object Constants { const val OTHER_PAYMENT_METHOD = "💰 OTHER" const val DEBIT_CARD_PAYMENT_METHOD = "💳 DEBIT CARD" const val CREDIT_CARD_PAYMENT_METHOD = "💳 CREDIT CARD" + // -------------------------------------- const val ADD_PAYMENT_METHOD_KEY = "add_payment_method_key" // will be used only for recycler view @@ -83,9 +95,22 @@ object Constants { const val PAYMENT_METHOD_KEY_FOR_EDIT = "PAYMENT_METHOD_KEY_FOR_EDIT_KEY" const val EXPENSE_FILTER_KEY = "EXPENSE_FILTER_KEY_sdbjhsbjs" + const val BUDGET = "passing_budget_object_using_bundle_key" const val SERVICE_STOP_TIME_IN_SECONDS: Long = 45 const val GENERIC_KEY_FOR_ACTIVITY_OR_FRAGMENT_COMMUNICATION = "GENERIC_KEY_skdjhshjsvhjs" const val GENERIC_KEY_FOR_ACTIVITY_OR_FRAGMENT_COMMUNICATION2 = "GENERIC_KEY_2**cnxjhshjxfnvkjss" + + const val INCOME_MONTH_KEY = "hjsjhsvhjvsvyugsgfgwv" + const val INCOME_YEAR_KEY = "sdghjsbhjsvhshghgschgc" + + //----------------------- Month and Year Picker Dialog --------------------------- + const val MONTH_YEAR_PICKER_MONTH_KEY = "sbhsbhjbjhdvgsfycsgfcgfwc" + const val MONTH_YEAR_PICKER_YEAR_KEY = "hsbjhsvhgvshgsgcfscgfcsg" + const val MONTH_YEAR_PICKER_MIN_YEAR_KEY = "sdhbsjhhjsvgsvghsvhgshhgs" + const val MONTH_YEAR_PICKER_MAX_YEAR_KEY = "jbhjsvgsygsfrtsfgctradxaa" + //-------------------------------------------------------------------------------- + + const val COPY_BUDGET_MONTH_AND_YEAR_KEY = "rdnjshbhsvygacfcfacfca" } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/others/FirestoreCollectionsConstants.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/others/FirestoreCollectionsConstants.kt index cd89b8ee..6b54a753 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/others/FirestoreCollectionsConstants.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/others/FirestoreCollectionsConstants.kt @@ -15,4 +15,6 @@ object FirestoreCollectionsConstants { const val PARTIAL_PAYMENTS = "Partial_Payments" const val APP_UPDATES = "AppUpdates" const val PAYMENT_METHODS = "PaymentMethods" + const val BUDGETS = "Budgets" + const val INCOMES = "Incomes" } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/BudgetRepository.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/BudgetRepository.kt new file mode 100644 index 00000000..57055831 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/BudgetRepository.kt @@ -0,0 +1,72 @@ +package com.rohitthebest.manageyourrenters.repositories + +import com.rohitthebest.manageyourrenters.database.dao.BudgetDao +import com.rohitthebest.manageyourrenters.database.model.Budget +import javax.inject.Inject + +class BudgetRepository @Inject constructor( + val dao: BudgetDao +) { + + suspend fun insertBudget(budget: Budget) = dao.insertBudget(budget) + + suspend fun insertAllBudget(budgets: List) = + dao.insertAllBudget(budgets) + + suspend fun updateBudget(budget: Budget) = + dao.updateBudget(budget) + + suspend fun deleteBudget(budget: Budget) = + dao.deleteBudget(budget) + + suspend fun deleteAllBudgets() = dao.deleteAllBudgets() + + suspend fun deleteBudgetsByExpenseCategoryKey(expenseCategoryKey: String) = + dao.deleteBudgetsByExpenseCategoryKey(expenseCategoryKey) + + suspend fun deleteBudgetsByMonthAndYear(month: Int, year: Int) = + dao.deleteBudgetsByMonthAndYear(month, year) + + fun getAllBudgets() = dao.getAllBudgets() + + fun getAllBudgetsByKey(keyList: List) = dao.getAllBudgetsByKey(keyList) + + fun getAllBudgetsByMonthAndYear(month: Int, year: Int) = + dao.getAllBudgetsByMonthAndYear(month, year) + + fun getAllBudgetsByMonthAndYearString(monthYearString: String) = + dao.getAllBudgetsByMonthAndYearString(monthYearString) + + fun getTheOldestSavedBudgetYear() = dao.getTheOldestSavedBudgetYear() + + fun getBudgetByKey(budgetKey: String) = dao.getBudgetBuyBudgetKey(budgetKey) + + fun getTotalBudgetByMonthAndYear(month: Int, year: Int) = + dao.getTotalBudgetByMonthAndYear(month, year) + + fun getAllTotalBudgetByYear(year: Int) = dao.getAllTotalBudgetByYear(year) + + fun getExpenseCategoryKeysOfAllBudgetsByMonthAndYear(month: Int, year: Int) = + dao.getExpenseCategoryKeysOfAllBudgetsByMonthAndYear( + month, year + ) + + suspend fun getKeysByExpenseCategoryKey(expenseCategoryKey: String) = + dao.getKeysByExpenseCategoryKey( + expenseCategoryKey + ) + + suspend fun getKeysByMonthAndYear(month: Int, year: Int) = dao.getKeysByMonthAndYear( + month, year + ) + + suspend fun deleteByIsSyncedValue(isSynced: Boolean) = dao.deleteByIsSyncedValue(isSynced) + + fun getAllBudgetMonthAndYearForWhichBudgetIsAdded() = + dao.getAllBudgetMonthAndYearForWhichBudgetIsAdded() + + fun isAnyBudgetAddedForThisMonthAndYear(monthYearString: String) = + dao.isAnyBudgetAddedForThisMonthAndYear( + monthYearString + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseCategoryRepository.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseCategoryRepository.kt index 110bb702..2a05b295 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseCategoryRepository.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseCategoryRepository.kt @@ -26,7 +26,10 @@ class ExpenseCategoryRepository @Inject constructor( dao.deleteAllExpenseCategoriesByIsSynced(isSynced) fun getAllExpenseCategories() = dao.getAllExpenseCategories() + fun getExpenseCategoriesByKey(expenseCategoryKeys: List) = + dao.getExpenseCategoriesByKey(expenseCategoryKeys) fun getExpenseCategoryByKey(key: String) = dao.getExpenseCategoryByKey(key) + fun getAllExpenseCategoriesByLimit(limit: Int) = dao.getAllExpenseCategoriesByLimit(limit) } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseRepository.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseRepository.kt index 0a27acd5..763f5ceb 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseRepository.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/ExpenseRepository.kt @@ -2,6 +2,7 @@ package com.rohitthebest.manageyourrenters.repositories import com.rohitthebest.manageyourrenters.database.dao.ExpenseDAO import com.rohitthebest.manageyourrenters.database.model.Expense +import com.rohitthebest.manageyourrenters.others.Constants import javax.inject.Inject class ExpenseRepository @Inject constructor( @@ -63,6 +64,22 @@ class ExpenseRepository @Inject constructor( ) = dao.getTotalExpenseAmountByCategoryKeyAndDateRange(expenseCategoryKey, date1, date2) + fun getTotalExpenseByCategoryKeys( + expenseCategoryKeys: List + ) = dao.getTotalExpenseByCategoryKeys(expenseCategoryKeys) + + fun getTotalExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys: List, + date1: Long, + date2: Long + ) = dao.getTotalExpenseByCategoryKeysAndDateRange(expenseCategoryKeys, date1, date2) + + fun getExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys: List, + date1: Long, + date2: Long + ) = dao.getExpenseByCategoryKeysAndDateRange(expenseCategoryKeys, date1, date2) + fun getExpenseByDateRangeAndExpenseCategoryKey( expenseCategoryKey: String, date1: Long, date2: Long ) = dao.getExpenseByDateRangeAndExpenseCategoryKey(expenseCategoryKey, date1, date2) @@ -80,4 +97,62 @@ class ExpenseRepository @Inject constructor( suspend fun getKeysByExpenseCategoryKey(expenseCategoryKey: String) = dao.getKeysByExpenseCategoryKey(expenseCategoryKey) + + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeys() = + dao.getTotalExpenseAmountsWithTheirExpenseCategoryKeys() + + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByListOfExpenseKeys( + expenseKeys: List + ) = dao.getTotalExpenseAmountsWithTheirExpenseCategoryKeysByListOfExpenseKeys(expenseKeys) + + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRange(date1: Long, date2: Long) = + dao.getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRange(date1, date2) + + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRangeAndByListOfExpenseKeys( + date1: Long, + date2: Long, + expenseKeys: List + ) = dao.getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRangeAndByListOfExpenseKeys( + date1, date2, expenseKeys + ) + + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategories( + selectedExpenseCategories: List + ) = dao.getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategories( + selectedExpenseCategories + ) + + fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategoriesByDateRange( + selectedExpenseCategories: List, + date1: Long, + date2: Long + ) = + dao.getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategoriesByDateRange( + selectedExpenseCategories, + date1, + date2 + ) + + fun isAnyExpenseAdded() = dao.isAnyExpenseAdded() + + fun applyExpenseFilterByPaymentMethods( + paymentMethodKeys: List, + expenses: List + ): List { + + val isOtherPaymentMethodKeyPresent = + paymentMethodKeys.contains(Constants.PAYMENT_METHOD_OTHER_KEY) + + val resultExpenses = expenses.filter { expense -> + + if (isOtherPaymentMethodKeyPresent) { + // for other payment method, get all the expenses where payment methods is null as well as payment method is other + expense.paymentMethods == null || expense.paymentMethods!!.any { it in paymentMethodKeys } + } else { + expense.paymentMethods != null && expense.paymentMethods!!.any { it in paymentMethodKeys } + } + } + + return resultExpenses + } } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/IncomeRepository.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/IncomeRepository.kt new file mode 100644 index 00000000..7ffaf97c --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/IncomeRepository.kt @@ -0,0 +1,61 @@ +package com.rohitthebest.manageyourrenters.repositories + +import com.rohitthebest.manageyourrenters.database.dao.IncomeDao +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.others.Constants +import javax.inject.Inject + +class IncomeRepository @Inject constructor( + val dao: IncomeDao +) { + + suspend fun insertIncome(income: Income) = dao.insertIncome(income) + + suspend fun insertAllIncome(incomes: List) = + dao.insertAllIncome(incomes) + + suspend fun updateIncome(income: Income) = + dao.updateIncome(income) + + suspend fun deleteIncome(income: Income) = + dao.deleteIncome(income) + + suspend fun deleteAllIncomes() = dao.deleteAllIncomes() + + fun getAllIncomes() = dao.getAllIncomes() + + fun getAllIncomesByMonthAndYear(month: Int, year: Int) = + dao.getAllIncomesByMonthAndYear(month, year) + + fun getIncomeByKey(incomeKey: String) = dao.getIncomeByKey(incomeKey) + + fun getTotalIncomeAddedByMonthAndYear(month: Int, year: Int) = + dao.getTotalIncomeAddedByMonthAndYear(month, year) + + fun getAllTotalIncomeByYear(year: Int) = dao.getAllTotalIncomeByYear(year) + + fun getAllIncomeSources() = dao.getAllIncomeSources() + suspend fun deleteByIsSyncedValue(isSynced: Boolean) = dao.deleteByIsSyncedValue(isSynced) + + fun applyFilterByPaymentMethods( + paymentMethodKeys: List, + incomes: List + ): List { + + val isOtherPaymentMethodKeyPresent = + paymentMethodKeys.contains(Constants.PAYMENT_METHOD_OTHER_KEY) + + val resultIncomes = incomes.filter { income -> + + if (isOtherPaymentMethodKeyPresent) { + // for other payment method, get all the expenses where payment methods is null as well as payment method is other + income.linkedPaymentMethods == null || income.linkedPaymentMethods!!.any { it in paymentMethodKeys } + } else { + income.linkedPaymentMethods != null && income.linkedPaymentMethods!!.any { it in paymentMethodKeys } + } + } + + return resultIncomes + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/RenterRepository.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/RenterRepository.kt index 3900e4a1..1c27391b 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/RenterRepository.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/RenterRepository.kt @@ -37,4 +37,6 @@ class RenterRepository @Inject constructor( ) = dao.getRentersWithTheirAmountPaidByDateCreated(startDate, endDate) fun getAllDistinctAddress() = dao.getAllDistinctAddress() + + fun getRentersWithTheirDues() = dao.getRentersWithTheirDues() } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/UpdateIsSyncedValueForAnyTable.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/UpdateIsSyncedValueForAnyTable.kt index 9a076efd..7f257205 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/UpdateIsSyncedValueForAnyTable.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/repositories/UpdateIsSyncedValueForAnyTable.kt @@ -1,6 +1,18 @@ package com.rohitthebest.manageyourrenters.repositories -import com.rohitthebest.manageyourrenters.database.dao.* +import com.rohitthebest.manageyourrenters.database.dao.BorrowerDao +import com.rohitthebest.manageyourrenters.database.dao.BorrowerPaymentDao +import com.rohitthebest.manageyourrenters.database.dao.BudgetDao +import com.rohitthebest.manageyourrenters.database.dao.EMIDao +import com.rohitthebest.manageyourrenters.database.dao.EMIPaymentDao +import com.rohitthebest.manageyourrenters.database.dao.ExpenseCategoryDAO +import com.rohitthebest.manageyourrenters.database.dao.ExpenseDAO +import com.rohitthebest.manageyourrenters.database.dao.IncomeDao +import com.rohitthebest.manageyourrenters.database.dao.MonthlyPaymentCategoryDao +import com.rohitthebest.manageyourrenters.database.dao.MonthlyPaymentDao +import com.rohitthebest.manageyourrenters.database.dao.PaymentMethodDao +import com.rohitthebest.manageyourrenters.database.dao.RenterDao +import com.rohitthebest.manageyourrenters.database.dao.RenterPaymentDao import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants import javax.inject.Inject @@ -15,7 +27,9 @@ class UpdateIsSyncedValueForAnyTableRepository @Inject constructor( private val expenseDAO: ExpenseDAO, private val expenseCategoryDAO: ExpenseCategoryDAO, private val monthlyPaymentDao: MonthlyPaymentDao, - private val monthlyPaymentCategoryDAO: MonthlyPaymentCategoryDao + private val monthlyPaymentCategoryDAO: MonthlyPaymentCategoryDao, + private val budgetDao: BudgetDao, + private val incomeDao: IncomeDao ) { suspend fun updateIsSyncValueToFalse(collection: String, key: String) { @@ -64,6 +78,14 @@ class UpdateIsSyncedValueForAnyTableRepository @Inject constructor( FirestoreCollectionsConstants.PAYMENT_METHODS -> { paymentMethodDao.updateIsSyncedValueToFalse(key) } + + FirestoreCollectionsConstants.BUDGETS -> { + budgetDao.updateIsSyncedValueToFalse(key) + } + + FirestoreCollectionsConstants.INCOMES -> { + incomeDao.updateIsSyncedValueToFalse(key) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/services/SyncDocumentsFromFirestoreService.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/services/SyncDocumentsFromFirestoreService.kt index 656cc796..5d6773c7 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/services/SyncDocumentsFromFirestoreService.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/services/SyncDocumentsFromFirestoreService.kt @@ -7,14 +7,44 @@ import android.util.Log import androidx.core.app.NotificationCompat import com.rohitthebest.manageyourrenters.R import com.rohitthebest.manageyourrenters.data.BillPeriodType -import com.rohitthebest.manageyourrenters.database.model.* +import com.rohitthebest.manageyourrenters.database.model.Borrower +import com.rohitthebest.manageyourrenters.database.model.BorrowerPayment +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.database.model.EMI +import com.rohitthebest.manageyourrenters.database.model.EMIPayment +import com.rohitthebest.manageyourrenters.database.model.Expense +import com.rohitthebest.manageyourrenters.database.model.ExpenseCategory +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.database.model.MonthlyPayment +import com.rohitthebest.manageyourrenters.database.model.MonthlyPaymentCategory +import com.rohitthebest.manageyourrenters.database.model.PartialPayment +import com.rohitthebest.manageyourrenters.database.model.PaymentMethod +import com.rohitthebest.manageyourrenters.database.model.Renter +import com.rohitthebest.manageyourrenters.database.model.RenterPayment import com.rohitthebest.manageyourrenters.others.Constants import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants -import com.rohitthebest.manageyourrenters.repositories.* +import com.rohitthebest.manageyourrenters.repositories.BorrowerPaymentRepository +import com.rohitthebest.manageyourrenters.repositories.BorrowerRepository +import com.rohitthebest.manageyourrenters.repositories.BudgetRepository +import com.rohitthebest.manageyourrenters.repositories.EMIPaymentRepository +import com.rohitthebest.manageyourrenters.repositories.EMIRepository +import com.rohitthebest.manageyourrenters.repositories.ExpenseCategoryRepository +import com.rohitthebest.manageyourrenters.repositories.ExpenseRepository +import com.rohitthebest.manageyourrenters.repositories.IncomeRepository +import com.rohitthebest.manageyourrenters.repositories.MonthlyPaymentCategoryRepository +import com.rohitthebest.manageyourrenters.repositories.MonthlyPaymentRepository +import com.rohitthebest.manageyourrenters.repositories.PartialPaymentRepository +import com.rohitthebest.manageyourrenters.repositories.PaymentMethodRepository +import com.rohitthebest.manageyourrenters.repositories.RenterPaymentRepository +import com.rohitthebest.manageyourrenters.repositories.RenterRepository import com.rohitthebest.manageyourrenters.utils.Functions import com.rohitthebest.manageyourrenters.utils.getDataFromFireStore import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -59,6 +89,12 @@ class SyncDocumentsFromFirestoreService : Service() { @Inject lateinit var paymentMethodRepository: PaymentMethodRepository + @Inject + lateinit var budgetRepository: BudgetRepository + + @Inject + lateinit var incomeRepository: IncomeRepository + private var uid = "" @@ -90,11 +126,28 @@ class SyncDocumentsFromFirestoreService : Service() { syncPaymentMethods() } + val job7 = CoroutineScope(Dispatchers.IO).launch { + syncBudgets() + } + + val job8 = CoroutineScope(Dispatchers.IO).launch { + syncIncomes() + } + + CoroutineScope(Dispatchers.IO).launch { delay(50) - while (!job1.isCompleted || !job2.isCompleted || !job3.isCompleted || !job4.isCompleted || !job5.isCompleted || !job6.isCompleted) { + while (!job1.isCompleted + || !job2.isCompleted + || !job3.isCompleted + || !job4.isCompleted + || !job5.isCompleted + || !job6.isCompleted + || !job7.isCompleted + || !job8.isCompleted + ) { delay(100) Log.d(TAG, "onStartCommand: sync time extended") @@ -390,6 +443,45 @@ class SyncDocumentsFromFirestoreService : Service() { } } + private suspend fun syncBudgets() { + + getDataFromFireStore( + FirestoreCollectionsConstants.BUDGETS, + uid + ) {}?.let { budgetSnapshot -> + + if (budgetSnapshot.size() != 0) { + + budgetRepository.deleteByIsSyncedValue(true) + delay(50) + budgetRepository.insertAllBudget( + budgetSnapshot.toObjects( + Budget::class.java + ) + ) + } + } + } + + private suspend fun syncIncomes() { + + getDataFromFireStore( + FirestoreCollectionsConstants.INCOMES, + uid + ) {}?.let { incomeSnapshot -> + + if (incomeSnapshot.size() != 0) { + + incomeRepository.deleteByIsSyncedValue(true) + delay(50) + incomeRepository.insertAllIncome( + incomeSnapshot.toObjects( + Income::class.java + ) + ) + } + } + } override fun onBind(intent: Intent?): IBinder? { return null diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UpdateService.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UpdateService.kt index 8184ef56..a38c3456 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UpdateService.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UpdateService.kt @@ -16,6 +16,7 @@ import com.rohitthebest.manageyourrenters.others.Constants.RANDOM_ID_KEY import com.rohitthebest.manageyourrenters.others.Constants.UPDATE_DOCUMENT_MAP_KEY import com.rohitthebest.manageyourrenters.repositories.UpdateIsSyncedValueForAnyTableRepository import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.convertJsonToObject import com.rohitthebest.manageyourrenters.utils.updateDocumentOnFireStore import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope @@ -38,12 +39,7 @@ class UpdateService : Service() { val collection = intent?.getStringExtra(COLLECTION_KEY) val key = intent?.getStringExtra(DOCUMENT_KEY) - val map: HashMap = - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { - intent?.getSerializableExtra(UPDATE_DOCUMENT_MAP_KEY) as HashMap - } else ({ - intent?.getSerializableExtra(UPDATE_DOCUMENT_MAP_KEY, HashMap::class.java) - }) as HashMap + val map = intent?.getStringExtra(UPDATE_DOCUMENT_MAP_KEY)?.convertJsonToObject(HashMap::class.java) as HashMap val randomId = intent?.getIntExtra(RANDOM_ID_KEY, 1000) diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListService.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListService.kt new file mode 100644 index 00000000..a78e67d8 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListService.kt @@ -0,0 +1,4 @@ +package com.rohitthebest.manageyourrenters.services + +private const val TAG = "UploadDocumentListService" + diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListToFireStoreService.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListToFireStoreService.kt index 1ad53d6b..3d3383ae 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListToFireStoreService.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadDocumentListToFireStoreService.kt @@ -7,32 +7,59 @@ import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.WriteBatch import com.rohitthebest.manageyourrenters.R import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BORROWER_PAYMENTS +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BUDGETS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.PARTIAL_PAYMENTS +import com.rohitthebest.manageyourrenters.repositories.BudgetRepository +import com.rohitthebest.manageyourrenters.repositories.UpdateIsSyncedValueForAnyTableRepository import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.convertJSONToStringList import com.rohitthebest.manageyourrenters.utils.fromStringToPartialPaymentList import com.rohitthebest.manageyourrenters.utils.uploadFilesOnFirestore +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.util.concurrent.TimeUnit +import javax.inject.Inject private const val TAG = "UploadDocumentListToFir" +@AndroidEntryPoint class UploadDocumentListToFireStoreService : Service() { + @Inject + lateinit var budgetRepository: BudgetRepository + + @Inject + lateinit var updateIsSyncedValueForAnyTableRepository: UpdateIsSyncedValueForAnyTableRepository + + private lateinit var db: FirebaseFirestore + private lateinit var batch: WriteBatch + + private var isUploadSuccessful = false + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val collection = intent?.getStringExtra(Constants.COLLECTION_KEY) + // this is used when list of objects need to be uploaded directly without db interaction (ex: partialPayments) val uploadData = intent?.getStringExtra(Constants.UPLOAD_DATA_KEY) - val randomId = intent?.getIntExtra(Constants.RANDOM_ID_KEY, 1003) + // this is used when only list of keys are passed and objects are retrieved through db here (ex: budgets) + val keys = intent?.getStringExtra(Constants.KEY_LIST_KEY) + val randomId = intent?.getIntExtra(Constants.RANDOM_ID_KEY, 2003) + + db = FirebaseFirestore.getInstance() + batch = db.batch() val pendingIntent: PendingIntent = Functions.getPendingIntentForForegroundServiceNotification( this, - if (collection == getString(R.string.partialPayments)) getString(R.string.borrowerPayments) else collection + if (collection == PARTIAL_PAYMENTS) BORROWER_PAYMENTS else collection ?: "" ) @@ -44,12 +71,33 @@ class UploadDocumentListToFireStoreService : Service() { ).setSmallIcon(image) .setContentIntent(pendingIntent) .setContentTitle("Uploading list to the $collection...") + .setProgress(100, 0, true) .build() startForeground(randomId!!, notification) - val db = FirebaseFirestore.getInstance() - val batch = db.batch() + val keyList = if (collection != PARTIAL_PAYMENTS) { + convertJSONToStringList(keys) + } else { + emptyList() + } + + // starting stop timer + val stopTimerJob = CoroutineScope(Dispatchers.IO).launch { + + Log.d( + TAG, + "onStartCommand: timer started for ${Constants.SERVICE_STOP_TIME_IN_SECONDS} seconds" + ) + delay(TimeUnit.SECONDS.toMillis(Constants.SERVICE_STOP_TIME_IN_SECONDS)) + + if (!isUploadSuccessful && collection != PARTIAL_PAYMENTS) { + + updateIsSyncedValueToFalse(collection!!, keyList) + } + + stopSelf() + } CoroutineScope(Dispatchers.IO).launch { @@ -64,42 +112,81 @@ class UploadDocumentListToFireStoreService : Service() { if (partialPaymentList.isNotEmpty()) { partialPaymentList.forEach { partialPayment -> - batch.set( - db.collection(collection) - .document(partialPayment.key), + db.collection(collection).document(partialPayment.key), partialPayment ) } - if (uploadFilesOnFirestore(batch)) { - Log.d(TAG, "onStartCommand: List Upload SUCCESSFUL") + isUploadSuccessful = true + stopTimerJob.cancel() stopSelf() } else { Log.d(TAG, "onStartCommand: List Upload UNSUCCESSFUL") + stopTimerJob.cancel() stopSelf() } } else { + stopTimerJob.cancel() + stopSelf() + } + } + + BUDGETS -> { + + if (keyList.isNotEmpty()) { + handleBudgetList(keyList, collection) + } else { + stopTimerJob.cancel() stopSelf() } } + + else -> { + stopTimerJob.cancel() + stopSelf() + } } } + return START_NOT_STICKY + } - // starting stop timer - CoroutineScope(Dispatchers.IO).launch { + private suspend fun handleBudgetList(keyList: List, collection: String) { - Log.d( - TAG, - "onStartCommand: timer started for ${Constants.SERVICE_STOP_TIME_IN_SECONDS} seconds" - ) - delay(TimeUnit.SECONDS.toMillis(Constants.SERVICE_STOP_TIME_IN_SECONDS)) - stopSelf() + val budgetModelList = budgetRepository.getAllBudgetsByKey(keyList).first() + + if (budgetModelList.isNotEmpty()) { + + budgetModelList.forEach { budget -> + batch.set(db.collection(collection).document(budget.key), budget) + } + + if (uploadFilesOnFirestore(batch)) { + + Log.d(TAG, "onStartCommand: Budget List Upload SUCCESSFUL") + isUploadSuccessful = true + stopSelf() + } else { + Log.d(TAG, "onStartCommand: Budget List Upload UNSUCCESSFUL") + + updateIsSyncedValueToFalse(collection, keyList) + stopSelf() + } } + } - return START_NOT_STICKY + private suspend fun updateIsSyncedValueToFalse(collection: String, keyList: List) { + + keyList.forEach { key -> + + key?.let { + updateIsSyncedValueForAnyTableRepository.updateIsSyncValueToFalse( + collection, key + ) + } + } } override fun onBind(intent: Intent?): IBinder? { diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadService.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadService.kt index 9861761b..04275542 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadService.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/services/UploadService.kt @@ -17,10 +17,12 @@ import com.rohitthebest.manageyourrenters.others.Constants.RANDOM_ID_KEY import com.rohitthebest.manageyourrenters.others.Constants.SERVICE_STOP_TIME_IN_SECONDS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BORROWERS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BORROWER_PAYMENTS +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BUDGETS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EMI_PAYMENTS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EMIs import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EXPENSES import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EXPENSE_CATEGORIES +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.INCOMES import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.MONTHLY_PAYMENTS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.MONTHLY_PAYMENT_CATEGORIES import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.PAYMENT_METHODS @@ -79,6 +81,12 @@ class UploadService : Service() { @Inject lateinit var updateIsSyncedValueForAnyTableRepository: UpdateIsSyncedValueForAnyTableRepository + @Inject + lateinit var budgetRepository: BudgetRepository + + @Inject + lateinit var incomeRepository: IncomeRepository + @SuppressLint("UnspecifiedImmutableFlag") override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -180,6 +188,15 @@ class UploadService : Service() { model = paymentMethodRepository.getPaymentMethodByKey(key).first() } + BUDGETS -> { + + model = budgetRepository.getBudgetByKey(key).first() + } + + INCOMES -> { + model = incomeRepository.getIncomeByKey(key).first() + } + else -> { stopTimerJob.cancel() stopSelf() diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/todo.txt b/app/src/main/java/com/rohitthebest/manageyourrenters/todo.txt new file mode 100644 index 00000000..0618d4a4 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/todo.txt @@ -0,0 +1,27 @@ + +- Also in BudgetAndIncomeOverviewFragment, for all the budget, also show message, like, + + * Starting (Early Warning Stage): + + == "Start Tracking Your Spending Now!": Encouraging users to begin monitoring their expenses early on to avoid surprises later. + == "Stay Ahead of Your Budget": Setting a proactive tone, reminding users to be mindful of their spending from the beginning. + == "It's Easier to Stay Within Budget from the Start": Highlighting the advantage of early budget adherence for better financial control. + + * Middle (Approaching the Budget Limit): + == "You're Doing Well, But Be Cautious": Acknowledging their efforts to stick to the budget while urging caution as they get closer to the limit. + == "Your Spending Is Getting Closer to the Limit": Providing a straightforward update on their proximity to the budget cap. + == "Keep an Eye on Your Remaining Budget": Advising users to keep track of their remaining funds to avoid exceeding their limit. + + * End (Near Budget Limit): + == "Alert: You're Approaching Your Budget Limit!": Using stronger language to grab their attention and indicate a critical stage. + == "Last Few Dollars Left – Spend Wisely": Reminding users that they have limited funds remaining and should use them judiciously. + == "You're Almost at Your Budget Limit": Clearly informing users that they are nearing the end of their budget. + + * Budget Exceeded: + == "Oops! You've Exceeded Your Budget": Sympathetic message to acknowledge when they've gone over the budget. + == "Time to Review Your Expenses": Encouraging users to reevaluate their spending habits and adjust accordingly. + == "Learn from This Month and Plan Better Next Time": Encouraging a positive outlook and improvement for future budgeting. + +// done +- click on budge to show overview of a particular budget +- create a fragment for showing the overview \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/activities/HomeActivity.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/activities/HomeActivity.kt index ffb502dc..8407a894 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/activities/HomeActivity.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/activities/HomeActivity.kt @@ -8,6 +8,7 @@ import android.view.View import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import com.google.android.gms.auth.api.signin.GoogleSignIn @@ -32,6 +33,7 @@ import com.rohitthebest.manageyourrenters.others.Constants.SHORTCUT_FRAGMENT_NAM import com.rohitthebest.manageyourrenters.others.Constants.SHORTCUT_HOUSE_RENTERS import com.rohitthebest.manageyourrenters.others.Constants.SHORTCUT_MONTHLY_PAYMENTS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.APP_UPDATES +import com.rohitthebest.manageyourrenters.services.SyncDocumentsFromFirestoreService import com.rohitthebest.manageyourrenters.ui.ProfileBottomSheet import com.rohitthebest.manageyourrenters.ui.viewModels.* import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAvailable @@ -228,15 +230,19 @@ class HomeActivity : AppCompatActivity(), RenterTypeAdapter.OnClickListener, SHORTCUT_EXPENSE -> { onClick(binding.shortcutExpenses) } + SHORTCUT_MONTHLY_PAYMENTS -> { onClick(binding.shortcutMonthlyPayments) } + SHORTCUT_EMI -> { onClick(binding.shortcutEmis) } + SHORTCUT_HOUSE_RENTERS -> { onItemClick(RenterTypes(1, "", 0)) } + SHORTCUT_BORROWERS -> { onItemClick(RenterTypes(2, "", 0)) } @@ -386,7 +392,7 @@ class HomeActivity : AppCompatActivity(), RenterTypeAdapter.OnClickListener, if (isInternetAvailable(this)) { - changeIsSyncedValue() + changeIsSyncedValueToFalse(syncValuesAgain = true) d.dismiss() } else { @@ -481,14 +487,15 @@ class HomeActivity : AppCompatActivity(), RenterTypeAdapter.OnClickListener, expenseCategoryViewModel.deleteAllExpenseCategories() monthlyPaymentCategoryViewModel.deleteAllMonthlyPaymentCategories() paymentMethodViewModel.deleteAllPaymentMethods() - changeIsSyncedValue() + + changeIsSyncedValueToFalse(syncValuesAgain = false) } catch (e: Exception) { e.printStackTrace() } } - private fun changeIsSyncedValue() { + private fun changeIsSyncedValueToFalse(syncValuesAgain: Boolean = false) { try { @@ -507,7 +514,11 @@ class HomeActivity : AppCompatActivity(), RenterTypeAdapter.OnClickListener, withContext(Dispatchers.Main) { - navigateToLoginActivity() + if (syncValuesAgain) { + syncDataFromFirestore() + } else { + navigateToLoginActivity() + } } } @@ -515,4 +526,16 @@ class HomeActivity : AppCompatActivity(), RenterTypeAdapter.OnClickListener, Log.e(TAG, "saveData: ${e.message}") } } + + private fun syncDataFromFirestore() { + + showToast(getString(R.string.syncing_values_now)) + + val foregroundService = Intent( + applicationContext, + SyncDocumentsFromFirestoreService::class.java + ) + ContextCompat.startForegroundService(applicationContext, foregroundService) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/MonthAndYearPickerDialog.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/MonthAndYearPickerDialog.kt new file mode 100644 index 00000000..42439b24 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/MonthAndYearPickerDialog.kt @@ -0,0 +1,156 @@ +package com.rohitthebest.manageyourrenters.ui.fragments + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.databinding.DialogMonthAndYearBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MonthAndYearPickerDialog : DialogFragment(R.layout.dialog_month_and_year) { + + private var _binding: DialogMonthAndYearBinding? = null + private val binding get() = _binding!! + + private var monthList: List = emptyList() + private var mListener: OnMonthAndYearDialogDismissListener? = null + + private var selectedMonth = 0 + private var selectedYear = 2023 + private var minimumYear = 2000 + private var maximumYear = 2099 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + _binding = DialogMonthAndYearBinding.bind(view) + + monthList = resources.getStringArray(R.array.months).toList() + + selectedMonth = WorkingWithDateAndTime.getCurrentMonth() + selectedYear = WorkingWithDateAndTime.getCurrentYear() + maximumYear = WorkingWithDateAndTime.getCurrentYear() + + initListeners() + initUI() + getMessage() + } + + private fun getMessage() { + + try { + if (!arguments?.isEmpty!!) { + + arguments?.let { bundle -> + + selectedMonth = bundle.getInt( + Constants.MONTH_YEAR_PICKER_MONTH_KEY, + WorkingWithDateAndTime.getCurrentMonth() + ) + selectedYear = bundle.getInt( + Constants.MONTH_YEAR_PICKER_YEAR_KEY, + WorkingWithDateAndTime.getCurrentYear() + ) + + minimumYear = bundle.getInt( + Constants.MONTH_YEAR_PICKER_MIN_YEAR_KEY, + 2000 + ) + + maximumYear = bundle.getInt( + Constants.MONTH_YEAR_PICKER_MAX_YEAR_KEY, + WorkingWithDateAndTime.getCurrentYear() + ) + + initUI() + } + } + } catch (e: Exception) { + showToast(requireContext(), getString(R.string.something_went_wrong)) + dismissDialog(false) + } + } + + private fun initUI() { + + binding.monthPicker.minValue = 0 + binding.monthPicker.maxValue = 11 + binding.monthPicker.displayedValues = monthList.toTypedArray() + + binding.yearPicker.minValue = minimumYear + binding.yearPicker.maxValue = maximumYear + + binding.monthPicker.value = selectedMonth + binding.yearPicker.value = selectedYear + } + + private fun initListeners() { + + binding.toolbar.setNavigationOnClickListener { + + dismissDialog(false) + } + + binding.toolbar.menu.findItem(R.id.menu_save_btn).apply { + this.icon = ContextCompat.getDrawable(requireContext(), R.drawable.baseline_check_24) + + this.setOnMenuItemClickListener { + + selectedMonth = binding.monthPicker.value + selectedYear = binding.yearPicker.value + + dismissDialog(true, selectedMonth, selectedYear) + + false + } + } + + } + + private fun dismissDialog(isMonthAndYearSelected: Boolean, month: Int = 0, year: Int = 0) { + + if (mListener != null) { + mListener!!.onMonthAndYearDialogDismissed(isMonthAndYearSelected, month, year) + } + + dismiss() + } + + interface OnMonthAndYearDialogDismissListener { + + fun onMonthAndYearDialogDismissed( + isMonthAndYearSelected: Boolean, + selectedMonth: Int, + selectedYear: Int + ) + } + + fun setOnMonthAndYearDialogDismissListener(listener: OnMonthAndYearDialogDismissListener) { + + mListener = listener + } + + + companion object { + @JvmStatic + fun newInstance(bundle: Bundle): MonthAndYearPickerDialog { + val fragment = MonthAndYearPickerDialog() + fragment.arguments = bundle + return fragment + } + } + + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/borrower/AddPartialPaymentFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/borrower/AddPartialPaymentFragment.kt index 1eb300c3..c263d3f5 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/borrower/AddPartialPaymentFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/borrower/AddPartialPaymentFragment.kt @@ -24,6 +24,7 @@ import com.rohitthebest.manageyourrenters.database.model.PartialPayment import com.rohitthebest.manageyourrenters.databinding.AddPartialPaymentLayoutBinding import com.rohitthebest.manageyourrenters.databinding.FragmentAddPartialPaymentBinding import com.rohitthebest.manageyourrenters.others.Constants.EDIT_TEXT_EMPTY_MESSAGE +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants import com.rohitthebest.manageyourrenters.ui.viewModels.BorrowerPaymentViewModel import com.rohitthebest.manageyourrenters.ui.viewModels.PartialPaymentViewModel import com.rohitthebest.manageyourrenters.utils.* @@ -444,7 +445,7 @@ class AddPartialPaymentFragment : BottomSheetDialogFragment(), uploadListOfDataToFireStore( requireContext(), - collection = getString(R.string.partialPayments), + collection = FirestoreCollectionsConstants.PARTIAL_PAYMENTS, fromPartialPaymentListToString(notSyncedList) ) } diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/houseRenters/HomeFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/houseRenters/HomeFragment.kt index 71dfd5c1..e41f5a2c 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/houseRenters/HomeFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/houseRenters/HomeFragment.kt @@ -32,7 +32,7 @@ import com.rohitthebest.manageyourrenters.ui.activities.ShowImageActivity import com.rohitthebest.manageyourrenters.ui.fragments.CustomMenuItems import com.rohitthebest.manageyourrenters.ui.fragments.SupportingDocumentDialogFragment import com.rohitthebest.manageyourrenters.ui.viewModels.RenterViewModel -import com.rohitthebest.manageyourrenters.utils.* +import com.rohitthebest.manageyourrenters.utils.Functions import com.rohitthebest.manageyourrenters.utils.Functions.Companion.getMillisecondsOfStartAndEndUsingConstants import com.rohitthebest.manageyourrenters.utils.Functions.Companion.getPairOfDateInMillisInStringInDateString import com.rohitthebest.manageyourrenters.utils.Functions.Companion.hideKeyBoard @@ -40,11 +40,24 @@ import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAv import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showDateRangePickerDialog import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showNoInternetMessage import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.changeVisibilityOfFABOnScrolled +import com.rohitthebest.manageyourrenters.utils.convertRenterToJSONString +import com.rohitthebest.manageyourrenters.utils.convertToJsonString +import com.rohitthebest.manageyourrenters.utils.deleteFileFromFirebaseStorage +import com.rohitthebest.manageyourrenters.utils.executeAfterDelay +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.isValid +import com.rohitthebest.manageyourrenters.utils.onTextChanged +import com.rohitthebest.manageyourrenters.utils.onTextSubmit +import com.rohitthebest.manageyourrenters.utils.show +import com.rohitthebest.manageyourrenters.utils.showAlertDialogForDeletion import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.* +import java.util.Locale +import kotlin.math.abs private const val TAG = "HomeFragment" @@ -68,6 +81,9 @@ class HomeFragment : Fragment(), View.OnClickListener, ShowRentersAdapter.OnClic private var searchView: SearchView? = null private var listSize = 0 + + private var renterNameWithTheirDues: Map? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -103,6 +119,15 @@ class HomeFragment : Fragment(), View.OnClickListener, ShowRentersAdapter.OnClic initListeners() observeRevenueGenerated() + observeRenterDues() + } + + private fun observeRenterDues() { + + renterViewModel.renterNameWithTheirDues.observe(viewLifecycleOwner) { renterNameAndTheirDuesMap -> + + renterNameWithTheirDues = renterNameAndTheirDuesMap + } } private fun getRvState() { @@ -564,7 +589,8 @@ class HomeFragment : Fragment(), View.OnClickListener, ShowRentersAdapter.OnClic .setOnMenuItemClickListener(this) binding.houseRentersHomeToolBar.menu.findItem(R.id.menu_renter_revenue_custom_range) .setOnMenuItemClickListener(this) - + binding.houseRentersHomeToolBar.menu.findItem(R.id.menu_renter_dues) + .setOnMenuItemClickListener(this) } private var d1 = System.currentTimeMillis() - (30 * Constants.ONE_DAY_MILLISECONDS) @@ -588,6 +614,7 @@ class HomeFragment : Fragment(), View.OnClickListener, ShowRentersAdapter.OnClic true } + R.id.menu_show_deleted_renters -> { findNavController().navigate(R.id.action_homeFragment_to_deletedRentersFragment) @@ -657,16 +684,47 @@ class HomeFragment : Fragment(), View.OnClickListener, ShowRentersAdapter.OnClic R.id.menu_renter_revenue_all_time -> { isRevenueObserveEnabled = true - revenueTitle = "Revenue - All Time" + revenueTitle = getString(R.string.revenue_all_time) renterViewModel.getRentersWithTheirAmountPaid() true } + R.id.menu_renter_dues -> { + + showDuesInAlertDialogBox() + + true + } + else -> false } } + private fun showDuesInAlertDialogBox() { + + var totalDues = 0.0 + + val message = StringBuilder() + + if (renterNameWithTheirDues != null && renterNameWithTheirDues!!.isNotEmpty()) { + + renterNameWithTheirDues!!.forEach { entry -> + totalDues += abs(entry.value) + message.append("${entry.key} ===> ${abs(entry.value).format(2)}\n\n") + } + } + + message.append("----------------------------------------\n\n") + message.append(" Total : ${totalDues.format(2)}") + + + showAlertDialogWithTitleAndMessage( + getString(R.string.renter_dues), + message.toString() + ) + } + private fun showRevenueMessageByDateRange( customDateRange: CustomDateRange, millis: Pair @@ -737,7 +795,7 @@ class HomeFragment : Fragment(), View.OnClickListener, ShowRentersAdapter.OnClic MaterialAlertDialogBuilder(requireContext()) .setTitle(title) .setMessage(message) - .setPositiveButton("Ok") { dialog, _ -> + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.dismiss() } diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/ShowPaymentMethodSelectorDialogFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/ShowPaymentMethodSelectorDialogFragment.kt new file mode 100644 index 00000000..b3712d7c --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/ShowPaymentMethodSelectorDialogFragment.kt @@ -0,0 +1,159 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney + +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.adapters.SelectPaymentMethodAdapter +import com.rohitthebest.manageyourrenters.data.filter.ExpenseFilterDto +import com.rohitthebest.manageyourrenters.database.model.PaymentMethod +import com.rohitthebest.manageyourrenters.databinding.DialogPaymentMethodSelectorBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.ui.viewModels.PaymentMethodViewModel +import com.rohitthebest.manageyourrenters.utils.convertJsonToObject +import com.rohitthebest.manageyourrenters.utils.isValid +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ShowPaymentMethodSelectorDialogFragment : + BottomSheetDialogFragment(R.layout.dialog_payment_method_selector), + SelectPaymentMethodAdapter.OnClickListener { + + private var _binding: DialogPaymentMethodSelectorBinding? = null + private val binding get() = _binding!! + + private val paymentMethodViewModel by viewModels() + + private lateinit var selectPaymentMethodAdapter: SelectPaymentMethodAdapter + + private var mListener: OnClickListener? = null + + private var selectedPaymentMethods: List = emptyList() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + _binding = DialogPaymentMethodSelectorBinding.bind(view) + + selectPaymentMethodAdapter = SelectPaymentMethodAdapter() + setUpRecyclerView() + getMessage() + getAllPaymentMethods() + initListeners() + } + + + private fun getMessage() { + + if (!arguments?.isEmpty!!) { + + arguments?.let { bundle -> + + if (bundle.getString(Constants.EXPENSE_FILTER_KEY).isValid()) { + + val expenseFilterDto = bundle.getString(Constants.EXPENSE_FILTER_KEY)!! + .convertJsonToObject(ExpenseFilterDto::class.java)!! + + selectedPaymentMethods = expenseFilterDto.paymentMethods + getAllPaymentMethods() + } + } + } + } + + private fun getAllPaymentMethods() { + + paymentMethodViewModel.getAllPaymentMethods() + .observe(viewLifecycleOwner) { paymentMethods -> + + if (selectedPaymentMethods.isNotEmpty()) { + paymentMethods.forEach { pm -> + if (selectedPaymentMethods.contains(pm.key)) { + pm.isSelected = true + } + } + } + + selectPaymentMethodAdapter.submitList(paymentMethods) + } + } + + private fun setUpRecyclerView() { + + binding.paymentMethodsRV.apply { + setHasFixedSize(true) + adapter = selectPaymentMethodAdapter + layoutManager = StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL) + } + + selectPaymentMethodAdapter.setOnClickListener(this) + } + + override fun onItemClick(paymentMethod: PaymentMethod, position: Int) { + + paymentMethod.isSelected = !paymentMethod.isSelected + selectPaymentMethodAdapter.notifyItemChanged(position) + } + + private fun initListeners() { + + binding.toolbar.menu.findItem(R.id.menu_item_filter_apply).apply { + + setOnMenuItemClickListener { + + if (mListener != null) { + mListener!!.onFilterApply( + selectPaymentMethodAdapter.currentList.filter { it.isSelected } + .map { it.key } + ) + } + + dismiss() + + true + } + } + + binding.toolbar.menu.findItem(R.id.menu_item_filter_clear).setOnMenuItemClickListener { + + if (mListener != null) { + mListener!!.onFilterApply(emptyList()) + } + + dismiss() + + true + } + + } + + fun setOnClickListener(listener: OnClickListener) { + mListener = listener + } + + interface OnClickListener { + + // method + fun onFilterApply(selectedPaymentMethods: List?) + } + + companion object { + @JvmStatic + fun newInstance(bundle: Bundle): ShowPaymentMethodSelectorDialogFragment { + val fragment = ShowPaymentMethodSelectorDialogFragment() + fragment.arguments = bundle + return fragment + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/AddEditExpense.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/AddEditExpense.kt index 6366429a..11f65528 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/AddEditExpense.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/AddEditExpense.kt @@ -31,10 +31,11 @@ import com.rohitthebest.manageyourrenters.utils.Functions.Companion.getUid import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime import com.rohitthebest.manageyourrenters.utils.isTextValid +import com.rohitthebest.manageyourrenters.utils.isValid import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.util.* +import java.util.Calendar private const val TAG = "AddEditExpense" @@ -113,7 +114,7 @@ class AddEditExpense : Fragment(R.layout.fragment_add_expense), View.OnClickList receivedExpenseKey = args.expenseKey!! - if (receivedExpenseKey != "") { + if (receivedExpenseKey.isValid()) { isMessageReceivedForEditing = true diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/DeepAnalyzeExpenseFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/DeepAnalyzeExpenseFragment.kt index 71d55d8e..06025232 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/DeepAnalyzeExpenseFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/DeepAnalyzeExpenseFragment.kt @@ -26,9 +26,16 @@ import com.rohitthebest.manageyourrenters.database.model.ExpenseCategory import com.rohitthebest.manageyourrenters.databinding.FragmentDeepAnalyzeExpenseBinding import com.rohitthebest.manageyourrenters.others.Constants import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseCategoryViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseGraphDataViewModel import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseViewModel -import com.rohitthebest.manageyourrenters.utils.* +import com.rohitthebest.manageyourrenters.utils.Functions import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.executeAfterDelay +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.loadAnyValueFromSharedPreference +import com.rohitthebest.manageyourrenters.utils.show import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -46,6 +53,7 @@ class DeepAnalyzeExpenseFragment : Fragment(R.layout.fragment_deep_analyze_expen private val expenseCategoryViewModel by viewModels() private val expenseViewModel by viewModels() + private val expenseGraphDataViewModel by viewModels() private lateinit var pie: Pie @@ -79,6 +87,50 @@ class DeepAnalyzeExpenseFragment : Fragment(R.layout.fragment_deep_analyze_expen getAllExpenseCategories() initListeners() + + observerExpenseGraphData() + } + + private fun observerExpenseGraphData() { + + expenseGraphDataViewModel.expenseGraphData.observe(viewLifecycleOwner) { + + if (it != null && it.first.isNotEmpty()) { + + val expenseCategoryNameAndTheirTotalList = it.first + val total = it.second + + pie.title("Total expense amount: ${total.format(2)}") + + val data = ArrayList() + + expenseCategoryNameAndTheirTotalList.forEach { expenseCategoryAndTheirTotalExpenseAmounts -> + + data.add( + ValueDataEntry( + expenseCategoryAndTheirTotalExpenseAmounts.categoryName, + expenseCategoryAndTheirTotalExpenseAmounts.totalAmount + ) + ) + } + + lifecycleScope.launch { + + delay(500) + + if (data.isNotEmpty()) { + + binding.chart.show() + pie.data(data) + } else { + + binding.chart.hide() + showToast(requireContext(), getString(R.string.no_data_available)) + } + + } + } + } } private fun loadCustomDateRangeValueFromSharedPreference() { @@ -243,12 +295,16 @@ class DeepAnalyzeExpenseFragment : Fragment(R.layout.fragment_deep_analyze_expen CustomDateRange.THIS_MONTH -> binding.toolbar.subtitle = getString(R.string.this_month) + CustomDateRange.THIS_WEEK -> binding.toolbar.subtitle = getString(R.string.this_week) + CustomDateRange.PREVIOUS_MONTH -> binding.toolbar.subtitle = getString(R.string.previous_month) + CustomDateRange.PREVIOUS_WEEK -> binding.toolbar.subtitle = getString(R.string.previous_week) + else -> {} } initChartData() @@ -380,146 +436,22 @@ class DeepAnalyzeExpenseFragment : Fragment(R.layout.fragment_deep_analyze_expen val selectedCategories = deepAnalyzeExpenseCategoryAdapter.currentList.filter { expenseCategory -> - expenseCategory.isSelected - } - - getTotalExpenseAmount(selectedCategories) - - prepareChart(selectedCategories) - - } - - private fun prepareChart(selectedCategories: List) { - - if (selectedCategories.isNotEmpty()) { - - val data = ArrayList() - - selectedCategories.forEach { expenseCategory -> - - lifecycleScope.launch { - - if (!isDateRangeSelected) { - - // get the all time amount - - try { - expenseViewModel.getExpenseAmountSumByExpenseCategoryKey( - expenseCategory.key - ).collect { amount -> - - data.add( - ValueDataEntry( - expenseCategory.categoryName, - amount - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - - } else { - - // get the amount by the date range selected - - try { - expenseViewModel.getExpenseAmountSumByExpenseCategoryByDateRange( - expenseCategory.key, - startDate, - endDate + Constants.ONE_DAY_MILLISECONDS - ).collect { amount -> - - data.add( - ValueDataEntry( - expenseCategory.categoryName, - amount - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - - } - - } - } - - lifecycleScope.launch { - - delay(500) - - if (data.isNotEmpty()) { - - binding.chart.show() - pie.data(data) - //binding.chart.setChart(pie) - } else { - - binding.chart.hide() - showToast(requireContext(), "No Data available!!!") - } - - } - - } - - } - - private fun getTotalExpenseAmount(selectedCategories: List) { - - var total = 0.0 + }.map { it.key } if (!isDateRangeSelected) { - selectedCategories.forEach { expenseCategory -> - - expenseViewModel.getTotalExpenseAmountByExpenseCategory(expenseCategory.key) - .observe(viewLifecycleOwner) { amount -> - - try { - - total += amount - } catch (e: NullPointerException) { - - e.printStackTrace() - } - - } - } + expenseGraphDataViewModel.getTotalExpenseAmountWithTheirExpenseCategoryNamesForSelectedExpenseCategories( + selectedCategories + ) } else { - selectedCategories.forEach { expenseCategory -> - - - expenseViewModel.getTotalExpenseAmountByCategoryKeyAndDateRange( - expenseCategory.key, - startDate, - endDate + Constants.ONE_DAY_MILLISECONDS - ) - .observe(viewLifecycleOwner) { amount -> - - try { - - total += amount - } catch (e: Exception) { - - e.printStackTrace() - } - } - - } - - } - - lifecycleScope.launch { - delay(500) - pie.title("Total expense amount: $total") - + expenseGraphDataViewModel.getTotalExpenseAmountWithTheirExpenseCategoryNamesForSelectedExpenseCategoriesByDateRange( + selectedCategories, + startDate, + endDate + Constants.ONE_DAY_MILLISECONDS + ) } - } private var changeChartDataJob: Job? = null @@ -530,7 +462,10 @@ class DeepAnalyzeExpenseFragment : Fragment(R.layout.fragment_deep_analyze_expen if (getSelectedExpenseCategoryCount() <= 2) { - showToast(requireContext(), "Minimum two categories has to be selected") + showToast( + requireContext(), + getString(R.string.minimum_two_categories_needs_to_be_selected) + ) return } } @@ -551,19 +486,9 @@ class DeepAnalyzeExpenseFragment : Fragment(R.layout.fragment_deep_analyze_expen binding.clearSelectionFAB.enable() } - try { - - if (changeChartDataJob != null && changeChartDataJob?.isActive == true) { - - changeChartDataJob?.cancel() - } - } catch (e: Exception) { - e.printStackTrace() - } finally { - - changeChartDataJob = lifecycleScope.launch { + changeChartDataJob = lifecycleScope.launch { - delay(250) + changeChartDataJob.executeAfterDelay(250) { initChartData() } diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ExpenseCategoryFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ExpenseCategoryFragment.kt index 9ece2a7d..894dae6e 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ExpenseCategoryFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ExpenseCategoryFragment.kt @@ -311,6 +311,11 @@ class ExpenseCategoryFragment : Fragment(R.layout.fragment_expense_category), findNavController().navigate(R.id.action_expenseCategoryFragment_to_paymentMethodsFragment) true } + binding.toolbar.menu.findItem(R.id.menu_create_budget).setOnMenuItemClickListener { + + findNavController().navigate(R.id.action_expenseCategoryFragment_to_budgetAndIncomeFragment) + true + } } private fun setNoExpenseCategoryMessageTvVisibility(isVisible: Boolean) { diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/GraphFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/GraphFragment.kt index f72c9352..ce3f5d7a 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/GraphFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/GraphFragment.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.util.Log import android.view.View import android.widget.Toast +import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -19,11 +20,15 @@ import com.anychart.enums.LegendLayout import com.rohitthebest.manageyourrenters.R import com.rohitthebest.manageyourrenters.data.CustomDateRange import com.rohitthebest.manageyourrenters.data.ShowExpenseBottomSheetTagsEnum +import com.rohitthebest.manageyourrenters.data.filter.ExpenseFilterDto import com.rohitthebest.manageyourrenters.databinding.FragmentGraphBinding +import com.rohitthebest.manageyourrenters.others.Constants import com.rohitthebest.manageyourrenters.others.Constants.CUSTOM_DATE_RANGE_FOR_GRAPH_FRAGMENT_SHARED_PREF_KEY import com.rohitthebest.manageyourrenters.others.Constants.CUSTOM_DATE_RANGE_FOR_GRAPH_FRAGMENT_SHARED_PREF_NAME import com.rohitthebest.manageyourrenters.others.Constants.ONE_DAY_MILLISECONDS +import com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.ShowPaymentMethodSelectorDialogFragment import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseCategoryViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseGraphDataViewModel import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseViewModel import com.rohitthebest.manageyourrenters.utils.* import com.rohitthebest.manageyourrenters.utils.Functions.Companion.getUid @@ -36,19 +41,25 @@ import kotlinx.coroutines.launch private const val TAG = "GraphFragment" @AndroidEntryPoint -class GraphFragment : Fragment(R.layout.fragment_graph) { +class GraphFragment : Fragment(R.layout.fragment_graph), + ShowPaymentMethodSelectorDialogFragment.OnClickListener { private var _binding: FragmentGraphBinding? = null private val binding get() = _binding!! private val expenseCategoryViewModel by viewModels() private val expenseViewModel by viewModels() + private val expenseGraphDataViewModel by viewModels() private lateinit var pie: Pie private var isAllTimeSelected = false + private var d1 = System.currentTimeMillis() - (ONE_DAY_MILLISECONDS * 30) + private var d2 = System.currentTimeMillis() private var selectedCustomDateRangeMenu: CustomDateRange? = CustomDateRange.ALL_TIME + private var expenseFilterDto: ExpenseFilterDto? = null + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentGraphBinding.bind(view) @@ -61,6 +72,60 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { handleDateRangeSelectionMenu(selectedCustomDateRangeMenu ?: CustomDateRange.ALL_TIME) initListeners() + + observeExpenseGraphData() + } + + private fun observeExpenseGraphData() { + + expenseGraphDataViewModel.expenseGraphData.observe(viewLifecycleOwner) { + + if (it != null && it.first.isNotEmpty()) { + + val expenseCategoryNameAndTheirTotalList = it.first + val total = it.second + + pie.legend().title(getString(R.string.total_expense, total.format(3))) + + val data = ArrayList() + + expenseCategoryNameAndTheirTotalList.forEach { expenseCategoryAndTheirTotalExpenseAmounts -> + + data.add( + ValueDataEntry( + expenseCategoryAndTheirTotalExpenseAmounts.categoryName, + expenseCategoryAndTheirTotalExpenseAmounts.totalAmount + ) + ) + } + + lifecycleScope.launch { + + delay(500) + + Log.d(TAG, "getAllExpenseCategory: ${data.size}") + + if (data.isNotEmpty()) { + + binding.chart.show() + binding.noDataTV.hide() + pie.data(data) + } else { + + handleUIForNoDataAvailable() + } + } + } else { + handleUIForNoDataAvailable() + } + } + } + + private fun handleUIForNoDataAvailable() { + + binding.chart.hide() + binding.noDataTV.show() + showToast(requireContext(), getString(R.string.no_data_available)) } private fun loadCustomDateRangeValueFromSharedPreference() { @@ -89,7 +154,7 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { binding.chart.setProgressBar(binding.progressBar) pie.title().enabled(true) - pie.title("All time") + pie.title(getString(R.string.all_time)) //pie.title("Expenses on each category") pie.labels().position("outside") @@ -107,9 +172,6 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { binding.chart.setChart(pie) } - private var d1 = System.currentTimeMillis() - (ONE_DAY_MILLISECONDS * 30) - private var d2 = System.currentTimeMillis() - private fun initListeners() { binding.toolbar.setNavigationOnClickListener { @@ -118,74 +180,44 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { binding.toolbar.menu.findItem(R.id.menu_deep_analyze_expense).setOnMenuItemClickListener { - expenseCategoryViewModel.getAllExpenseCategories().observe(viewLifecycleOwner) { - - if (it.size >= 2) { - - findNavController().navigate(R.id.action_graphFragment_to_deepAnalyzeExpenseFragment) - } else { - - showToast( - requireContext(), - getString(R.string.depp_analyze_error_message), - Toast.LENGTH_LONG - ) - } - } - + handleCategoryCompareGraph() true } binding.toolbar.menu.findItem(R.id.menu_monthly_graph).setOnMenuItemClickListener { - expenseViewModel.getAllExpenses().observe(viewLifecycleOwner) { - - if (it.isNotEmpty()) { - findNavController().navigate(R.id.action_graphFragment_to_monthlyGraphFragment) - } else { - - showToast(requireContext(), getString(R.string.no_expense_added)) - } - } - + handleMonthlyGraphMenu() true } - binding.toolbar.menu.findItem(R.id.menu_share_expense_graph_sc).setOnMenuItemClickListener { - - binding.toolbar.hide() + binding.toolbar.menu.findItem(R.id.menu_category_graph).setOnMenuItemClickListener { - val bitmap = binding.root.loadBitmap() - - binding.toolbar.show() + handleCategoryGraph() + true + } - lifecycleScope.launch { - saveBitmapToCacheDirectoryAndShare(requireActivity(), bitmap) - } + binding.toolbar.menu.findItem(R.id.menu_share_expense_graph_sc).setOnMenuItemClickListener { + handleShareExpenseGraphScreenshotMenu() true } binding.toolbar.menu.findItem(R.id.menu_save_expense_graph_sc).setOnMenuItemClickListener { - binding.toolbar.hide() - - val bitmap = binding.root.loadBitmap() - - binding.toolbar.show() - - bitmap.saveToStorage(requireContext(), "${getUid()}_expense_graph") + handleSaveExpenseGraphScreenshotMenu() + true + } - showToast(requireContext(), "Screenshot saved to phone storage") + binding.toolbar.menu.findItem(R.id.menu_filter_expense_graph).setOnMenuItemClickListener { + handleFilterExpenseGraphMenu() true } binding.dateRangeMenuBtn.setOnClickListener { view -> showMenuForSelectingCustomTime(view) - } binding.dateRangeCv.setOnClickListener { @@ -196,12 +228,147 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { date1 = d1, date2 = d2, callingFragementTag = ShowExpenseBottomSheetTagsEnum.GRAPH_FRAGMENT, - paymentMethodKey = null + paymentMethodKey = if (expenseFilterDto != null && !expenseFilterDto?.paymentMethods.isNullOrEmpty()) { + convertStringListToJSON(expenseFilterDto?.paymentMethods ?: emptyList()) + } else { + null + } ) findNavController().navigate(action) } } + private fun handleFilterExpenseGraphMenu() { + + //showing Payment Method Selector Dialog + + requireActivity().supportFragmentManager.let { fragmentManager -> + + val bundle = Bundle() + bundle.putString( + Constants.EXPENSE_FILTER_KEY, + if (expenseFilterDto == null) "" else expenseFilterDto.convertToJsonString() + ) + + ShowPaymentMethodSelectorDialogFragment.newInstance( + bundle + ).apply { + show(fragmentManager, TAG) + }.setOnClickListener(this) + } + + } + + override fun onFilterApply(selectedPaymentMethods: List?) { + + binding.toolbar.menu.findItem(R.id.menu_filter_expense_graph) + .apply { + + if (selectedPaymentMethods.isNullOrEmpty()) { + this.icon = + ContextCompat.getDrawable( + requireContext(), + R.drawable.baseline_filter_list_24 + ) + + expenseFilterDto = null + + } else { + this.icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.baseline_filter_list_colored_24 + ) + + expenseFilterDto = ExpenseFilterDto() + expenseFilterDto!!.isPaymentMethodEnabled = true + expenseFilterDto!!.paymentMethods = selectedPaymentMethods + } + } + + if (selectedCustomDateRangeMenu == CustomDateRange.ALL_TIME) { + // making this value to false, so it will enter the ALL_TIME case in handleDateRangeSelectionMenu function + isAllTimeSelected = false + } + + handleDateRangeSelectionMenu(selectedCustomDateRangeMenu ?: CustomDateRange.ALL_TIME) + } + + private fun handleSaveExpenseGraphScreenshotMenu() { + + binding.toolbar.hide() + + val bitmap = binding.root.loadBitmap() + + binding.toolbar.show() + + bitmap.saveToStorage(requireContext(), "${getUid()}_expense_graph") + + showToast(requireContext(), getString(R.string.screenshot_saved_to_phone_storage)) + } + + private fun handleShareExpenseGraphScreenshotMenu() { + + binding.toolbar.hide() + + val bitmap = binding.root.loadBitmap() + + binding.toolbar.show() + + lifecycleScope.launch { + + saveBitmapToCacheDirectoryAndShare(requireActivity(), bitmap) + } + } + + private fun handleCategoryGraph() { + + expenseViewModel.isAnyExpenseAdded().observe(viewLifecycleOwner) { + + if (it) { + val action = GraphFragmentDirections.actionGraphFragmentToMonthlyGraphFragment( + true + ) + findNavController().navigate(action) + } else { + + showToast(requireContext(), getString(R.string.no_expense_added)) + } + } + + } + + private fun handleMonthlyGraphMenu() { + + expenseViewModel.isAnyExpenseAdded().observe(viewLifecycleOwner) { + + if (it) { + findNavController().navigate(R.id.action_graphFragment_to_monthlyGraphFragment) + } else { + + showToast(requireContext(), getString(R.string.no_expense_added)) + } + } + } + + private fun handleCategoryCompareGraph() { + + expenseCategoryViewModel.getAllExpenseCategoriesByLimit(2).observe(viewLifecycleOwner) { + + if (it.size >= 2) { + + findNavController().navigate(R.id.action_graphFragment_to_deepAnalyzeExpenseFragment) + } else { + + showToast( + requireContext(), + getString(R.string.depp_analyze_error_message), + Toast.LENGTH_LONG + ) + } + } + + } + private fun showMenuForSelectingCustomTime(view: View) { Functions.showCustomDateRangeOptionMenu( @@ -225,14 +392,16 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { if (!isAllTimeSelected) { + isAllTimeSelected = true + Log.d( TAG, - "handleDateRangeSelectionMenu: all time selected : $isAllTimeSelected" + "handleDateRangeSelectionMenu: paymentMethods: ${expenseFilterDto?.paymentMethods}" ) - isAllTimeSelected = true - - getExpenseCategoryExpensesByAllTime() + expenseGraphDataViewModel.getTotalExpenseAmountsWithTheirExpenseCategoryNames( + expenseFilterDto?.paymentMethods ?: emptyList() + ) changeSelectionUI() @@ -330,9 +499,11 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { if (!isAllTimeSelected) { changeSelectionUI() - getExpenseCategoryExpensesByDateRange( - d1, - d2 + + expenseGraphDataViewModel.getTotalExpenseAmountsWithTheirExpenseCategoryNamesByDateRange( + date1 = d1, + date2 = d2 + ONE_DAY_MILLISECONDS, + paymentMethodKeys = expenseFilterDto?.paymentMethods ?: emptyList() ) } } @@ -342,7 +513,7 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { if (isAllTimeSelected) { - binding.dateRangeTv.text = "All time" + binding.dateRangeTv.text = getString(R.string.all_time) } else { binding.dateRangeTv.text = "${ @@ -358,148 +529,6 @@ class GraphFragment : Fragment(R.layout.fragment_graph) { } } - private fun getExpenseCategoryExpensesByDateRange(date1: Long?, date2: Long?) { - - if (!isAllTimeSelected) { - - Log.d(TAG, "getExpenseCategoryExpensesByDateRange: ") - - expenseViewModel.getTotalExpenseAmountByDateRange( - date1!!, - date2!! + ONE_DAY_MILLISECONDS - ) - .observe(viewLifecycleOwner) { total -> - - pie.legend().title("Total expense : %.3f".format(total)) - } - - - expenseCategoryViewModel.getAllExpenseCategories() - .observe(viewLifecycleOwner) { expenseCategories -> - - if (expenseCategories.isNotEmpty()) { - - val data = ArrayList() - - expenseCategories.forEach { expenseCategory -> - - lifecycleScope.launch { - - try { - expenseViewModel.getExpenseAmountSumByExpenseCategoryByDateRange( - expenseCategory.key, date1, date2 + ONE_DAY_MILLISECONDS - ).collect { amount -> - - Log.d( - TAG, - "getExpenseCategoryExpensesByDateRange: category : ${expenseCategory.categoryName} -> amount : $amount" - ) - - data.add( - ValueDataEntry( - expenseCategory.categoryName, - amount - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - lifecycleScope.launch { - - delay(500) - - Log.d(TAG, "getAllExpenseCategory: $data") - - if (data.isNotEmpty()) { - - binding.chart.show() - pie.data(data) - } else { - - binding.chart.hide() - showToast(requireContext(), "No data found") - } - } - - } - } - } - - } - - private fun getExpenseCategoryExpensesByAllTime() { - - if (isAllTimeSelected) { - - - Log.d(TAG, "getExpenseCategoryExpensesByAllTime: ") - - expenseViewModel.getTotalExpenseAmount().observe(viewLifecycleOwner) { total -> - - pie.legend().title("Total expense : $total") - } - - expenseCategoryViewModel.getAllExpenseCategories() - .observe(viewLifecycleOwner) { expenseCategories -> - - if (expenseCategories.isNotEmpty()) { - - val data = ArrayList() - - expenseCategories.forEach { expenseCategory -> - - lifecycleScope.launch { - - try { - expenseViewModel.getExpenseAmountSumByExpenseCategoryKey( - expenseCategory.key - ).collect { amount -> - - Log.d( - TAG, - "getExpenseCategoryExpensesByAllTime: category : ${expenseCategory.categoryName} -> amount : $amount" - ) - - data.add( - ValueDataEntry( - expenseCategory.categoryName, - amount - ) - ) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - - lifecycleScope.launch { - - delay(500) - - Log.d(TAG, "getAllExpenseCategory: $data") - - if (data.isNotEmpty()) { - - binding.chart.show() - pie.data(data) - //binding.chart.setChart(pie) - } else { - - binding.chart.hide() - } - - } - - } - } - } - } - override fun onDestroyView() { super.onDestroyView() diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/MonthlyGraphFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/MonthlyGraphFragment.kt index d128a89b..dc6d6b9d 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/MonthlyGraphFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/MonthlyGraphFragment.kt @@ -2,6 +2,7 @@ package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense import android.os.Bundle import android.view.View +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -12,7 +13,10 @@ import com.anychart.charts.Cartesian import com.anychart.enums.HoverMode import com.anychart.enums.TooltipPositionMode import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.database.model.ExpenseCategory import com.rohitthebest.manageyourrenters.databinding.FragmentMonthlyGraphBinding +import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseCategoryViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseGraphDataViewModel import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseViewModel import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime import com.rohitthebest.manageyourrenters.utils.setListToSpinner @@ -28,26 +32,24 @@ class MonthlyGraphFragment : Fragment(R.layout.fragment_monthly_graph) { private var _binding: FragmentMonthlyGraphBinding? = null private val binding get() = _binding!! + private val expenseGraphDataViewModel by viewModels() private val expenseViewModel by viewModels() + private val expenseCategoryViewModel by viewModels() private lateinit var cartesian: Cartesian private var selectedYear = 2020 - private var isRefreshEnabled = true + private lateinit var expenseCategories: List + private lateinit var selectedCategory: ExpenseCategory + + private var isCategoryEnabled = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentMonthlyGraphBinding.bind(view) - observeExpenseOfEachMonth() - + getMessage() setUpChart() - getStartAndEndYear() - - lifecycleScope.launch { - delay(250) - loadData() - } binding.toolbar.setNavigationOnClickListener { @@ -55,38 +57,76 @@ class MonthlyGraphFragment : Fragment(R.layout.fragment_monthly_graph) { } } - private fun loadData() { - expenseViewModel.getExpensesOfAllMonthsOfYear(selectedYear) + private fun getMessage() { + + if (!arguments?.isEmpty!!) { + + val args = arguments?.let { + + MonthlyGraphFragmentArgs.fromBundle(it) + } + + isCategoryEnabled = args?.isCategoryExpenseEnabled ?: false + binding.mcvCategories.isVisible = isCategoryEnabled + + if (isCategoryEnabled) { + observeAllExpenseCategories() + binding.toolbar.title = getString(R.string.monthly_category_expense_graph) + } + observeExpenseOfEachMonth() + getStartAndEndYear() + } } - private fun observeExpenseOfEachMonth() { + private fun observeAllExpenseCategories() { - expenseViewModel.expenseOfEachMonth.observe(viewLifecycleOwner) { expensePerMonth -> + expenseCategoryViewModel.getAllExpenseCategories().observe(viewLifecycleOwner) { + expenseCategories = it - if (isRefreshEnabled) { - val monthList = resources.getStringArray(R.array.months_short).asList() - var i = 0 + if (it.isNotEmpty()) { + initCategorySpinner() + } + } + } - val data = ArrayList() + private fun loadData() { + expenseGraphDataViewModel.getExpensesOfAllMonthsOfYear(selectedYear) + } - expensePerMonth.forEach { expense -> + private fun observeExpenseOfEachMonth() { - data.add(ValueDataEntry(monthList[i], expense)) - ++i - } + expenseGraphDataViewModel.expenseOfEachMonth.observe(viewLifecycleOwner) { expensePerMonth -> - lifecycleScope.launch { + val data = ArrayList() - delay(500) + expensePerMonth.forEach { monthAndAmount -> + data.add(ValueDataEntry(monthAndAmount.first, monthAndAmount.second)) + } - cartesian.title("Monthly expense for the year : $selectedYear") - cartesian.data(data) + lifecycleScope.launch { + + delay(500) + + if (isCategoryEnabled) { + cartesian.title( + getString( + R.string.monthly_expense_for_the_year_and_category, + selectedYear.toString(), selectedCategory.categoryName + ) + ) + } else { + + cartesian.title( + getString( + R.string.monthly_expense_for_the_year, + selectedYear.toString() + ) + ) } - isRefreshEnabled = false + cartesian.data(data) } } - } private fun setUpChart() { @@ -105,8 +145,13 @@ class MonthlyGraphFragment : Fragment(R.layout.fragment_monthly_graph) { cartesian.tooltip().positionMode(TooltipPositionMode.POINT) cartesian.interactivity().hoverMode(HoverMode.BY_X) - cartesian.xAxis(0).title("Month") - cartesian.yAxis(0).title("Expense") + cartesian.xAxis(0).title(getString(R.string.month)) + cartesian.yAxis(0).title(getString(R.string.expense)) + + cartesian.labels(true) + cartesian.labels().format("{%value}") + cartesian.labels().fontSize(10) + cartesian.labels().fontColor("#4D4C4C") binding.chart.setChart(cartesian) } @@ -141,12 +186,44 @@ class MonthlyGraphFragment : Fragment(R.layout.fragment_monthly_graph) { { position -> selectedYear = yearList[position] - isRefreshEnabled = true - loadData() + if (isCategoryEnabled) { + loadDataForSelectedCategory() + } else { + loadData() + } }, {} ) } + private fun initCategorySpinner() { + + if (this::expenseCategories.isInitialized) { + + binding.mcvCategories.isVisible = expenseCategories.isNotEmpty() + + if (expenseCategories.isNotEmpty()) { + + binding.categorySpinner.setListToSpinner( + requireContext(), + expenseCategories.map { it.categoryName }, + { position -> + + selectedCategory = expenseCategories[position] + loadDataForSelectedCategory() + }, {} + ) + } + } + } + + private fun loadDataForSelectedCategory() { + expenseGraphDataViewModel.getExpensesOfAllMonthsOfYearForSelectedCategory( + selectedYear, + selectedCategory.key + ) + } + + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ShowExpenseBottomSheetFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ShowExpenseBottomSheetFragment.kt index da811002..a21ffd36 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ShowExpenseBottomSheetFragment.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/ShowExpenseBottomSheetFragment.kt @@ -19,6 +19,7 @@ import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseCategoryViewModel import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseViewModel import com.rohitthebest.manageyourrenters.ui.viewModels.PaymentMethodViewModel import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.convertJSONToStringList import com.rohitthebest.manageyourrenters.utils.isValid import dagger.hilt.android.AndroidEntryPoint @@ -117,10 +118,45 @@ class ShowExpenseBottomSheetFragment : BottomSheetDialogFragment(), ExpenseAdapt ShowExpenseBottomSheetTagsEnum.PAYMENT_METHOD_FRAGMENT -> { showInPaymentMethodFragment(args) } + + ShowExpenseBottomSheetTagsEnum.BUDGET_AND_INCOME_FRAGMENT -> { + showInBudgetAndIncomeOverviewFragment(args) + } } } } + private fun showInBudgetAndIncomeOverviewFragment(args: ShowExpenseBottomSheetFragmentArgs) { + + val budgetAndIncomeExpenseFilter = args.expenseFilterForBudgetAndIncome + val date1 = args.date1 + val date2 = args.date2 + Constants.ONE_DAY_MILLISECONDS + + if (budgetAndIncomeExpenseFilter != null) { + + expenseViewModel.getExpenseByCategoryKeysAndDateRange( + budgetAndIncomeExpenseFilter.categoryKeys, + date1, + date2 + ).observe(viewLifecycleOwner) { expenses -> + + submitListToAdapter( + if (budgetAndIncomeExpenseFilter.paymentMethods.isEmpty()) { + expenses + } else { + expenseViewModel.applyFilterByPaymentMethods( + budgetAndIncomeExpenseFilter.paymentMethods, expenses + ) + } + ) + } + } else { + showToast(requireContext(), getString(R.string.something_went_wrong)) + dismiss() + } + + } + private fun showInPaymentMethodFragment(args: ShowExpenseBottomSheetFragmentArgs) { paymentMethodKey = args.paymentMethodKey ?: "" @@ -153,24 +189,37 @@ class ShowExpenseBottomSheetFragment : BottomSheetDialogFragment(), ExpenseAdapt date1 = args.date1 date2 = args.date2 + Constants.ONE_DAY_MILLISECONDS + val paymentMethodKeys = + if (args.paymentMethodKey.isValid()) convertJSONToStringList(args.paymentMethodKey) else emptyList() + if (dateRangeEnum == CustomDateRange.ALL_TIME) { - getAllExpenses() + getAllExpenses(paymentMethodKeys) } else if (dateRangeEnum == CustomDateRange.CUSTOM_DATE_RANGE) { - getExpensesByDateRange() + getExpensesByDateRange(paymentMethodKeys) } } - private fun getExpensesByDateRange() { + private fun getExpensesByDateRange(paymentMethodKeys: List = emptyList()) { expenseViewModel.getExpensesByDateRange(date1, date2) .observe(viewLifecycleOwner) { expenses -> - submitListToAdapter(expenses) + submitListToAdapter( + if (paymentMethodKeys.isEmpty()) expenses else expenseViewModel.applyFilterByPaymentMethods( + paymentMethodKeys, + expenses + ) + ) } } - private fun getAllExpenses() { + private fun getAllExpenses(paymentMethodKeys: List = emptyList()) { expenseViewModel.getAllExpenses().observe(viewLifecycleOwner) { expenses -> - submitListToAdapter(expenses) + submitListToAdapter( + if (paymentMethodKeys.isEmpty()) expenses else expenseViewModel.applyFilterByPaymentMethods( + paymentMethodKeys, + expenses + ) + ) } } diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddBudgetFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddBudgetFragment.kt new file mode 100644 index 00000000..fe604ff0 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddBudgetFragment.kt @@ -0,0 +1,611 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.PopupMenu +import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome.SetBudgetExpenseCategoryAdapter +import com.rohitthebest.manageyourrenters.data.CustomDateRange +import com.rohitthebest.manageyourrenters.data.ShowExpenseBottomSheetTagsEnum +import com.rohitthebest.manageyourrenters.data.filter.BudgetAndIncomeExpenseFilter +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.databinding.FragmentAddBudgetBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.ui.fragments.MonthAndYearPickerDialog +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetViewModel +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showNoInternetMessage +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.convertStringListToJSON +import com.rohitthebest.manageyourrenters.utils.convertToJsonString +import com.rohitthebest.manageyourrenters.utils.executeAfterDelay +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.isValid +import com.rohitthebest.manageyourrenters.utils.onTextChanged +import com.rohitthebest.manageyourrenters.utils.onTextSubmit +import com.rohitthebest.manageyourrenters.utils.show +import com.rohitthebest.manageyourrenters.utils.showAlertDialogForDeletion +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val TAG = "AddBudgetFragment" + +@AndroidEntryPoint +class AddBudgetFragment : Fragment(R.layout.fragment_add_budget), + SetBudgetExpenseCategoryAdapter.OnClickListener, + AddBudgetLimitBottomSheetFragment.OnBottomSheetDismissListener, + MonthAndYearPickerDialog.OnMonthAndYearDialogDismissListener, + ChooseMonthAndYearBottomSheetFragment.OnBottomSheetDismissListener { + + private var _binding: FragmentAddBudgetBinding? = null + private val binding get() = _binding!! + + private val budgetViewModel by viewModels() + + private var selectedMonth: Int = 0 + private var selectedYear: Int = 0 + private var monthList: List = emptyList() + + private lateinit var setBudgetExpenseCategoryAdapter: SetBudgetExpenseCategoryAdapter + private var adapterPosition = 0 + private var searchView: SearchView? = null + + private var oldestYearWhenBudgetWasSaved = 2000 + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentAddBudgetBinding.bind(view) + + monthList = resources.getStringArray(R.array.months).toList() + setBudgetExpenseCategoryAdapter = SetBudgetExpenseCategoryAdapter() + + binding.progressBar.show() + + getMessage() + initListeners() + setUpRecyclerView() + observerBudgetList() + } + + private fun observerBudgetList() { + + budgetViewModel.allExpenseCategoryAsBudgets.observe(viewLifecycleOwner) { budgets -> + setUpSearchViewMenu(budgets) + getTotalBudget() + binding.progressBar.hide() + } + } + + private fun setUpRecyclerView() { + + binding.setBudgetRV.apply { + + setHasFixedSize(true) + layoutManager = LinearLayoutManager(requireContext()) + adapter = setBudgetExpenseCategoryAdapter + } + setBudgetExpenseCategoryAdapter.setOnClickListener(this) + + } + + override fun onItemClicked(expenseCategoryKey: String, isBudgetLimitAdded: Boolean) { + + if (!isBudgetLimitAdded) { + showToast(requireContext(), getString(R.string.please_add_limit_for_this_category)) + return + } + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + selectedMonth, selectedYear + ) + + val action = + AddBudgetFragmentDirections.actionAddBudgetFragmentToShowExpenseBottomSheetFragment( + null, + CustomDateRange.CUSTOM_DATE_RANGE, + datePairForExpense.first, + datePairForExpense.second, + ShowExpenseBottomSheetTagsEnum.BUDGET_AND_INCOME_FRAGMENT, + BudgetAndIncomeExpenseFilter(listOf(expenseCategoryKey), emptyList()) + ) + + findNavController().navigate(action) + } + + override fun onAddBudgetClicked(budget: Budget, position: Int) { + + adapterPosition = position + val bundle = Bundle() + bundle.putBoolean(Constants.IS_FOR_EDIT, false) + bundle.putString(Constants.BUDGET, budget.convertToJsonString()) + + requireActivity().supportFragmentManager.let { fm -> + + AddBudgetLimitBottomSheetFragment.newInstance(bundle) + .apply { + show(fm, TAG) + }.setOnBottomSheetDismissListener(this) + } + } + + override fun onBottomSheetDismissed(isBudgetLimitAdded: Boolean) { + + if (isBudgetLimitAdded) { + + getAllExpenseCategoriesAsBudget() + + setBudgetExpenseCategoryAdapter.notifyItemChanged(adapterPosition) + } + } + + + override fun onBudgetMenuBtnClicked(budget: Budget, view: View, position: Int) { + + adapterPosition = position + + val popupMenu = PopupMenu(requireContext(), view) + popupMenu.menuInflater.inflate(R.menu.menu_add_budget_item, popupMenu.menu) + + popupMenu.show() + + popupMenu.setOnMenuItemClickListener { + + return@setOnMenuItemClickListener when (it.itemId) { + + R.id.menu_ab_edit -> { + + handleEditBudgetMenu(budget) + true + } + + R.id.menu_ab_remove_limit -> { + + handleRemoveBudgetLimitMenu(budget) + true + } + + else -> false + } + + } + } + + override fun onBudgetSyncBtnClicked(budget: Budget, position: Int) { + + if (isInternetAvailable(requireContext())) { + + if (!budget.isSynced) { + + budgetViewModel.insertBudget(budget) + setBudgetExpenseCategoryAdapter.notifyItemChanged(position) + } + + } else { + showNoInternetMessage(requireContext()) + } + } + + private fun handleRemoveBudgetLimitMenu(budget: Budget) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.are_you_sure)) + .setMessage(getString(R.string.remove_budget_warning)) + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> + if (requireContext().isInternetAvailable()) { + budgetViewModel.deleteBudget(budget) + getAllExpenseCategoriesAsBudget() + setBudgetExpenseCategoryAdapter.notifyItemChanged(adapterPosition) + } else { + showNoInternetMessage(requireContext()) + } + dialog.dismiss() + } + .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.dismiss() } + .create() + .show() + + } + + private fun handleEditBudgetMenu(budget: Budget) { + + val bundle = Bundle() + bundle.putBoolean(Constants.IS_FOR_EDIT, true) + bundle.putString(Constants.DOCUMENT_KEY, budget.key) + + requireActivity().supportFragmentManager.let { fm -> + + AddBudgetLimitBottomSheetFragment.newInstance(bundle) + .apply { + show(fm, TAG) + }.setOnBottomSheetDismissListener(this) + } + } + + private fun getMessage() { + + try { + + if (!arguments?.isEmpty!!) { + + val args = arguments?.let { + AddBudgetFragmentArgs.fromBundle(it) + } + + selectedMonth = args?.monthMessage ?: WorkingWithDateAndTime.getCurrentMonth() + selectedYear = args?.yearMessage ?: WorkingWithDateAndTime.getCurrentYear() + } else { + selectedMonth = WorkingWithDateAndTime.getCurrentMonth() + selectedYear = WorkingWithDateAndTime.getCurrentYear() + } + + lifecycleScope.launch { + delay(300) + initUI() + } + } catch (e: Exception) { + e.printStackTrace() + + selectedMonth = WorkingWithDateAndTime.getCurrentMonth() + selectedYear = WorkingWithDateAndTime.getCurrentYear() + lifecycleScope.launch { + delay(300) + initUI() + } + } + + } + + private fun getAllExpenseCategoriesAsBudget() { + + budgetViewModel.getAllExpenseCategoryAsBudget(selectedMonth, selectedYear) + } + + private var searchTextDelayJob: Job? = null + private fun setUpSearchViewMenu(budgets: List) { + + searchView = + binding.toolbar.menu.findItem(R.id.menu_search_budget_list).actionView as SearchView + + searchView?.let { sv -> + + searchBudget(sv.query.toString(), budgets) + + sv.onTextSubmit { query -> searchBudget(query ?: "", budgets) } + sv.onTextChanged { query -> + + searchTextDelayJob = lifecycleScope.launch { + searchTextDelayJob.executeAfterDelay { + searchBudget(query ?: "", budgets) + } + } + } + } + } + + private fun searchBudget(query: String, budgets: List) { + + if (!query.isValid()) { + setBudgetExpenseCategoryAdapter.submitList(budgets) + + showNoBudgetAddedTV( + budgets.isEmpty(), + getString(R.string.no_expense_category_added_for_budget_message) + ) + + } else { + + val filteredList = budgets.filter { budget -> + + budget.categoryName.lowercase() + .contains(query.trim().lowercase()) + } + + showNoBudgetAddedTV( + filteredList.isEmpty(), + getString(R.string.no_matching_results_found_message) + ) + + setBudgetExpenseCategoryAdapter.submitList(filteredList) + } + } + + private fun showNoBudgetAddedTV( + isVisible: Boolean, + noExpenseCategoryAddedForBudgetMessage: String = "" + ) { + + binding.noBudgetLimitAddedTV.text = noExpenseCategoryAddedForBudgetMessage + binding.noBudgetLimitAddedTV.isVisible = isVisible + binding.setBudgetRV.isVisible = !isVisible + } + + private fun initListeners() { + + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + binding.nextMonthBtn.setOnClickListener { + handleNextDateButton() + } + binding.previousMonthBtn.setOnClickListener { + handlePreviousDateButton() + } + binding.monthMCV.setOnClickListener { + handleMonthAndYearSelection() + } + binding.toolbar.menu.findItem(R.id.copy_previous_months_budget).setOnMenuItemClickListener { + + handleCopyPreviousMonthBudgetMenu() + true + } + binding.toolbar.menu.findItem(R.id.delete_all_budget_limit).setOnMenuItemClickListener { + handleDeleteAllBudgetLimitMenu() + true + } + + } + + private fun handleDeleteAllBudgetLimitMenu() { + + showAlertDialogForDeletion( + context = requireContext(), + message = getString(R.string.all_the_budget_limits_for_this_month_will_be_reset), + positiveButtonListener = { + + if (isInternetAvailable(requireContext())) { + budgetViewModel.deleteAllBudgetsByMonthAndYear(selectedMonth, selectedYear) + getAllExpenseCategoriesAsBudget() + } else { + showNoInternetMessage(requireContext()) + } + + it.dismiss() + }, + negativeButtonListener = { + it.dismiss() + } + ) + } + + private var isBudgetAddedForSelectedMonth = false + private fun handleCopyPreviousMonthBudgetMenu() { + + var isRefreshEnabled = true + + budgetViewModel.getAllBudgetMonthAndYearForWhichBudgetIsAdded() + .observe(viewLifecycleOwner) { monthYearStringList -> + + if (isRefreshEnabled) { + + Log.d(TAG, "initListeners: monthYearString: $monthYearStringList") + + val monthYearStringForSelectedMonthAndYear = + WorkingWithDateAndTime.getMonthAndYearString( + selectedMonth, + selectedYear + ) + + val monthYearStringListAfterRemovingSelectedMonth = + ArrayList(monthYearStringList) + + monthYearStringListAfterRemovingSelectedMonth.remove( + monthYearStringForSelectedMonthAndYear + ) + + if (monthYearStringListAfterRemovingSelectedMonth.isNotEmpty()) { + + budgetViewModel.isAnyBudgetAddedForThisMonthAndYear( + monthYearStringForSelectedMonthAndYear + ).observe(viewLifecycleOwner) { budgetKeys -> + + if (isRefreshEnabled) { + if (budgetKeys.isNotEmpty()) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.are_you_sure)) + .setMessage(getString(R.string.replace_the_current_budget_limit_for_this_month)) + .setPositiveButton(getString(R.string.ok)) { dialog, _ -> + + isBudgetAddedForSelectedMonth = true + showPreviousMonthAndYearListForWhichBudgetIsAdded( + monthYearStringListAfterRemovingSelectedMonth + ) + dialog.dismiss() + } + .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> + dialog.dismiss() + } + .create() + .show() + + } else { + + isBudgetAddedForSelectedMonth = false + showPreviousMonthAndYearListForWhichBudgetIsAdded( + monthYearStringList + ) + } + } + isRefreshEnabled = false + } + + } else { + + showToast( + requireContext(), + getString(R.string.no_budget_limit_added_yet_for_any_month) + ) + } + + } + } + + } + + private fun showPreviousMonthAndYearListForWhichBudgetIsAdded(monthYearStringList: List?) { + + Log.d(TAG, "showPreviousMonthAndYearListForWhichBudgetIsAdded: monthYearString") + + val bundle = Bundle() + bundle.putString( + Constants.COPY_BUDGET_MONTH_AND_YEAR_KEY, + convertStringListToJSON(monthYearStringList ?: listOf("")) + ) + + requireActivity().supportFragmentManager.let { fm -> + + ChooseMonthAndYearBottomSheetFragment.newInstance(bundle) + .apply { + show(fm, TAG) + }.setOnBottomSheetDismissListener(this) + } + } + + override fun onMonthAndYearSelectedForCopyingBudget( + isMonthAndYearSelected: Boolean, + selectedMonthYearString: String // this is the month from which user want to copy/duplicate the budget + ) { + + budgetViewModel.duplicateBudgetOfPreviouslyAddedBudgetMonth( + isBudgetAddedForSelectedMonth, + Pair(selectedMonth, selectedYear), + selectedMonthYearString + ) + + lifecycleScope.launch { + binding.progressBar.show() + + delay(200) + + getAllExpenseCategoriesAsBudget() + + } + } + + + private fun handleMonthAndYearSelection() { + + val bundle = Bundle() + bundle.putInt(Constants.MONTH_YEAR_PICKER_MONTH_KEY, selectedMonth) + bundle.putInt(Constants.MONTH_YEAR_PICKER_YEAR_KEY, selectedYear) + bundle.putInt( + Constants.MONTH_YEAR_PICKER_MIN_YEAR_KEY, + oldestYearWhenBudgetWasSaved - 4 + ) + bundle.putInt( + Constants.MONTH_YEAR_PICKER_MAX_YEAR_KEY, + WorkingWithDateAndTime.getCurrentYear() + ) + + requireActivity().supportFragmentManager.let { fm -> + MonthAndYearPickerDialog.newInstance( + bundle + ).apply { + show(fm, TAG) + } + }.setOnMonthAndYearDialogDismissListener(this) + } + + override fun onMonthAndYearDialogDismissed( + isMonthAndYearSelected: Boolean, + selectedMonth: Int, + selectedYear: Int + ) { + + if (isMonthAndYearSelected) { + + this.selectedMonth = selectedMonth + this.selectedYear = selectedYear + handleUiAfterDateChange() + } + } + + + private fun initUI() { + + binding.monthAndYearTV.text = + getString(R.string.month_and_year, monthList[selectedMonth], selectedYear.toString()) + + handleUiAfterDateChange() + + budgetViewModel.getTheOldestSavedBudgetYear().observe(viewLifecycleOwner) { year -> + try { + if (year != null) { + oldestYearWhenBudgetWasSaved = year + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun handlePreviousDateButton() { + + if (selectedMonth == 0) { + selectedYear -= 1 + } + + selectedMonth = WorkingWithDateAndTime.getPreviousMonth(selectedMonth) + + handleUiAfterDateChange() + } + + private fun handleNextDateButton() { + + if (selectedMonth == 11) { + selectedYear += 1 + } + + selectedMonth = WorkingWithDateAndTime.getNextMonth(selectedMonth) + handleUiAfterDateChange() + } + + private fun handleUiAfterDateChange() { + + binding.monthAndYearTV.text = + getString(R.string.month_and_year, monthList[selectedMonth], selectedYear.toString()) + + getAllExpenseCategoriesAsBudget() + try { + binding.setBudgetRV.scrollToPosition(0) + } catch (_: Exception) { + } + + getTotalBudget() + } + + private fun getTotalBudget() { + + budgetViewModel.getTotalBudgetByMonthAndYear(selectedMonth, selectedYear) + .observe(viewLifecycleOwner) { totalBudget -> + try { + binding.toolbar.subtitle = + getString(R.string.total_with_arg, totalBudget.format(2)) + } catch (e: NullPointerException) { + e.printStackTrace() + binding.toolbar.subtitle = + getString(R.string.total_with_arg, getString(R.string._0_0)) + } + } + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddBudgetLimitBottomSheetFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddBudgetLimitBottomSheetFragment.kt new file mode 100644 index 00000000..81209030 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddBudgetLimitBottomSheetFragment.kt @@ -0,0 +1,269 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.text.InputFilter +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.databinding.EditTextBottomSheetLayoutBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetViewModel +import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.generateKey +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.getUid +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.convertJsonToObject +import com.rohitthebest.manageyourrenters.utils.isNotValid +import com.rohitthebest.manageyourrenters.utils.isTextValid +import com.rohitthebest.manageyourrenters.utils.isValid +import com.rohitthebest.manageyourrenters.utils.onTextChangedListener +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AddBudgetLimitBottomSheetFragment : BottomSheetDialogFragment() { + + private var _binding: EditTextBottomSheetLayoutBinding? = null + private val binding get() = _binding!! + + private val budgetViewModel by viewModels() + private var receivedBudgetKey = "" + private lateinit var receivedBudget: Budget + + private var isForEdit = false + private var mListener: OnBottomSheetDismissListener? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + return inflater.inflate(R.layout.edit_text_bottom_sheet_layout, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + _binding = EditTextBottomSheetLayoutBinding.bind(view) + + binding.toolbar.title = getString(R.string.add_budget_limit) + setUpEditText() + + getMessage() + initListeners() + textWatchers() + } + + private fun setUpEditText() { + + binding.editText.counterMaxLength = 12 + binding.editText.hint = getString(R.string.enter_budget_limit) + + binding.insideEditText.apply { + + inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL + + val maxLengthFilter: Array = arrayOf(InputFilter.LengthFilter(12)) + filters = maxLengthFilter + + hint = getString(R.string.enter_budget_limit) + } + + binding.insideEditText.requestFocus() + Functions.showKeyboard(requireActivity(), binding.insideEditText) + + } + + private fun getMessage() { + + if (!arguments?.isEmpty!!) { + + arguments?.let { bundle -> + + try { + + isForEdit = bundle.getBoolean(Constants.IS_FOR_EDIT, false) + + if (isForEdit) { + + receivedBudgetKey = bundle.getString(Constants.DOCUMENT_KEY, "") + if (receivedBudgetKey.isNotValid()) { + requireContext().showToast(getString(R.string.something_went_wrong)) + dismiss() + } else { + + getBudgetByKey() + } + } else { + + try { + receivedBudget = bundle.getString(Constants.BUDGET, "") + .convertJsonToObject(Budget::class.java)!! + receivedBudgetKey = receivedBudget.key + } catch (e: NullPointerException) { + + requireContext().showToast(getString(R.string.something_went_wrong)) + dismiss() + } + + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + requireContext().showToast(getString(R.string.something_went_wrong)) + dismiss() + } + } + } + + } + + private fun getBudgetByKey() { + + budgetViewModel.getBudgetByKey(receivedBudgetKey).observe(viewLifecycleOwner) { budget -> + + receivedBudget = budget + updateUI() + } + } + + private fun updateUI() { + + if (this::receivedBudget.isInitialized) { + binding.editText.editText?.setText(receivedBudget.budgetLimit.toString()) + } + } + + private fun initListeners() { + + binding.toolbar.setNavigationOnClickListener { + Functions.hideKeyBoard(requireActivity()) + dismiss() + } + + binding.toolbar.menu.findItem(R.id.menu_save_btn).setOnMenuItemClickListener { + + if (isFormValid()) { + + if (isForEdit) { + + updateBudgetLimit() + } else { + insertBudgetLimit() + } + } + + true + } + } + + private fun updateBudgetLimit() { + + val budgetBefore = receivedBudget.copy() + + receivedBudget.apply { + + this.budgetLimit = binding.insideEditText.text.toString().trim().toDouble() + this.modified = System.currentTimeMillis() + } + + budgetViewModel.updateBudget(budgetBefore, receivedBudget) + + if (mListener != null) { + mListener!!.onBottomSheetDismissed( + true + ) + } + + dismiss() + } + + private fun insertBudgetLimit() { + + receivedBudget.apply { + + this.uid = getUid()!! + this.key = generateKey("_${this.uid}") + this.modified = System.currentTimeMillis() + this.created = System.currentTimeMillis() + this.budgetLimit = binding.insideEditText.text.toString().trim().toDouble() + } + + budgetViewModel.insertBudget(receivedBudget) + + if (mListener != null) { + mListener!!.onBottomSheetDismissed(true) + } + + dismiss() + } + + private fun isFormValid(): Boolean { + + if (!binding.editText.editText?.isTextValid()!!) { + binding.editText.error = Constants.EDIT_TEXT_EMPTY_MESSAGE + return false + } + + if (binding.insideEditText.text.toString().toDouble() <= 0.0) { + + binding.editText.error = + getString(R.string.budget_limit_should_be_grater_than_0) + return false + } + + return binding.editText.error == null + } + + private fun textWatchers() { + + binding.insideEditText.onTextChangedListener { s -> + + if (!s.toString().isValid()) { + + binding.editText.error = Constants.EDIT_TEXT_EMPTY_MESSAGE + } else { + + if (s?.toString()?.trim()?.toDouble()!! <= 0.0) { + binding.editText.error = + getString(R.string.budget_limit_should_be_grater_than_0) + } else { + binding.editText.error = null + } + } + } + } + + companion object { + @JvmStatic + fun newInstance(bundle: Bundle): AddBudgetLimitBottomSheetFragment { + val fragment = AddBudgetLimitBottomSheetFragment() + fragment.arguments = bundle + return fragment + } + } + + interface OnBottomSheetDismissListener { + + fun onBottomSheetDismissed( + isBudgetLimitAdded: Boolean + ) + } + + fun setOnBottomSheetDismissListener(listener: OnBottomSheetDismissListener) { + + mListener = listener + } + + + override fun onDestroyView() { + super.onDestroyView() + + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddIncomeBottomSheetFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddIncomeBottomSheetFragment.kt new file mode 100644 index 00000000..44ce06a2 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/AddIncomeBottomSheetFragment.kt @@ -0,0 +1,364 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.adapters.SelectPaymentMethodAdapter +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.database.model.PaymentMethod +import com.rohitthebest.manageyourrenters.databinding.FragmentAddIncomeBootmsheetBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.others.Constants.EDIT_TEXT_EMPTY_MESSAGE +import com.rohitthebest.manageyourrenters.ui.fragments.AddEditPaymentMethodBottomSheetFragment +import com.rohitthebest.manageyourrenters.ui.viewModels.IncomeViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.PaymentMethodViewModel +import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.isTextValid +import com.rohitthebest.manageyourrenters.utils.isValid +import com.rohitthebest.manageyourrenters.utils.onTextChangedListener +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val TAG = "AddIncomeBottomSheetFra" + +@AndroidEntryPoint +class AddIncomeBottomSheetFragment : BottomSheetDialogFragment(), + SelectPaymentMethodAdapter.OnClickListener { + + private var _binding: FragmentAddIncomeBootmsheetBinding? = null + private val binding get() = _binding!! + + private val incomeViewModel by viewModels() + private val paymentMethodViewModel by viewModels() + + private var isForEdit = false + private var receivedMonth: Int = 0 + private var receivedYear: Int = 0 + private var receivedIncomeKey = "" + private lateinit var receivedIncome: Income + + private var mListener: OnIncomeBottomSheetDismissListener? = null + private lateinit var selectPaymentMethodAdapter: SelectPaymentMethodAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + + return inflater.inflate(R.layout.fragment_add_income_bootmsheet, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentAddIncomeBootmsheetBinding.bind(view) + + selectPaymentMethodAdapter = SelectPaymentMethodAdapter() + + getMessage() + initListener() + textWatchers() + setUpSourceAutoTextView() + setUpPaymentMethodRecyclerView() + } + + private fun setUpPaymentMethodRecyclerView() { + + binding.linkPaymentMethodsRV.apply { + + adapter = selectPaymentMethodAdapter + layoutManager = StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL) + setHasFixedSize(true) + } + + selectPaymentMethodAdapter.setOnClickListener(this) + } + + override fun onItemClick(paymentMethod: PaymentMethod, position: Int) { + + if (paymentMethod.key != Constants.ADD_PAYMENT_METHOD_KEY) { + + paymentMethod.isSelected = !paymentMethod.isSelected + selectPaymentMethodAdapter.notifyItemChanged(position) + } else { + + requireActivity().supportFragmentManager.let { + + AddEditPaymentMethodBottomSheetFragment.newInstance(Bundle()) + .apply { + show(it, TAG) + } + } + } + } + + private fun setUpSourceAutoTextView() { + + incomeViewModel.getAllIncomeSources().observe(viewLifecycleOwner) { sources -> + + val adapter = ArrayAdapter( + requireContext(), + android.R.layout.simple_list_item_1, + sources + ) + + binding.insideEditTextSource.setAdapter(adapter) + } + } + + private fun getMessage() { + + if (!arguments?.isEmpty!!) { + + arguments?.let { bundle -> + + try { + + isForEdit = bundle.getBoolean(Constants.IS_FOR_EDIT, false) + + if (isForEdit) { + + receivedIncomeKey = bundle.getString(Constants.DOCUMENT_KEY, "") + if (receivedIncomeKey.isValid()) { + + getIncomeByKey() + } else { + requireContext().showToast(getString(R.string.something_went_wrong)) + dismiss() + } + + binding.toolbar.title = getString(R.string.edit_income) + + } else { + + receivedMonth = bundle.getInt(Constants.INCOME_MONTH_KEY, -1) + receivedYear = bundle.getInt(Constants.INCOME_YEAR_KEY, -1) + + if (receivedMonth == -1 || receivedYear == -1) { + + showToast( + requireContext(), + getString(R.string.no_month_or_year_received_for_which_income_needs_to_be_added) + ) + dismiss() + } + } + + lifecycleScope.launch { + + delay(300) + getAllPaymentMethods() + } + + + } catch (e: java.lang.Exception) { + e.printStackTrace() + requireContext().showToast(getString(R.string.something_went_wrong)) + dismiss() + } + } + } + + } + + private fun getAllPaymentMethods() { + + paymentMethodViewModel.getAllPaymentMethods() + .observe(viewLifecycleOwner) { paymentMethods -> + + if (isForEdit && selectPaymentMethodAdapter.currentList.isEmpty()) { + + receivedIncome.linkedPaymentMethods?.let { alreadySelectedPM -> + paymentMethods.forEach { pm -> + if (alreadySelectedPM.contains(pm.key)) { + pm.isSelected = true + } + } + } + } + + val addPaymentMethod = PaymentMethod( + key = Constants.ADD_PAYMENT_METHOD_KEY, + paymentMethod = getString(R.string.add), + uid = "", + isSynced = false, + isSelected = false + ) + + selectPaymentMethodAdapter.submitList(paymentMethods + listOf(addPaymentMethod)) + } + + } + + private fun getIncomeByKey() { + + incomeViewModel.getIncomeByKey(receivedIncomeKey).observe(viewLifecycleOwner) { income -> + receivedIncome = income + receivedMonth = receivedIncome.month + receivedYear = receivedIncome.year + updateUI() + } + } + + private fun updateUI() { + + if (this::receivedIncome.isInitialized) { + + binding.apply { + insideEditTextIncome.setText(receivedIncome.income.toString()) + insideEditTextSource.setText(receivedIncome.source) + } + } + } + + + private fun initListener() { + + binding.toolbar.setNavigationOnClickListener { + + if (mListener != null) { + mListener!!.onIncomeBottomSheetDismissed(false) + } + dismiss() + } + + binding.toolbar.menu.findItem(R.id.menu_save_btn).setOnMenuItemClickListener { + + if (isFormValid()) { + + initIncome() + } + true + } + } + + private fun initIncome() { + + var income = Income() + + if (isForEdit) { + + income = receivedIncome.copy() + } else { + + income.apply { + this.created = System.currentTimeMillis() + this.uid = Functions.getUid()!! + this.key = Functions.generateKey("_${Functions.getUid()}") + this.month = receivedMonth + this.year = receivedYear + this.monthYearString = this.generateMonthYearString() + this.isSynced = isInternetAvailable(requireContext()) + } + } + + income.source = binding.insideEditTextSource.text.toString().trim() + income.income = binding.insideEditTextIncome.text.toString().trim().toDouble() + + val selectedPaymentMethods = + selectPaymentMethodAdapter.currentList.filter { pm -> pm.isSelected } + .map { pm -> pm.key } + + income.linkedPaymentMethods = + selectedPaymentMethods.ifEmpty { listOf(Constants.PAYMENT_METHOD_OTHER_KEY) } + + income.modified = System.currentTimeMillis() + + if (!isForEdit) { + incomeViewModel.insertIncome(income) + } else { + incomeViewModel.updateIncome( + receivedIncome, income + ) + } + + if (mListener != null) { + + mListener!!.onIncomeBottomSheetDismissed(true) + } + + dismiss() + } + + private fun isFormValid(): Boolean { + + if (!binding.insideEditTextIncome.isTextValid()) { + + binding.incomeET.error = EDIT_TEXT_EMPTY_MESSAGE + return false + } + + if (!binding.insideEditTextSource.isTextValid()) { + binding.sourceET.error = EDIT_TEXT_EMPTY_MESSAGE + return false + } + + return binding.incomeET.error == null && binding.sourceET.error == null + } + + private fun textWatchers() { + + binding.insideEditTextIncome.onTextChangedListener { s -> + + if (!s?.toString().isValid()) { + + binding.incomeET.error = EDIT_TEXT_EMPTY_MESSAGE + } else { + + if (s?.toString()?.trim()?.toDouble()!! <= 0.0) { + binding.incomeET.error = getString(R.string.income_should_be_grater_than_0) + } else { + binding.incomeET.error = null + } + } + } + + binding.insideEditTextSource.onTextChangedListener { s -> + + if (!s?.toString().isValid()) { + + binding.sourceET.error = EDIT_TEXT_EMPTY_MESSAGE + } else { + + binding.sourceET.error = null + } + } + } + + interface OnIncomeBottomSheetDismissListener { + + fun onIncomeBottomSheetDismissed( + isIncomeAdded: Boolean + ) + } + + fun setOnBottomSheetDismissListener(listener: OnIncomeBottomSheetDismissListener) { + + mListener = listener + } + + + companion object { + @JvmStatic + fun newInstance(bundle: Bundle): AddIncomeBottomSheetFragment { + val fragment = AddIncomeBottomSheetFragment() + fragment.arguments = bundle + return fragment + } + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetAndIncomeGraphFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetAndIncomeGraphFragment.kt new file mode 100644 index 00000000..e362cdb6 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetAndIncomeGraphFragment.kt @@ -0,0 +1,243 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.BarData +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.BarEntry +import com.github.mikephil.charting.formatter.IndexAxisValueFormatter +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.data.filter.ExpenseFilterDto +import com.rohitthebest.manageyourrenters.databinding.FragmentBudgetAndIncomeGraphBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants +import com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.ShowPaymentMethodSelectorDialogFragment +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetAndIncomeGraphViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetViewModel +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.convertToJsonString +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.setListToSpinner +import com.rohitthebest.manageyourrenters.utils.show +import dagger.hilt.android.AndroidEntryPoint + +private const val TAG = "BudgetAndIncomeGraphFragment" + +@AndroidEntryPoint +class BudgetAndIncomeGraphFragment : Fragment(R.layout.fragment_budget_and_income_graph), + ShowPaymentMethodSelectorDialogFragment.OnClickListener { + + private var _binding: FragmentBudgetAndIncomeGraphBinding? = null + private val binding get() = _binding!! + + private val budgetAndIncomeGraphViewModel by viewModels() + private val budgetViewModel by viewModels() + + private var oldestYearWhenBudgetWasSaved = 2000 + private var selectedYear = 2020 + + private var expenseFilterDto: ExpenseFilterDto? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentBudgetAndIncomeGraphBinding.bind(view) + + initUI() + initListeners() + + observeBarDataForBudgetIncomeAndExpense() + } + + private fun observeBarDataForBudgetIncomeAndExpense() { + + budgetAndIncomeGraphViewModel.incomeBudgetAndExpenseBarEntryData.observe(viewLifecycleOwner) { data -> + + binding.progressBar.show() + + data[FirestoreCollectionsConstants.BUDGETS]?.let { budgets -> + data[FirestoreCollectionsConstants.INCOMES]?.let { incomes -> + data[FirestoreCollectionsConstants.EXPENSES]?.let { expenses -> + initializeBarDataAndGraph( + budgets, + incomes, + expenses + ) + } + } + } + } + } + + private fun initUI() { + + budgetViewModel.getTheOldestSavedBudgetYear().observe(viewLifecycleOwner) { year -> + try { + oldestYearWhenBudgetWasSaved = year ?: WorkingWithDateAndTime.getCurrentYear() + + // set up year spinner + initYearSpinner( + oldestYearWhenBudgetWasSaved, + WorkingWithDateAndTime.getCurrentYear() + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun initYearSpinner(startYear: Int, endYear: Int) { + + val yearList = ArrayList() + + for (year in endYear downTo startYear) { + yearList.add(year) + } + + binding.yearSpinner.setListToSpinner( + context = requireContext(), + list = yearList, + position = { position -> + + selectedYear = yearList[position] + handleGraphData() + }, {} + ) + } + + private fun handleGraphData() { + + budgetAndIncomeGraphViewModel.getBarEntryDataForIncomeBudgetAndExpenseByYear( + selectedYear, expenseFilterDto?.paymentMethods ?: emptyList() + ) + } + + + private fun initializeBarDataAndGraph( + budgetBarEntry: MutableList, + incomeBarEntry: MutableList, + expenseBarEntry: MutableList + ) { + + val barDataSetBudget = BarDataSet(budgetBarEntry, getString(R.string.budget)) + barDataSetBudget.color = ContextCompat.getColor(requireContext(), R.color.blue_text_color) + + val barDataSetIncome = BarDataSet(incomeBarEntry, getString(R.string.income)) + barDataSetIncome.color = ContextCompat.getColor(requireContext(), R.color.color_green) + + val barDataSetExpense = BarDataSet(expenseBarEntry, getString(R.string.expense)) + barDataSetExpense.color = ContextCompat.getColor(requireContext(), R.color.color_Red) + + val barData = BarData(barDataSetIncome, barDataSetBudget, barDataSetExpense) + + binding.chart.data = barData + val months = resources.getStringArray(R.array.months) + + val xAxis = binding.chart.xAxis + xAxis.valueFormatter = IndexAxisValueFormatter(months) + xAxis.setCenterAxisLabels(true) + xAxis.position = XAxis.XAxisPosition.BOTTOM + xAxis.granularity = 1F + xAxis.isGranularityEnabled = true + + binding.chart.isDragEnabled = true + binding.chart.setVisibleXRangeMaximum(3f) + + val barSpace = 0.05f + val groupSpace = 0.31f + barData.barWidth = 0.18f + + binding.chart.apply { + xAxis.axisMinimum = 0f + xAxis.axisMaximum = + 0 + binding.chart.barData.getGroupWidth(groupSpace, barSpace) * 12 + axisLeft.axisMinimum = 0f + + groupBars(0f, groupSpace, barSpace) + description.text = context.getString(R.string.income_vs_budget_vs_expense) + animateY(600) + } + + binding.toolbar.subtitle = requireContext().getString(R.string.income_vs_budget_vs_expense) + binding.progressBar.hide() + binding.chart.invalidate() + } + + private fun initListeners() { + + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + binding.toolbar.menu.findItem(R.id.menu_saving_growth).setOnMenuItemClickListener { + + showToast(requireContext(), getString(R.string.coming_soon)) + true + } + + binding.toolbar.menu.findItem(R.id.menu_filter_income_budget_graph_by_paymentMethods) + .setOnMenuItemClickListener { + + showPaymentMethodSelectorDialog() + + true + } + + } + + private fun showPaymentMethodSelectorDialog() { + + requireActivity().supportFragmentManager.let { fragmentManager -> + + val bundle = Bundle() + bundle.putString( + Constants.EXPENSE_FILTER_KEY, + if (expenseFilterDto == null) "" else expenseFilterDto.convertToJsonString() + ) + + ShowPaymentMethodSelectorDialogFragment.newInstance( + bundle + ).apply { + show(fragmentManager, TAG) + }.setOnClickListener(this) + } + } + + override fun onFilterApply(selectedPaymentMethods: List?) { + + binding.toolbar.menu.findItem(R.id.menu_filter_income_budget_graph_by_paymentMethods) + .apply { + + if (selectedPaymentMethods.isNullOrEmpty()) { + this.icon = + ContextCompat.getDrawable( + requireContext(), + R.drawable.baseline_filter_list_24 + ) + + expenseFilterDto = null + + } else { + this.icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.baseline_filter_list_colored_24 + ) + + expenseFilterDto = ExpenseFilterDto() + expenseFilterDto!!.isPaymentMethodEnabled = true + expenseFilterDto!!.paymentMethods = selectedPaymentMethods + } + } + + handleGraphData() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetAndIncomeOverviewFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetAndIncomeOverviewFragment.kt new file mode 100644 index 00000000..bd619fe8 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetAndIncomeOverviewFragment.kt @@ -0,0 +1,662 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.view.View +import android.widget.PopupMenu +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome.BudgetRVAdapter +import com.rohitthebest.manageyourrenters.data.CustomDateRange +import com.rohitthebest.manageyourrenters.data.ShowExpenseBottomSheetTagsEnum +import com.rohitthebest.manageyourrenters.data.filter.BudgetAndIncomeExpenseFilter +import com.rohitthebest.manageyourrenters.data.filter.ExpenseFilterDto +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.databinding.FragmentBudgetBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.ui.fragments.MonthAndYearPickerDialog +import com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.ShowPaymentMethodSelectorDialogFragment +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.ExpenseViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.IncomeViewModel +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.changeVisibilityOfViewOnScrolled +import com.rohitthebest.manageyourrenters.utils.convertToJsonString +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.isNotValid +import com.rohitthebest.manageyourrenters.utils.show +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val TAG = "BudgetAndIncomeFragment" + +@AndroidEntryPoint +class BudgetAndIncomeOverviewFragment : Fragment(R.layout.fragment_budget), View.OnClickListener, + BudgetRVAdapter.OnClickListener, MonthAndYearPickerDialog.OnMonthAndYearDialogDismissListener, + ShowPaymentMethodSelectorDialogFragment.OnClickListener, + AddBudgetLimitBottomSheetFragment.OnBottomSheetDismissListener { + + private var _binding: FragmentBudgetBinding? = null + private val binding get() = _binding!! + + private val budgetViewModel by viewModels() + private val expenseViewModel by viewModels() + private val incomeViewModel by viewModels() + + private var selectedMonth: Int = 0 + private var selectedYear: Int = 0 + private var oldestYearWhenBudgetWasSaved = 2000 + private var monthList: List = emptyList() + + private lateinit var budgetAdapter: BudgetRVAdapter + + private var expenseFilterDto: ExpenseFilterDto? = null + + private var totalIncome = 0.0 + private var totalExpense = 0.0 + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentBudgetBinding.bind(view) + + monthList = resources.getStringArray(R.array.months).toList() + budgetAdapter = BudgetRVAdapter() + + initListeners() + setUpRecyclerView() + + binding.progressBar.show() + + observeBudgetList() + + initUI() + } + + private fun observeBudgetList() { + + budgetViewModel.allBudgetListByMonthAndYear.observe(viewLifecycleOwner) { budgets -> + + binding.noBudgetAddedTV.isVisible = budgets.isEmpty() + + if (budgets.isNotEmpty()) { + binding.iabAddBudgetFAB.text = getString(R.string.modify_budgets) + } else { + binding.iabAddBudgetFAB.text = getString(R.string.add_budget) + } + + budgetAdapter.submitList(budgets) + binding.progressBar.hide() + } + } + + private fun setUpRecyclerView() { + + binding.iabBudgetRV.apply { + + setHasFixedSize(true) + layoutManager = + LinearLayoutManager(requireContext()) + adapter = budgetAdapter + changeVisibilityOfViewOnScrolled(binding.iabAddBudgetFAB) + } + + budgetAdapter.setOnClickListener(this) + } + + override fun onItemClick(budget: Budget) { + + showExpensesInBottomSheet(budget.expenseCategoryKey) + } + + override fun onDetailsButtonClicked(budget: Budget) { + + val action = + BudgetAndIncomeOverviewFragmentDirections.actionBudgetAndIncomeFragmentToBudgetOverviewFragment( + budget.key, + selectedMonth, + selectedYear + ) + findNavController().navigate(action) + } + + + private var adapterPosition = 0 + override fun onMenuBtnClick(budget: Budget, view: View, position: Int) { + + adapterPosition = position + val popupMenu = PopupMenu(requireActivity(), view) + + popupMenu.menuInflater.inflate(R.menu.budget_overview_item_menu, popupMenu.menu) + + popupMenu.show() + + popupMenu.setOnMenuItemClickListener { + + return@setOnMenuItemClickListener when (it.itemId) { + + R.id.menuEditBudgetLimit_bo -> { + + handleEditBudgetMenu(budget) + true + } + + R.id.menuAddExpense_bo -> { + + handleAddExpenseMenu(budget) + true + } + + else -> { + false + } + } + } + + } + + + private fun handleAddExpenseMenu(budget: Budget) { + val action = + BudgetAndIncomeOverviewFragmentDirections.actionBudgetAndIncomeFragmentToAddEditExpense( + budget.expenseCategoryKey, "" + ) + findNavController().navigate(action) + } + + private fun handleEditBudgetMenu(budget: Budget) { + + val bundle = Bundle() + bundle.putBoolean(Constants.IS_FOR_EDIT, true) + bundle.putString(Constants.DOCUMENT_KEY, budget.key) + + requireActivity().supportFragmentManager.let { fm -> + + AddBudgetLimitBottomSheetFragment.newInstance(bundle) + .apply { + show(fm, TAG) + }.setOnBottomSheetDismissListener(this) + } + } + + override fun onBottomSheetDismissed(isBudgetLimitAdded: Boolean) { + + if (isBudgetLimitAdded) { + lifecycleScope.launch { + delay(200) + handleUiAfterChange() + } + } + } + + private fun initUI() { + + selectedMonth = WorkingWithDateAndTime.getCurrentMonth() + selectedYear = WorkingWithDateAndTime.getCurrentYear() + + binding.iabDateTV.text = + getString(R.string.month_and_year, monthList[selectedMonth], selectedYear.toString()) + + binding.progressBar.show() + + lifecycleScope.launch { + delay(300) + handleUiAfterChange() + } + + budgetViewModel.getTheOldestSavedBudgetYear().observe(viewLifecycleOwner) { year -> + try { + if (year != null) { + oldestYearWhenBudgetWasSaved = year + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun initListeners() { + + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + binding.nextMonthBtn.setOnClickListener(this) + binding.previousMonthBtn.setOnClickListener(this) + binding.iabIncomeAddBtn.setOnClickListener(this) + binding.iabBudgetMCV.setOnClickListener(this) + binding.iabSavingMCV.setOnClickListener(this) + binding.iabExpenseMCV.setOnClickListener(this) + binding.iabIncomeMCV.setOnClickListener(this) + binding.iabAddBudgetFAB.setOnClickListener(this) + binding.monthMCV.setOnClickListener(this) + + binding.toolbar.menu.findItem(R.id.menu_filter_income_and_budget_overview) + .setOnMenuItemClickListener { + + showPaymentMethodSelectorDialog() + + true + } + + binding.toolbar.menu.findItem(R.id.menu_budget_and_income_graph) + .setOnMenuItemClickListener { + + openGraphFragment() + true + } + } + + private fun openGraphFragment() { + + findNavController().navigate(R.id.action_budgetAndIncomeFragment_to_budgetAndIncomeGraphFragment) + } + + private fun showPaymentMethodSelectorDialog() { + + requireActivity().supportFragmentManager.let { fragmentManager -> + + val bundle = Bundle() + bundle.putString( + Constants.EXPENSE_FILTER_KEY, + if (expenseFilterDto == null) "" else expenseFilterDto.convertToJsonString() + ) + + ShowPaymentMethodSelectorDialogFragment.newInstance( + bundle + ).apply { + show(fragmentManager, TAG) + }.setOnClickListener(this) + } + } + + override fun onFilterApply(selectedPaymentMethods: List?) { + + binding.toolbar.menu.findItem(R.id.menu_filter_income_and_budget_overview).apply { + + if (selectedPaymentMethods.isNullOrEmpty()) { + this.icon = + ContextCompat.getDrawable(requireContext(), R.drawable.baseline_filter_list_24) + + expenseFilterDto = null + + } else { + this.icon = ContextCompat.getDrawable( + requireContext(), + R.drawable.baseline_filter_list_colored_24 + ) + + expenseFilterDto = ExpenseFilterDto() + expenseFilterDto!!.isPaymentMethodEnabled = true + expenseFilterDto!!.paymentMethods = selectedPaymentMethods + } + } + + // apply payment filter on income and expense + handleUiAfterChange() + } + + + override fun onClick(v: View?) { + + when (v?.id) { + + binding.nextMonthBtn.id -> { + + handleNextDateButton() + } + + binding.previousMonthBtn.id -> { + handlePreviousDateButton() + } + + binding.iabIncomeAddBtn.id -> { + + val bundle = Bundle() + + bundle.putBoolean(Constants.IS_FOR_EDIT, false) + bundle.putInt(Constants.INCOME_MONTH_KEY, selectedMonth) + bundle.putInt(Constants.INCOME_YEAR_KEY, selectedYear) + + requireActivity().supportFragmentManager.let { fragmentManager -> + + AddIncomeBottomSheetFragment.newInstance( + bundle + ).apply { + show(fragmentManager, TAG) + } + } + } + + binding.iabAddBudgetFAB.id -> { + + val action = + BudgetAndIncomeOverviewFragmentDirections.actionBudgetAndIncomeFragmentToAddBudgetFragment( + selectedMonth, selectedYear + ) + + findNavController().navigate(action) + } + + binding.iabIncomeMCV.id -> { + + val action = + BudgetAndIncomeOverviewFragmentDirections.actionBudgetAndIncomeFragmentToIncomeFragment( + selectedMonth, selectedYear + ) + findNavController().navigate(action) + } + + binding.monthMCV.id -> { + + handleMonthAndYearSelection() + } + + binding.iabExpenseMCV.id -> { + showExpensesInBottomSheet() + } + } + } + + private fun showExpensesInBottomSheet(categoryKey: String = "") { + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + selectedMonth, selectedYear + ) + + if (categoryKey.isNotValid()) { + + // show all the expenses of the category for which budget is added + + budgetViewModel.getAllExpenseCategoryOfBudgetsByMonthAndYear( + selectedMonth, selectedYear + ) + .observe(viewLifecycleOwner) { categoryKeys -> + + if (categoryKeys.isNotEmpty()) { + + val action = + BudgetAndIncomeOverviewFragmentDirections.actionBudgetAndIncomeFragmentToShowExpenseBottomSheetFragment( + null, + CustomDateRange.CUSTOM_DATE_RANGE, + datePairForExpense.first, + datePairForExpense.second, + ShowExpenseBottomSheetTagsEnum.BUDGET_AND_INCOME_FRAGMENT, + BudgetAndIncomeExpenseFilter( + categoryKeys, + if (expenseFilterDto == null) emptyList() else expenseFilterDto!!.paymentMethods + ) + ) + + findNavController().navigate(action) + + } else { + showToast( + requireContext(), + getString(R.string.click_on_add_budget_button_to_add_budgets_for_this_month) + ) + } + } + + } else { + + // show all the expenses for a clicked budget category + + val action = + BudgetAndIncomeOverviewFragmentDirections.actionBudgetAndIncomeFragmentToShowExpenseBottomSheetFragment( + null, + CustomDateRange.CUSTOM_DATE_RANGE, + datePairForExpense.first, + datePairForExpense.second, + ShowExpenseBottomSheetTagsEnum.BUDGET_AND_INCOME_FRAGMENT, + BudgetAndIncomeExpenseFilter( + listOf(categoryKey), + if (expenseFilterDto == null) emptyList() else expenseFilterDto!!.paymentMethods + ) + ) + + findNavController().navigate(action) + } + } + + private fun handleMonthAndYearSelection() { + + val bundle = Bundle() + bundle.putInt(Constants.MONTH_YEAR_PICKER_MONTH_KEY, selectedMonth) + bundle.putInt(Constants.MONTH_YEAR_PICKER_YEAR_KEY, selectedYear) + bundle.putInt( + Constants.MONTH_YEAR_PICKER_MIN_YEAR_KEY, + oldestYearWhenBudgetWasSaved - 4 + ) + bundle.putInt( + Constants.MONTH_YEAR_PICKER_MAX_YEAR_KEY, + WorkingWithDateAndTime.getCurrentYear() + ) + + requireActivity().supportFragmentManager.let { fm -> + MonthAndYearPickerDialog.newInstance( + bundle + ).apply { + show(fm, TAG) + } + }.setOnMonthAndYearDialogDismissListener(this) + } + + override fun onMonthAndYearDialogDismissed( + isMonthAndYearSelected: Boolean, + selectedMonth: Int, + selectedYear: Int + ) { + + if (isMonthAndYearSelected) { + + this.selectedMonth = selectedMonth + this.selectedYear = selectedYear + handleUiAfterChange() + } + } + + + private fun handlePreviousDateButton() { + + if (selectedMonth == 0) { + selectedYear -= 1 + } + + selectedMonth = WorkingWithDateAndTime.getPreviousMonth(selectedMonth) + + handleUiAfterChange() + } + + private fun handleNextDateButton() { + + if (selectedMonth == 11) { + selectedYear += 1 + } + + selectedMonth = WorkingWithDateAndTime.getNextMonth(selectedMonth) + handleUiAfterChange() + } + + private fun handleUiAfterChange() { + + binding.progressBar.show() + + setMonthAndYearInTextView() + + // get all budgets (this list is being observed in the method observeBudgetList()) + budgetViewModel.getAllBudgetsByMonthAndYear( + selectedMonth, + selectedYear, + if (expenseFilterDto == null) emptyList() else expenseFilterDto!!.paymentMethods + ) + + binding.progressBar.show() + + lifecycleScope.launch { + delay(350) + budgetAdapter.notifyDataSetChanged() + } + + showTotalExpense() + showTotalBudget() + showTotalIncomeAdded() + + lifecycleScope.launch { + delay(250) + showTotalSavings() + } + } + + private fun showTotalSavings() { + + binding.iabSavingValueTV.text = (totalIncome - totalExpense).format(2) + } + + private fun showTotalIncomeAdded() { + + if (expenseFilterDto != null && expenseFilterDto!!.paymentMethods.isNotEmpty()) { + + incomeViewModel.getAllIncomesByMonthAndYear(selectedMonth, selectedYear) + .observe(viewLifecycleOwner) { incomes -> + + try { + if (incomes.isEmpty()) { + updateIncomeTotalUI(0.0) + } else { + val tempIncome = incomeViewModel.applyFilterByPaymentMethods( + expenseFilterDto!!.paymentMethods, incomes + ) + + updateIncomeTotalUI(tempIncome.sumOf { it.income }) + } + } catch (e: NullPointerException) { + e.printStackTrace() + totalIncome = 0.0 + binding.iabIncomeValueTV.text = getString(R.string._0_0) + } + } + } else { + incomeViewModel.getTotalIncomeAddedByMonthAndYear( + selectedMonth, selectedYear + ).observe(viewLifecycleOwner) { totalIncomeAdded -> + + try { + updateIncomeTotalUI(totalIncomeAdded) + } catch (e: NullPointerException) { + e.printStackTrace() + totalIncome = 0.0 + binding.iabIncomeValueTV.text = getString(R.string._0_0) + } + } + } + } + + private fun updateIncomeTotalUI(totalIncomeAdded: Double) { + + totalIncome = totalIncomeAdded + binding.iabIncomeValueTV.text = totalIncomeAdded.format(2) + showTotalSavings() + } + + + private fun showTotalExpense() { + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + selectedMonth, selectedYear + ) + + budgetViewModel.getAllExpenseCategoryOfBudgetsByMonthAndYear( + selectedMonth, selectedYear + ) + .observe(viewLifecycleOwner) { categoryKeys -> + + if (categoryKeys.isNotEmpty()) { + + if (expenseFilterDto != null && expenseFilterDto!!.paymentMethods.isNotEmpty()) { + + // get total expense by filtering the list by paymentMethods + expenseViewModel.getExpenseByCategoryKeysAndDateRange( + categoryKeys, + datePairForExpense.first, + datePairForExpense.second + Constants.ONE_DAY_MILLISECONDS + ).observe(viewLifecycleOwner) { expenses -> + + if (expenses.isEmpty()) { + updateExpenseTotalUI(0.0) + } else { + val tempExpense = expenseViewModel.applyFilterByPaymentMethods( + expenseFilterDto!!.paymentMethods, + expenses + ) + + updateExpenseTotalUI(tempExpense.sumOf { it.amount }) + } + } + + } else { + + expenseViewModel.getTotalExpenseByCategoryKeysAndDateRange( + categoryKeys, + datePairForExpense.first, + datePairForExpense.second + Constants.ONE_DAY_MILLISECONDS + ).observe(viewLifecycleOwner) { total -> + + try { + updateExpenseTotalUI(total) + } catch (e: NullPointerException) { + e.printStackTrace() + totalExpense = 0.0 + binding.iabExpenseValueTV.text = getString(R.string._0_0) + } + } + } + } else { + binding.iabExpenseValueTV.text = getString(R.string._0_0) + totalExpense = 0.0 + } + } + } + + private fun updateExpenseTotalUI(total: Double) { + + totalExpense = total + binding.iabExpenseValueTV.text = total.format(2) + showTotalSavings() + } + + private fun showTotalBudget() { + + budgetViewModel.getTotalBudgetByMonthAndYear(selectedMonth, selectedYear) + .observe(viewLifecycleOwner) { totalBudget -> + try { + binding.iabBudgetValueTV.text = totalBudget.format(2) + } catch (e: NullPointerException) { + e.printStackTrace() + binding.iabBudgetValueTV.text = getString(R.string._0_0) + } + } + } + + + private fun setMonthAndYearInTextView() { + + binding.iabDateTV.text = + getString(R.string.month_and_year, monthList[selectedMonth], selectedYear.toString()) + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetOverviewFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetOverviewFragment.kt new file mode 100644 index 00000000..9195851e --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/BudgetOverviewFragment.kt @@ -0,0 +1,300 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.content.res.ColorStateList +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.BarData +import com.github.mikephil.charting.data.BarDataSet +import com.github.mikephil.charting.data.BarEntry +import com.github.mikephil.charting.formatter.IndexAxisValueFormatter +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.databinding.BudgetOverviewLayoutBinding +import com.rohitthebest.manageyourrenters.databinding.FragmentBudgetOverviewBinding +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetAndIncomeGraphViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetViewModel +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.getAppropriateBudgetSuggestionOrMessage +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.setImageToImageViewUsingGlide +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.changeTextColor +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.isNotValid +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class BudgetOverviewFragment : Fragment(R.layout.fragment_budget_overview) { + + private var _binding: FragmentBudgetOverviewBinding? = null + private val binding get() = _binding!! + + private val budgetViewModel by viewModels() + private val budgetAndIncomeGraphViewModel by viewModels() + + private var receivedBudgetKey = "" + private var receivedMonth = 0 + private var receivedYear = 0 + private lateinit var receivedBudget: Budget + private lateinit var includeLayoutBinding: BudgetOverviewLayoutBinding + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentBudgetOverviewBinding.bind(view) + + includeLayoutBinding = binding.includeLayout + + initListeners() + getMessage() + observeBudget() + observeGraph() + } + + private fun observeGraph() { + + budgetAndIncomeGraphViewModel.budgetExpense.observe(viewLifecycleOwner) { data -> + setUpGraph(data) + } + } + + private fun setUpGraph(dataEntry: List) { + + val barDataSet = BarDataSet(dataEntry, getString(R.string.expenses)) + barDataSet.color = ContextCompat.getColor(requireContext(), R.color.purple_500) + + val barData = BarData(barDataSet) + includeLayoutBinding.budgetPerDayChart.data = barData + + val days = ArrayList() + + val numberOfDaysInMonth = + WorkingWithDateAndTime.getNumberOfDaysInMonth(receivedMonth, receivedYear) + + val daysOfWeek = + WorkingWithDateAndTime.getDayOfWeeksForEntireMonth(receivedMonth, receivedYear) + + for (i in 0..numberOfDaysInMonth) { + + days.add( + when { + + i.toString().startsWith("0") -> i.toString() + i.toString().endsWith("1") -> i.toString() + "st (${daysOfWeek[i - 1].second})" + i.toString().endsWith("2") -> i.toString() + "nd (${daysOfWeek[i - 1].second})" + i.toString().endsWith("3") -> i.toString() + "rd (${daysOfWeek[i - 1].second})" + else -> i.toString() + "th (${daysOfWeek[i - 1].second})" + } + ) + } + + val xAxis = includeLayoutBinding.budgetPerDayChart.xAxis + xAxis.valueFormatter = IndexAxisValueFormatter(days) + //xAxis.setCenterAxisLabels(true) + xAxis.position = XAxis.XAxisPosition.BOTTOM + xAxis.granularity = 1F + xAxis.isGranularityEnabled = true + + barData.barWidth = 0.3f + + includeLayoutBinding.budgetPerDayChart.apply { + xAxis.axisMinimum = 0f + animateY(600) + } + + includeLayoutBinding.budgetPerDayChart.isDragEnabled = true + includeLayoutBinding.budgetPerDayChart.setVisibleXRangeMaximum(3f) + + + includeLayoutBinding.budgetPerDayChart.description.text = "Per day expenses" + includeLayoutBinding.budgetPerDayChart.invalidate() + } + + private fun getMessage() { + + if (!arguments?.isEmpty!!) { + + val args = arguments?.let { + BudgetOverviewFragmentArgs.fromBundle(it) + } + + receivedBudgetKey = args?.budgetKeyMessage ?: "" + receivedMonth = args?.month ?: 0 + receivedYear = args?.year ?: 0 + + if (receivedBudgetKey.isNotValid() || receivedYear == 0) { + showToast(requireContext(), getString(R.string.something_went_wrong)) + requireActivity().onBackPressedDispatcher.onBackPressed() + } else { + + budgetViewModel.getBudgetByKeyWithACategoryAndExpenseDetails( + budgetKey = receivedBudgetKey, + month = receivedMonth, + year = receivedYear, + selectedPaymentMethods = emptyList() + ) + } + + } + } + + private fun observeBudget() { + + budgetViewModel.budgetByKey.observe(viewLifecycleOwner) { budget -> + + receivedBudget = budget + updateUI() + budgetAndIncomeGraphViewModel.getEveryDayExpenseData( + budget.expenseCategoryKey, receivedMonth, receivedYear + ) + } + } + + private fun updateUI() { + + if (!this::receivedBudget.isInitialized) { + showToast(requireContext(), getString(R.string.something_went_wrong)) + requireActivity().onBackPressedDispatcher.onBackPressed() + } else { + + binding.toolbar.title = receivedBudget.categoryName + + includeLayoutBinding.apply { + + + budgetCatNameTV.text = receivedBudget.categoryName + setImageToImageViewUsingGlide( + requireContext(), + budgetCategoryIV, + receivedBudget.categoryImageUrl, + {}, + {} + ) + + val numberOfDaysLeftInMonth = + WorkingWithDateAndTime.getNumberOfDaysLeftInAnyMonth( + receivedBudget.month, receivedBudget.year + ) + + var perDayExpense = if (numberOfDaysLeftInMonth != 0) { + (receivedBudget.budgetLimit - receivedBudget.currentExpenseAmount) / numberOfDaysLeftInMonth + } else { + 0.0 + } + + if (perDayExpense < 0) perDayExpense = 0.0 + + budgetPerDayTV.text = binding.root.context.getString( + R.string.budgetPerDay, perDayExpense.format(2), + numberOfDaysLeftInMonth.toString() + ) + + amountSpentBudgetTV.text = receivedBudget.currentExpenseAmount.format(2) + budgetLimitAmountTV.text = receivedBudget.budgetLimit.format(2) + + val amountLeft = receivedBudget.budgetLimit - receivedBudget.currentExpenseAmount + + when { + + amountLeft > 0 -> amountLeftBudgetTV.text = + getString(R.string.you_still_have_left_in_your_budget, amountLeft.format(2)) + + amountLeft == 0.0 -> amountLeftBudgetTV.text = getString(R.string.limit_reached) + + amountLeft < 0 -> getString(R.string.overspent) + + } + + val progressInPercent = + ((receivedBudget.currentExpenseAmount / receivedBudget.budgetLimit) * 100).toInt() + + budgetProgressBar.max = receivedBudget.budgetLimit.toInt() + + if (receivedBudget.currentExpenseAmount > receivedBudget.budgetLimit) { + budgetProgressBar.progress = receivedBudget.budgetLimit.toInt() + } else { + budgetProgressBar.progress = receivedBudget.currentExpenseAmount.toInt() + } + + changeUIRelatedToProgress(progressInPercent) + + } + } + } + + private fun changeUIRelatedToProgress(progressInPercent: Int) { + + // budget message + includeLayoutBinding.budgetMessageTV.text = getAppropriateBudgetSuggestionOrMessage( + requireContext(), + progressInPercent, + receivedBudget.currentExpenseAmount, + receivedBudget.budgetLimit + ) + + when { + + (progressInPercent in 0..35) -> { + val colorGreen = ContextCompat.getColor( + requireContext(), + R.color.color_green + ) + includeLayoutBinding.budgetProgressBar.progressTintList = ColorStateList.valueOf( + colorGreen + ) + + includeLayoutBinding.budgetMessageIV.setImageResource( + R.drawable.baseline_check_circle_green_24 + ) + + } + + (progressInPercent in 36..68) -> { + val colorYellow = ContextCompat.getColor( + requireContext(), + R.color.color_yellow + ) + includeLayoutBinding.budgetProgressBar.progressTintList = ColorStateList.valueOf( + colorYellow + ) + + includeLayoutBinding.budgetMessageIV.setImageResource( + R.drawable.baseline_check_circle_yellow_24 + ) + } + + else -> { + val colorRed = ContextCompat.getColor( + requireContext(), + R.color.color_Red + ) + includeLayoutBinding.budgetProgressBar.progressTintList = ColorStateList.valueOf( + colorRed + ) + includeLayoutBinding.budgetMessageIV.setImageResource( + R.drawable.baseline_check_circle_red_24 + ) + } + } + + if (progressInPercent >= 80) { + includeLayoutBinding.budgetLimitAmountTV.changeTextColor( + requireContext(), R.color.color_white + ) + } + } + + private fun initListeners() { + + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/ChooseMonthAndYearBottomSheetFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/ChooseMonthAndYearBottomSheetFragment.kt new file mode 100644 index 00000000..37a4444c --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/ChooseMonthAndYearBottomSheetFragment.kt @@ -0,0 +1,124 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome.SelectMonthAndYearAdapter +import com.rohitthebest.manageyourrenters.databinding.FragmentChooseMonthAndYearBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.utils.Functions.Companion.showToast +import com.rohitthebest.manageyourrenters.utils.convertJSONToStringList + +private const val TAG = "ChooseMonthAndYearBotto" + +class ChooseMonthAndYearBottomSheetFragment : + BottomSheetDialogFragment(R.layout.fragment_choose_month_and_year), + SelectMonthAndYearAdapter.OnClickListener { + + private var _binding: FragmentChooseMonthAndYearBinding? = null + private val binding get() = _binding!! + + private var mListener: OnBottomSheetDismissListener? = null + private lateinit var selectMonthAndYearAdapter: SelectMonthAndYearAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentChooseMonthAndYearBinding.bind(view) + + selectMonthAndYearAdapter = SelectMonthAndYearAdapter() + setUpRecyclerView() + + getMessage() + + binding.toolbar.setNavigationOnClickListener { + + if (mListener != null) { + + mListener!!.onMonthAndYearSelectedForCopyingBudget(false, "") + } + dismiss() + } + } + + private fun setUpRecyclerView() { + + binding.monthAndYearRV.apply { + + setHasFixedSize(true) + adapter = selectMonthAndYearAdapter + layoutManager = LinearLayoutManager(requireContext()) + } + + selectMonthAndYearAdapter.setOnClickListener(this) + } + + override fun onMonthAndYearClicked(monthAndYear: String) { + + if (mListener != null) { + mListener!!.onMonthAndYearSelectedForCopyingBudget( + true, + monthAndYear + ) + + dismiss() + } + } + + + private fun getMessage() { + + if (!arguments?.isEmpty!!) { + + arguments?.let { bundle -> + + try { + + val monthYearList = + convertJSONToStringList(bundle.getString(Constants.COPY_BUDGET_MONTH_AND_YEAR_KEY)) + + Log.d(TAG, "getMessage: monthYearString: $monthYearList") + + selectMonthAndYearAdapter.submitList(monthYearList) + + } catch (e: java.lang.Exception) { + e.printStackTrace() + requireContext().showToast(getString(R.string.something_went_wrong)) + dismiss() + } + } + } + + } + + companion object { + @JvmStatic + fun newInstance(bundle: Bundle): ChooseMonthAndYearBottomSheetFragment { + val fragment = ChooseMonthAndYearBottomSheetFragment() + fragment.arguments = bundle + return fragment + } + } + + interface OnBottomSheetDismissListener { + + fun onMonthAndYearSelectedForCopyingBudget( + isMonthAndYearSelected: Boolean, + selectedMonthYearString: String + ) + } + + fun setOnBottomSheetDismissListener(listener: OnBottomSheetDismissListener) { + + mListener = listener + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/IncomeFragment.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/IncomeFragment.kt new file mode 100644 index 00000000..593849bd --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/fragments/trackMoney/expense/budgetAndIncome/IncomeFragment.kt @@ -0,0 +1,357 @@ +package com.rohitthebest.manageyourrenters.ui.fragments.trackMoney.expense.budgetAndIncome + +import android.os.Bundle +import android.view.View +import android.widget.PopupMenu +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.adapters.trackMoneyAdapters.expenseAdapters.budgetAndIncome.IncomeRVAdapter +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.databinding.FragmentIncomeBinding +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.ui.fragments.MonthAndYearPickerDialog +import com.rohitthebest.manageyourrenters.ui.viewModels.BudgetViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.IncomeViewModel +import com.rohitthebest.manageyourrenters.ui.viewModels.PaymentMethodViewModel +import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.changeVisibilityOfFABOnScrolled +import com.rohitthebest.manageyourrenters.utils.format +import com.rohitthebest.manageyourrenters.utils.hide +import com.rohitthebest.manageyourrenters.utils.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.show +import com.rohitthebest.manageyourrenters.utils.showAlertDialogForDeletion +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val TAG = "IncomeFragment" + +@AndroidEntryPoint +class IncomeFragment : Fragment(R.layout.fragment_income), IncomeRVAdapter.OnClickListener, + MonthAndYearPickerDialog.OnMonthAndYearDialogDismissListener { + + private var _binding: FragmentIncomeBinding? = null + private val binding get() = _binding!! + + private val incomeViewModel by viewModels() + private val budgetViewModel by viewModels() + private val paymentMethodViewModel by viewModels() + + private var selectedMonth: Int = 0 + private var selectedYear: Int = 0 + private var monthList: List = emptyList() + + private lateinit var incomeRVAdapter: IncomeRVAdapter + + private var oldestYearWhenBudgetWasSaved = 2000 + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentIncomeBinding.bind(view) + + monthList = resources.getStringArray(R.array.months).toList() + getMessage() + initListener() + } + + private fun setUpRecyclerView() { + + binding.incomeRV.apply { + + setHasFixedSize(true) + layoutManager = LinearLayoutManager(requireContext()) + adapter = incomeRVAdapter + changeVisibilityOfFABOnScrolled(binding.addIncomeFAB) + } + + incomeRVAdapter.setOnClickListener(this) + } + + + override fun onItemClick(income: Income) { + // todo + } + + override fun onIncomeItemMenuBtnClicked(income: Income, view: View) { + + val popupMenu = PopupMenu(requireContext(), view) + popupMenu.menuInflater.inflate(R.menu.income_item_menu, popupMenu.menu) + + popupMenu.show() + + popupMenu.setOnMenuItemClickListener { + + return@setOnMenuItemClickListener when (it.itemId) { + + R.id.menu_income_edit -> { + + handleEditIncomeMenu(income) + true + } + + R.id.menu_delete_income -> { + + deleteIncome(income) + true + } + + else -> false + } + + } + } + + override fun onSyncBtnCLicked(income: Income, position: Int) { + + if (requireContext().isInternetAvailable()) { + + if (!income.isSynced) { + + incomeViewModel.insertIncome(income) + incomeRVAdapter.notifyItemChanged(position) + } + + } else { + Functions.showNoInternetMessage(requireContext()) + } + + } + + private fun deleteIncome(income: Income) { + + showAlertDialogForDeletion( + requireContext(), + { + incomeViewModel.deleteIncome(income) + it.dismiss() + }, + { + it.dismiss() + } + ) + } + + private fun handleEditIncomeMenu(income: Income) { + + showAddEditIncomeBottomSheetFragment(true, income.key) + } + + private fun initListener() { + + binding.toolbar.setNavigationOnClickListener { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + binding.previousMonthBtn.setOnClickListener { + handlePreviousDateButton() + } + + binding.nextMonthBtn.setOnClickListener { + handleNextDateButton() + } + + binding.addIncomeFAB.setOnClickListener { + + showAddEditIncomeBottomSheetFragment(false) + + } + + binding.monthMCV.setOnClickListener { + handleMonthAndYearSelection() + } + } + + private fun handleMonthAndYearSelection() { + + val bundle = Bundle() + bundle.putInt(Constants.MONTH_YEAR_PICKER_MONTH_KEY, selectedMonth) + bundle.putInt(Constants.MONTH_YEAR_PICKER_YEAR_KEY, selectedYear) + bundle.putInt( + Constants.MONTH_YEAR_PICKER_MIN_YEAR_KEY, + oldestYearWhenBudgetWasSaved - 4 + ) + bundle.putInt( + Constants.MONTH_YEAR_PICKER_MAX_YEAR_KEY, + WorkingWithDateAndTime.getCurrentYear() + ) + + requireActivity().supportFragmentManager.let { fm -> + MonthAndYearPickerDialog.newInstance( + bundle + ).apply { + show(fm, TAG) + } + }.setOnMonthAndYearDialogDismissListener(this) + } + + override fun onMonthAndYearDialogDismissed( + isMonthAndYearSelected: Boolean, + selectedMonth: Int, + selectedYear: Int + ) { + + if (isMonthAndYearSelected) { + this.selectedMonth = selectedMonth + this.selectedYear = selectedYear + handleUiAfterDateChange() + } + } + + + private fun showAddEditIncomeBottomSheetFragment(isForEdit: Boolean, incomeKey: String = "") { + + val bundle = Bundle() + + bundle.putBoolean(Constants.IS_FOR_EDIT, isForEdit) + bundle.putInt(Constants.INCOME_MONTH_KEY, selectedMonth) + bundle.putInt(Constants.INCOME_YEAR_KEY, selectedYear) + + if (isForEdit) { + + bundle.putString(Constants.DOCUMENT_KEY, incomeKey) + } + + requireActivity().supportFragmentManager.let { fragmentManager -> + + AddIncomeBottomSheetFragment.newInstance( + bundle + ).apply { + show(fragmentManager, TAG) + } + } + } + + private fun getMessage() { + + try { + + if (!arguments?.isEmpty!!) { + + val args = arguments?.let { + IncomeFragmentArgs.fromBundle(it) + } + + selectedMonth = args?.monthMessage ?: WorkingWithDateAndTime.getCurrentMonth() + selectedYear = args?.yearMessage ?: WorkingWithDateAndTime.getCurrentYear() + } else { + selectedMonth = WorkingWithDateAndTime.getCurrentMonth() + selectedYear = WorkingWithDateAndTime.getCurrentYear() + } + + lifecycleScope.launch { + delay(300) + initUI() + } + } catch (e: Exception) { + e.printStackTrace() + + selectedMonth = WorkingWithDateAndTime.getCurrentMonth() + selectedYear = WorkingWithDateAndTime.getCurrentYear() + lifecycleScope.launch { + delay(300) + initUI() + } + } + + } + + private fun initUI() { + + handleUiAfterDateChange() + + budgetViewModel.getTheOldestSavedBudgetYear().observe(viewLifecycleOwner) { year -> + try { + if (year != null) { + oldestYearWhenBudgetWasSaved = year + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun handlePreviousDateButton() { + + if (selectedMonth == 0) { + selectedYear -= 1 + } + + selectedMonth = WorkingWithDateAndTime.getPreviousMonth(selectedMonth) + + handleUiAfterDateChange() + } + + private fun handleNextDateButton() { + + if (selectedMonth == 11) { + selectedYear += 1 + } + + selectedMonth = WorkingWithDateAndTime.getNextMonth(selectedMonth) + handleUiAfterDateChange() + } + + private fun handleUiAfterDateChange() { + + binding.monthAndYearTV.text = + getString(R.string.month_and_year, monthList[selectedMonth], selectedYear.toString()) + + paymentMethodViewModel.getAllPaymentMethods() + .observe(viewLifecycleOwner) { paymentMethods -> + + incomeRVAdapter = IncomeRVAdapter( + linkedPaymentMethodsMap = paymentMethods.associate { it.key to it.paymentMethod }) + + setUpRecyclerView() + getAllIncomes() + } + + getTotalIncome() + } + + private fun getTotalIncome() { + + incomeViewModel.getTotalIncomeAddedByMonthAndYear( + selectedMonth, selectedYear + ).observe(viewLifecycleOwner) { total -> + + try { + + binding.toolbar.subtitle = getString(R.string.total_with_arg, total.format(2)) + } catch (e: NullPointerException) { + + binding.toolbar.subtitle = + getString(R.string.total_with_arg, getString(R.string._0_0)) + } + } + } + + private fun getAllIncomes() { + + incomeViewModel.getAllIncomesByMonthAndYear(selectedMonth, selectedYear) + .observe(viewLifecycleOwner) { incomes -> + + if (incomes.isNotEmpty()) { + + binding.incomeRV.show() + binding.noIncomeAddedTV.hide() + } else { + binding.incomeRV.hide() + binding.noIncomeAddedTV.show() + } + + incomeRVAdapter.submitList(incomes) + getTotalIncome() + } + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/BudgetAndIncomeGraphViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/BudgetAndIncomeGraphViewModel.kt new file mode 100644 index 00000000..d539253f --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/BudgetAndIncomeGraphViewModel.kt @@ -0,0 +1,270 @@ +package com.rohitthebest.manageyourrenters.ui.viewModels + + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.github.mikephil.charting.data.BarEntry +import com.rohitthebest.manageyourrenters.data.MonthAndTotalSum +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants +import com.rohitthebest.manageyourrenters.repositories.BudgetRepository +import com.rohitthebest.manageyourrenters.repositories.ExpenseRepository +import com.rohitthebest.manageyourrenters.repositories.IncomeRepository +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val TAG = "BudgetAndIncomeGraphViewModel" + +@HiltViewModel +class BudgetAndIncomeGraphViewModel @Inject constructor( + app: Application, + private val expenseRepository: ExpenseRepository, + private val budgetRepository: BudgetRepository, + private val incomeRepository: IncomeRepository +) : AndroidViewModel(app) { + + private var _incomeBudgetAndExpenseBarEntryData = + MutableLiveData>>( + emptyMap() + ) + + val incomeBudgetAndExpenseBarEntryData: LiveData>> + get() = _incomeBudgetAndExpenseBarEntryData + + fun getBarEntryDataForIncomeBudgetAndExpenseByYear( + year: Int, + paymentMethodKeys: List = emptyList() + ) { + + viewModelScope.launch { + + // budget + val budgetTotals = budgetRepository.getAllTotalBudgetByYear(year).first() + val budgetBarEntry = getBudgetBarEntry(budgetTotals) + + // income + val incomeTotals = if (paymentMethodKeys.isNotEmpty()) { + incomeTotalAfterApplyingPaymentMethodFilter( + year, paymentMethodKeys + ) + } else { + incomeRepository.getAllTotalIncomeByYear(year).first() + } + + Log.d( + TAG, + "getBarEntryDataForIncomeBudgetAndExpenseByYear: incomeTotals: $incomeTotals" + ) + + val incomeBarEntry = getIncomeBarEntry(incomeTotals) + + // expense + val monthAndTotalExpenseList: ArrayList = ArrayList() + for (i in 0..11) { + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + i, year + ) + + val categoryKeys = + budgetRepository.getExpenseCategoryKeysOfAllBudgetsByMonthAndYear( + i, year + ).first() + + + val expenseTotal: Double = try { + + if (paymentMethodKeys.isNotEmpty()) { + getExpenseTotalAfterApplyingPaymentMethodFilter( + datePairForExpense, categoryKeys, paymentMethodKeys + ) + } else { + expenseRepository.getTotalExpenseByCategoryKeysAndDateRange( + categoryKeys, + datePairForExpense.first, + datePairForExpense.second + Constants.ONE_DAY_MILLISECONDS + ).first() + } + } catch (e: NullPointerException) { + 0.0 + } + + monthAndTotalExpenseList.add(MonthAndTotalSum(i, expenseTotal)) + } + + val expenseBarEntry = getExpenseBarEntry(monthAndTotalExpenseList) + + val consolidatedBarEntries: HashMap> = HashMap() + + consolidatedBarEntries[FirestoreCollectionsConstants.BUDGETS] = budgetBarEntry + consolidatedBarEntries[FirestoreCollectionsConstants.INCOMES] = incomeBarEntry + consolidatedBarEntries[FirestoreCollectionsConstants.EXPENSES] = expenseBarEntry + + Log.d( + TAG, + "getBarEntryDataForIncomeBudgetAndExpenseByYear: ${ + consolidatedBarEntries[FirestoreCollectionsConstants.EXPENSES] + }" + ) + + _incomeBudgetAndExpenseBarEntryData.value = consolidatedBarEntries + } + } + + private fun getBudgetBarEntry(budgetTotals: List): ArrayList { + + val budgetLimitList = ArrayList() + val monthAndBudgetSumMap = budgetTotals.associate { it.month to it.total } + + for (i in 1..12) { + + if (monthAndBudgetSumMap.contains(i - 1)) { + budgetLimitList.add(BarEntry(i.toFloat(), monthAndBudgetSumMap[i - 1]!!.toFloat())) + } else { + budgetLimitList.add(BarEntry(i.toFloat(), 0f)) + } + } + + return budgetLimitList + } + + private fun getIncomeBarEntry(incomeTotals: List): ArrayList { + + val incomeList = ArrayList() + val monthAndIncomeSumMap = incomeTotals.associate { it.month to it.total } + + for (i in 1..12) { + + if (monthAndIncomeSumMap.contains(i - 1)) { + incomeList.add(BarEntry(i.toFloat(), monthAndIncomeSumMap[i - 1]!!.toFloat())) + } else { + incomeList.add(BarEntry(i.toFloat(), 0f)) + } + } + + return incomeList + } + + private fun getExpenseBarEntry(expenseTotals: List): ArrayList { + + val expenseList = ArrayList() + val monthAndIncomeSumMap = expenseTotals.associate { it.month to it.total } + + for (i in 1..12) { + + if (monthAndIncomeSumMap.contains(i - 1)) { + expenseList.add(BarEntry(i.toFloat(), monthAndIncomeSumMap[i - 1]!!.toFloat())) + } else { + expenseList.add(BarEntry(i.toFloat(), 0f)) + } + } + + return expenseList + } + + private suspend fun incomeTotalAfterApplyingPaymentMethodFilter( + year: Int, + paymentMethods: List + ): List { + + val incomeTotals: ArrayList = ArrayList() + + for (month in 0..11) { + + val incomes = incomeRepository.getAllIncomesByMonthAndYear(month, year).first() + + val total: Double = try { + if (incomes.isEmpty()) { + 0.0 + } else { + val tempIncome = + incomeRepository.applyFilterByPaymentMethods(paymentMethods, incomes) + + tempIncome.sumOf { it.income } + } + } catch (e: NullPointerException) { + e.printStackTrace() + 0.0 + } + + incomeTotals.add(MonthAndTotalSum(month, total)) + } + + return incomeTotals + } + + private suspend fun getExpenseTotalAfterApplyingPaymentMethodFilter( + datePairForExpense: Pair, + categoryKeys: List, + paymentMethodKeys: List + ): Double { + + val expenses = expenseRepository.getExpenseByCategoryKeysAndDateRange( + categoryKeys, + datePairForExpense.first, + datePairForExpense.second + Constants.ONE_DAY_MILLISECONDS + ).first() + + return try { + if (expenses.isEmpty()) { + 0.0 + } else { + val tempExpense = expenseRepository.applyExpenseFilterByPaymentMethods( + paymentMethodKeys, + expenses + ) + + tempExpense.sumOf { it.amount } + } + } catch (e: Exception) { + 0.0 + } + + } + + private var _budgetExpense = MutableLiveData>() + val budgetExpense: LiveData> get() = _budgetExpense + + fun getEveryDayExpenseData(categoryKey: String, month: Int, year: Int) { + + viewModelScope.launch { + + val startingMillisOfDays = + WorkingWithDateAndTime.getStartMillisecondOfAllDaysInMonth(month, year) + + val dataList = ArrayList() + + for (i in 1..startingMillisOfDays.size) { + + val dayMillis = startingMillisOfDays[i - 1] + + Log.d( + TAG, + "getEveryDayExpenseData: $i ----> ${dayMillis.first} ,,,,, ${dayMillis.second}" + ) + + val expenseAmount = try { + expenseRepository.getTotalExpenseAmountByCategoryKeyAndDateRange( + categoryKey, dayMillis.first, dayMillis.second + ).first() + } catch (e: java.lang.NullPointerException) { + 0.0 + } + + val barEntry = BarEntry(i.toFloat(), expenseAmount.toFloat()) + dataList.add(barEntry) + } + + _budgetExpense.value = dataList + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/BudgetViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/BudgetViewModel.kt new file mode 100644 index 00000000..dba389ee --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/BudgetViewModel.kt @@ -0,0 +1,427 @@ +package com.rohitthebest.manageyourrenters.ui.viewModels + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.rohitthebest.manageyourrenters.database.model.Budget +import com.rohitthebest.manageyourrenters.database.model.Expense +import com.rohitthebest.manageyourrenters.database.model.ExpenseCategory +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BUDGETS +import com.rohitthebest.manageyourrenters.repositories.BudgetRepository +import com.rohitthebest.manageyourrenters.repositories.ExpenseCategoryRepository +import com.rohitthebest.manageyourrenters.repositories.ExpenseRepository +import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import com.rohitthebest.manageyourrenters.utils.compareObjects +import com.rohitthebest.manageyourrenters.utils.convertStringListToJSON +import com.rohitthebest.manageyourrenters.utils.deleteAllDocumentsUsingKeyFromFirestore +import com.rohitthebest.manageyourrenters.utils.deleteDocumentFromFireStore +import com.rohitthebest.manageyourrenters.utils.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.updateDocumentOnFireStore +import com.rohitthebest.manageyourrenters.utils.uploadDocumentToFireStore +import com.rohitthebest.manageyourrenters.utils.uploadListOfDataToFireStore +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val TAG = "BudgetViewModel" + +@HiltViewModel +class BudgetViewModel @Inject constructor( + app: Application, + private val budgetRepository: BudgetRepository, + private val expenseCategoryRepository: ExpenseCategoryRepository, + private val expenseRepository: ExpenseRepository +) : AndroidViewModel(app) { + + fun insertBudget(budget: Budget) = viewModelScope.launch { + + val context = getApplication().applicationContext + + if (context.isInternetAvailable()) { + + budget.isSynced = true + + uploadDocumentToFireStore( + context, BUDGETS, budget.key + ) + } else { + + budget.isSynced = false + } + + budgetRepository.insertBudget(budget) + } + + fun updateBudget(oldValue: Budget, newValue: Budget) = viewModelScope.launch { + + val context = getApplication().applicationContext + + if (Functions.isInternetAvailable(context)) { + + newValue.isSynced = true + + if (!oldValue.isSynced) { + uploadDocumentToFireStore( + context, + BUDGETS, + newValue.key + ) + } else { + + val map = compareObjects( + oldData = oldValue, + newData = newValue, + notToCompareFields = listOf("modified") + ) + + Log.d(TAG, "updateBudget: difference: $map") + + if (map.isNotEmpty()) { + + map["modified"] = newValue.modified + + updateDocumentOnFireStore( + context, + map, + BUDGETS, + oldValue.key + ) + } + } + } else { + newValue.isSynced = false + } + + budgetRepository.updateBudget(newValue) + } + + fun deleteBudget(budget: Budget) = viewModelScope.launch { + + val context = getApplication().applicationContext + + deleteDocumentFromFireStore( + context, + BUDGETS, + budget.key + ) + + budgetRepository.deleteBudget(budget) + } + + fun deleteAllBudgets() = viewModelScope.launch { + budgetRepository.deleteAllBudgets() + } + + private val _allBudgetListByMonthAndYear = MutableLiveData>() + val allBudgetListByMonthAndYear: LiveData> get() = _allBudgetListByMonthAndYear + + fun getAllBudgetsByMonthAndYear( + month: Int = WorkingWithDateAndTime.getCurrentMonth(), + year: Int = WorkingWithDateAndTime.getCurrentYear(), + selectedPaymentMethods: List = emptyList() + ) { + + viewModelScope.launch { + + var allBudgets = + budgetRepository.getAllBudgetsByMonthAndYear(month, year).first() + + val allBudgetExpenseCategoryKeys = allBudgets.map { it.expenseCategoryKey } + var expenseCategories = + expenseCategoryRepository.getExpenseCategoriesByKey(allBudgetExpenseCategoryKeys) + .first() + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + month, year + ) + + allBudgets = allBudgets.sortedBy { it.expenseCategoryKey } + expenseCategories = expenseCategories.sortedBy { it.key } + + addOtherDetailsToBudget( + allBudgets, + expenseCategories, + datePairForExpense, + selectedPaymentMethods + ) + + _allBudgetListByMonthAndYear.value = allBudgets + } + } + + fun getAllExpenseCategoryOfBudgetsByMonthAndYear( + month: Int = WorkingWithDateAndTime.getCurrentMonth(), + year: Int = WorkingWithDateAndTime.getCurrentYear() + ) = budgetRepository.getExpenseCategoryKeysOfAllBudgetsByMonthAndYear(month, year).asLiveData() + + + private val _allExpenseCategoryAsBudgets = MutableLiveData>() + val allExpenseCategoryAsBudgets: LiveData> get() = _allExpenseCategoryAsBudgets + + fun getAllExpenseCategoryAsBudget(month: Int, year: Int) { + + viewModelScope.launch { + + // get all expense categories + val allExpenseCategories = + ArrayList(expenseCategoryRepository.getAllExpenseCategories().first()) + + // get all budgets for the selected month and year + val allBudgetsAlreadyAddedByMonthAndYear = + budgetRepository.getAllBudgetsByMonthAndYear(month, year).first() + + + // get all the expense category key for which budget is added + val budgetCategoryKeys = + allBudgetsAlreadyAddedByMonthAndYear.map { it.expenseCategoryKey } + + val categoriesWithBudgetAlreadyAdded = allExpenseCategories.filter { + budgetCategoryKeys.contains(it.key) + } + + // remove all the expense categories for which budget is already added + allExpenseCategories.removeAll(categoriesWithBudgetAlreadyAdded.toSet()) + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + month, year + ) + + addOtherDetailsToBudget( + allBudgetsAlreadyAddedByMonthAndYear.sortedBy { it.expenseCategoryKey }, + categoriesWithBudgetAlreadyAdded.sortedBy { it.key }, + datePairForExpense + ) + + val expenseCategoryAsBudget: ArrayList = + ArrayList(allBudgetsAlreadyAddedByMonthAndYear) + + allExpenseCategories.forEach { category -> + + val budget = Budget() + budget.apply { + this.currentExpenseAmount = 0.0 + this.budgetLimit = 0.0 + this.month = month + this.year = year + this.expenseCategoryKey = category.key + this.monthYearString = this.generateMonthYearString() + this.categoryName = category.categoryName + this.categoryImageUrl = category.imageUrl ?: "" + this.modified = System.currentTimeMillis() + } + expenseCategoryAsBudget.add(budget) + } + + _allExpenseCategoryAsBudgets.value = + expenseCategoryAsBudget.sortedBy { it.modified } + } + } + + private suspend fun addOtherDetailsToBudget( + allBudgets: List, + expenseCategories: List, + datePairForExpense: Pair, + selectedPaymentMethods: List = emptyList() + ) { + + allBudgets.forEachIndexed { index, budget -> + + budget.categoryName = expenseCategories[index].categoryName + budget.categoryImageUrl = expenseCategories[index].imageUrl ?: "" + + val currentExpense = calculateCurrentExpense( + datePairForExpense, + expenseCategories[index].key, + selectedPaymentMethods + ) + + Log.d( + TAG, + "addOtherDetailsToBudget: currentExpense (${budget.categoryName}): $currentExpense" + ) + + budget.currentExpenseAmount = currentExpense + } + + } + + private fun applyExpenseFilterByPaymentMethods( + paymentMethodKeys: List, + expenses: List + ): List { + + return expenseRepository.applyExpenseFilterByPaymentMethods(paymentMethodKeys, expenses) + } + + + fun getTheOldestSavedBudgetYear() = budgetRepository.getTheOldestSavedBudgetYear().asLiveData() + + fun getBudgetByKey(budgetKey: String) = budgetRepository.getBudgetByKey(budgetKey).asLiveData() + + private var _budgetByKey = MutableLiveData() + val budgetByKey: LiveData get() = _budgetByKey + + fun getBudgetByKeyWithACategoryAndExpenseDetails( + budgetKey: String, + month: Int, + year: Int, + selectedPaymentMethods: List = emptyList() + ) { + + viewModelScope.launch { + + val budget = budgetRepository.getBudgetByKey(budgetKey).first() + val category = + expenseCategoryRepository.getExpenseCategoryByKey(budget.expenseCategoryKey).first() + + budget.categoryName = category.categoryName + budget.categoryImageUrl = category.imageUrl ?: "" + + val datePairForExpense = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + month, year + ) + + val currentExpense: Double = calculateCurrentExpense( + datePairForExpense, + category.key, + selectedPaymentMethods + ) + + budget.currentExpenseAmount = currentExpense + + _budgetByKey.value = budget + } + } + + private suspend fun calculateCurrentExpense( + datePairForExpense: Pair, + categoryKey: String, + selectedPaymentMethods: List + ): Double { + + return try { + if (selectedPaymentMethods.isEmpty()) { + expenseRepository.getTotalExpenseAmountByCategoryKeyAndDateRange( + categoryKey, + datePairForExpense.first, + datePairForExpense.second + Constants.ONE_DAY_MILLISECONDS + ).first() + } else { + val tempExpense = expenseRepository.getExpenseByDateRangeAndExpenseCategoryKey( + categoryKey, + datePairForExpense.first, + datePairForExpense.second + Constants.ONE_DAY_MILLISECONDS + ).first() + + val expenseAfterPaymentMethodFilter = applyExpenseFilterByPaymentMethods( + selectedPaymentMethods, + tempExpense + ) + expenseAfterPaymentMethodFilter.sumOf { it.amount } + } + } catch (e: Exception) { + 0.0 + } + + } + + + fun getTotalBudgetByMonthAndYear(month: Int, year: Int) = + budgetRepository.getTotalBudgetByMonthAndYear(month, year).asLiveData() + + fun getAllBudgetMonthAndYearForWhichBudgetIsAdded() = + budgetRepository.getAllBudgetMonthAndYearForWhichBudgetIsAdded().asLiveData() + + fun isAnyBudgetAddedForThisMonthAndYear(monthYearString: String) = + budgetRepository.isAnyBudgetAddedForThisMonthAndYear(monthYearString).asLiveData() + + fun duplicateBudgetOfPreviouslyAddedBudgetMonth( + isBudgetAddedForSelectedMonth: Boolean, + selectedMonthAndYear: Pair, + monthAndYearFromWhichBudgetHasToBeDuplicated: String + ) { + + viewModelScope.launch { + + if (isBudgetAddedForSelectedMonth) { + + // delete all the budgets already added for this month + deleteAllBudgetsByMonthAndYear( + selectedMonthAndYear.first, + selectedMonthAndYear.second + ) + + delay(120) + } + + // getting all the budgets + val allBudgetsOfMonthFromWhichBudgetHasToBeDuplicated = + budgetRepository.getAllBudgetsByMonthAndYearString( + monthAndYearFromWhichBudgetHasToBeDuplicated + ).first() + + val duplicatedBudgetList = + allBudgetsOfMonthFromWhichBudgetHasToBeDuplicated.toMutableList() + + + duplicatedBudgetList.forEach { budget -> + + budget.key = Functions.generateKey("_${Functions.getUid()}") + budget.created = System.currentTimeMillis() + budget.modified = System.currentTimeMillis() + budget.month = selectedMonthAndYear.first + budget.year = selectedMonthAndYear.second + budget.monthYearString = budget.generateMonthYearString() + budget.isSynced = true + } + + insertAllBudget(duplicatedBudgetList) + } + } + + fun deleteAllBudgetsByMonthAndYear(month: Int, year: Int) { + + viewModelScope.launch { + + val context = getApplication().applicationContext + + val budgetKeys = budgetRepository.getKeysByMonthAndYear(month, year) + + if (budgetKeys.isNotEmpty()) { + + deleteAllDocumentsUsingKeyFromFirestore( + context, + BUDGETS, + convertStringListToJSON(budgetKeys) + ) + } + + budgetRepository.deleteBudgetsByMonthAndYear(month, year) + } + + } + + private fun insertAllBudget(budgets: List) = viewModelScope.launch { + + val context = getApplication().applicationContext + + uploadListOfDataToFireStore( + context, + BUDGETS, + convertStringListToJSON(budgets.map { it.key }) + ) + + budgetRepository.insertAllBudget(budgets) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseCategoryViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseCategoryViewModel.kt index 701c938f..67b81da7 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseCategoryViewModel.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseCategoryViewModel.kt @@ -4,20 +4,37 @@ import android.app.Application import android.content.Context import android.os.Parcelable import android.util.Log -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import com.rohitthebest.manageyourrenters.database.model.ExpenseCategory import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.BUDGETS import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EXPENSES import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EXPENSE_CATEGORIES +import com.rohitthebest.manageyourrenters.repositories.BudgetRepository import com.rohitthebest.manageyourrenters.repositories.ExpenseCategoryRepository import com.rohitthebest.manageyourrenters.repositories.ExpenseRepository +import com.rohitthebest.manageyourrenters.repositories.IncomeRepository import com.rohitthebest.manageyourrenters.repositories.MonthlyPaymentRepository -import com.rohitthebest.manageyourrenters.utils.* +import com.rohitthebest.manageyourrenters.utils.Functions import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.compareExpenseCategoryModel +import com.rohitthebest.manageyourrenters.utils.convertStringListToJSON +import com.rohitthebest.manageyourrenters.utils.deleteAllDocumentsUsingKeyFromFirestore +import com.rohitthebest.manageyourrenters.utils.deleteDocumentFromFireStore +import com.rohitthebest.manageyourrenters.utils.deleteFileFromFirebaseStorage +import com.rohitthebest.manageyourrenters.utils.isValid +import com.rohitthebest.manageyourrenters.utils.updateDocumentOnFireStore +import com.rohitthebest.manageyourrenters.utils.uploadDocumentToFireStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.collections.set private const val TAG = "ExpenseCategoryViewMode" @@ -26,6 +43,8 @@ class ExpenseCategoryViewModel @Inject constructor( app: Application, private val expenseCategoryRepository: ExpenseCategoryRepository, private val expenseRepository: ExpenseRepository, + private val budgetRepository: BudgetRepository, + private val incomeRepository: IncomeRepository, private val monthlyPaymentRepository: MonthlyPaymentRepository, private val state: SavedStateHandle ) : AndroidViewModel(app) { @@ -68,13 +87,13 @@ class ExpenseCategoryViewModel @Inject constructor( } else { - expenseCategory.isSynced = false - } + expenseCategory.isSynced = false + } - expenseCategoryRepository.insertExpenseCategory(expenseCategory) + expenseCategoryRepository.insertExpenseCategory(expenseCategory) - Functions.showToast(context, "Expense Category saved") - } + Functions.showToast(context, "Expense Category saved") + } fun updateExpenseCategory( oldValue: ExpenseCategory, @@ -140,6 +159,7 @@ class ExpenseCategoryViewModel @Inject constructor( } val expenseKeys = expenseRepository.getKeysByExpenseCategoryKey(expenseCategory.key) + val budgetKeys = budgetRepository.getKeysByExpenseCategoryKey(expenseCategory.key) unlinkMonthlyPaymentsIfAny(context, expenseKeys) @@ -152,9 +172,19 @@ class ExpenseCategoryViewModel @Inject constructor( ) } + if (budgetKeys.isNotEmpty()) { + + deleteAllDocumentsUsingKeyFromFirestore( + context, + BUDGETS, + convertStringListToJSON(budgetKeys) + ) + } + } expenseRepository.deleteExpenseByExpenseCategoryKey(expenseCategory.key) + budgetRepository.deleteBudgetsByExpenseCategoryKey(expenseCategory.key) expenseCategoryRepository.deleteExpenseCategory(expenseCategory) } @@ -202,6 +232,8 @@ class ExpenseCategoryViewModel @Inject constructor( expenseCategoryRepository.deleteAllExpenseCategories() expenseRepository.deleteAllExpenses() + budgetRepository.deleteAllBudgets() + incomeRepository.deleteAllIncomes() } fun getExpenseCategoryByKey(key: String) = @@ -209,4 +241,6 @@ class ExpenseCategoryViewModel @Inject constructor( fun getAllExpenseCategories() = expenseCategoryRepository.getAllExpenseCategories().asLiveData() + fun getAllExpenseCategoriesByLimit(limit: Int) = + expenseCategoryRepository.getAllExpenseCategoriesByLimit(limit).asLiveData() } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseGraphDataViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseGraphDataViewModel.kt new file mode 100644 index 00000000..588a0290 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseGraphDataViewModel.kt @@ -0,0 +1,300 @@ +package com.rohitthebest.manageyourrenters.ui.viewModels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.rohitthebest.manageyourrenters.R +import com.rohitthebest.manageyourrenters.data.ExpenseCategoryAndTheirTotalExpenseAmounts +import com.rohitthebest.manageyourrenters.others.Constants +import com.rohitthebest.manageyourrenters.repositories.ExpenseRepository +import com.rohitthebest.manageyourrenters.utils.WorkingWithDateAndTime +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import java.util.Calendar +import javax.inject.Inject + +@HiltViewModel +class ExpenseGraphDataViewModel @Inject constructor( + app: Application, + private val expenseRepository: ExpenseRepository +) : AndroidViewModel(app) { + + /* + This variable will be used for storing the data in Pair as + first: ExpenseCategoryAndTotalAmount + second: Total of all the totalAmounts + */ + private val _expenseGraphData = + MutableLiveData, Double>>() + val expenseGraphData: LiveData, Double>> get() = _expenseGraphData + + fun getTotalExpenseAmountsWithTheirExpenseCategoryNames(paymentMethodKeys: List = emptyList()) { + + viewModelScope.launch { + + val expenseCategoriesWithTheirAmounts = + try { + if (paymentMethodKeys.isEmpty()) { + expenseRepository.getTotalExpenseAmountsWithTheirExpenseCategoryKeys() + .first() + } else { + getExpenseCategoriesAndTheirAmountsAfterFilteringByPaymentMethods( + paymentMethodKeys + ) + } + } catch (e: Exception) { + emptyList() + } + + val totalAmount = if (expenseCategoriesWithTheirAmounts.isNotEmpty()) { + try { + if (paymentMethodKeys.isEmpty()) { + expenseRepository.getTotalExpenseAmount().first() + } else { + expenseCategoriesWithTheirAmounts.sumOf { it.totalAmount } + } + } catch (e: NullPointerException) { + 0.0 + } + } else { + 0.0 + } + + setValueForExpenseGraphDataPair(expenseCategoriesWithTheirAmounts, totalAmount) + } + } + + private suspend fun getExpenseCategoriesAndTheirAmountsAfterFilteringByPaymentMethods( + paymentMethodKeys: List + ): List { + + val expenses = expenseRepository.getAllExpenses().first() + + val filteredList = + expenseRepository.applyExpenseFilterByPaymentMethods(paymentMethodKeys, expenses) + + return expenseRepository.getTotalExpenseAmountsWithTheirExpenseCategoryKeysByListOfExpenseKeys( + filteredList.map { expense -> expense.key } + ).first() + } + + fun getTotalExpenseAmountsWithTheirExpenseCategoryNamesByDateRange( + date1: Long, + date2: Long, + paymentMethodKeys: List = emptyList() + ) { + + viewModelScope.launch { + val expenseCategoriesWithTheirAmounts = + try { + if (paymentMethodKeys.isEmpty()) { + expenseRepository.getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRange( + date1, + date2 + ).first() + } else { + getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRangeAndFilterByPaymentMethods( + date1, + date2, + paymentMethodKeys + ) + } + } catch (e: Exception) { + emptyList() + } + + val totalAmount = if (expenseCategoriesWithTheirAmounts.isNotEmpty()) { + try { + if (paymentMethodKeys.isEmpty()) { + expenseRepository.getTotalExpenseAmountByDateRange( + date1, + date2 + ).first() + } else { + expenseCategoriesWithTheirAmounts.sumOf { it.totalAmount } + } + } catch (e: java.lang.NullPointerException) { + 0.0 + } + } else { + 0.0 + } + + setValueForExpenseGraphDataPair(expenseCategoriesWithTheirAmounts, totalAmount) + } + } + + private suspend fun getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRangeAndFilterByPaymentMethods( + date1: Long, + date2: Long, + paymentMethodKeys: List + ): List { + + val expenses = expenseRepository.getExpensesByDateRange(date1, date2).first() + + val filteredList = + expenseRepository.applyExpenseFilterByPaymentMethods(paymentMethodKeys, expenses) + + return expenseRepository.getTotalExpenseAmountsWithTheirExpenseCategoryKeysByDateRangeAndByListOfExpenseKeys( + date1, date2, filteredList.map { expense -> expense.key } + ).first() + } + + fun getTotalExpenseAmountWithTheirExpenseCategoryNamesForSelectedExpenseCategories( + selectedExpenseCategoryKeys: List + ) { + + viewModelScope.launch { + + val expenseCategoriesWithTheirAmounts = + try { + expenseRepository.getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategories( + selectedExpenseCategoryKeys + ).first() + } catch (e: Exception) { + emptyList() + } + + val totalAmount = + try { + expenseRepository.getTotalExpenseByCategoryKeys(selectedExpenseCategoryKeys) + .first() + } catch (e: java.lang.NullPointerException) { + 0.0 + } + + setValueForExpenseGraphDataPair(expenseCategoriesWithTheirAmounts, totalAmount) + } + } + + fun getTotalExpenseAmountWithTheirExpenseCategoryNamesForSelectedExpenseCategoriesByDateRange( + selectedExpenseCategoryKeys: List, + date1: Long, + date2: Long + ) { + + viewModelScope.launch { + + val expenseCategoriesWithTheirAmounts = + try { + expenseRepository.getTotalExpenseAmountsWithTheirExpenseCategoryKeysForSelectedExpenseCategoriesByDateRange( + selectedExpenseCategoryKeys, date1, date2 + ).first() + } catch (e: Exception) { + emptyList() + } + + val totalAmount = + try { + expenseRepository.getTotalExpenseByCategoryKeysAndDateRange( + selectedExpenseCategoryKeys, + date1, + date2 + ).first() + } catch (e: NullPointerException) { + 0.0 + } + + setValueForExpenseGraphDataPair(expenseCategoriesWithTheirAmounts, totalAmount) + } + } + + + private fun setValueForExpenseGraphDataPair( + expenseCategoriesWithTheirAmounts: List, + totalAmount: Double + ) { + + val expenseGraphDataPair: Pair, Double> = + Pair(expenseCategoriesWithTheirAmounts, totalAmount) + + _expenseGraphData.value = expenseGraphDataPair + } + + private val _expenseOfEachMonth = MutableLiveData>>() + val expenseOfEachMonth: LiveData>> get() = _expenseOfEachMonth + + fun getExpensesOfAllMonthsOfYear(year: Int) { + + val context = getApplication().applicationContext + + viewModelScope.launch { + + val listOfExpensesInEachMonth = ArrayList>() + val monthList = context.resources.getStringArray(R.array.months_short).asList() + + for (i in 1..12) { + + val calendar = Calendar.getInstance() + calendar.set(year, i - 1, 2) + + val startAndEndDateInMillis = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonth( + calendar.timeInMillis + ) + + val amount = try { + expenseRepository.getTotalExpenseAmountByDateRange( + startAndEndDateInMillis.first, + startAndEndDateInMillis.second + Constants.ONE_DAY_MILLISECONDS + ).first() + + } catch (e: NullPointerException) { + e.printStackTrace() + 0.0 + } + + listOfExpensesInEachMonth.add(Pair(monthList[i - 1], amount)) + } + + _expenseOfEachMonth.value = listOfExpensesInEachMonth + } + + } + + fun getExpensesOfAllMonthsOfYearForSelectedCategory( + year: Int, + selectedExpenseCategoryKey: String + ) { + + val context = getApplication().applicationContext + + viewModelScope.launch { + + val listOfExpensesInEachMonth = ArrayList>() + val monthList = context.resources.getStringArray(R.array.months_short).asList() + + for (i in 1..12) { + + val calendar = Calendar.getInstance() + calendar.set(year, i - 1, 2) + + val startAndEndDateInMillis = + WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonth( + calendar.timeInMillis + ) + + val amount = try { + expenseRepository.getTotalExpenseAmountByCategoryKeyAndDateRange( + selectedExpenseCategoryKey, + startAndEndDateInMillis.first, + startAndEndDateInMillis.second + Constants.ONE_DAY_MILLISECONDS + ).first() + + } catch (e: NullPointerException) { + e.printStackTrace() + 0.0 + } + + listOfExpensesInEachMonth.add(Pair(monthList[i - 1], amount)) + } + + _expenseOfEachMonth.value = listOfExpensesInEachMonth + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseViewModel.kt index c449c0d0..aa44c731 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseViewModel.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/ExpenseViewModel.kt @@ -3,22 +3,34 @@ package com.rohitthebest.manageyourrenters.ui.viewModels import android.app.Application import android.content.Context import android.util.Log -import androidx.lifecycle.* -import com.rohitthebest.manageyourrenters.data.filter.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.rohitthebest.manageyourrenters.data.filter.ExpenseFilterDto +import com.rohitthebest.manageyourrenters.data.filter.IntFilterOptions +import com.rohitthebest.manageyourrenters.data.filter.SortFilter +import com.rohitthebest.manageyourrenters.data.filter.SortOrder +import com.rohitthebest.manageyourrenters.data.filter.StringFilterOptions import com.rohitthebest.manageyourrenters.database.model.Expense import com.rohitthebest.manageyourrenters.others.Constants import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.EXPENSES import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.MONTHLY_PAYMENTS import com.rohitthebest.manageyourrenters.repositories.ExpenseRepository import com.rohitthebest.manageyourrenters.repositories.MonthlyPaymentRepository -import com.rohitthebest.manageyourrenters.utils.* +import com.rohitthebest.manageyourrenters.utils.Functions import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.compareExpenseModel +import com.rohitthebest.manageyourrenters.utils.deleteDocumentFromFireStore +import com.rohitthebest.manageyourrenters.utils.updateDocumentOnFireStore +import com.rohitthebest.manageyourrenters.utils.uploadDocumentToFireStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import java.util.* import java.util.regex.Pattern import javax.inject.Inject +import kotlin.collections.set private const val TAG = "ExpenseViewModel" @@ -233,52 +245,6 @@ class ExpenseViewModel @Inject constructor( fun getExpensesByExpenseCategoryKey(expenseCategoryKey: String) = expenseRepository.getExpensesByExpenseCategoryKey(expenseCategoryKey).asLiveData() - private val _expenseOfEachMonth = MutableLiveData>(emptyList()) - val expenseOfEachMonth: LiveData> get() = _expenseOfEachMonth - - fun getExpensesOfAllMonthsOfYear(year: Int) { - - viewModelScope.launch { - - val calendars = ArrayList() - - val listOfExpensesInEachMonth = ArrayList() - - for (i in 1..12) { - - calendars.add(Calendar.getInstance()) - calendars[i - 1].set(year, i - 1, 2) - - val startAndEndDateInMillis = - WorkingWithDateAndTime.getMillisecondsOfStartAndEndDayOfMonth( - calendars[i - 1].timeInMillis - ) - - val amount = try { - expenseRepository.getTotalExpenseAmountByDateRange( - startAndEndDateInMillis.first, - startAndEndDateInMillis.second + Constants.ONE_DAY_MILLISECONDS - ).first() - - } catch (e: NullPointerException) { - e.printStackTrace() - 0.0 - } - - listOfExpensesInEachMonth.add(amount) - - Log.d( - TAG, - "getExpensesOfAllMonthsOfYear: Year : $year, Month : ${ - calendars[i - 1].get(Calendar.MONTH) - }, Amount : $amount" - ) - } - - _expenseOfEachMonth.value = listOfExpensesInEachMonth - } - - } // issue #78 private val _expensesByPaymentMethods = MutableLiveData>(emptyList()) @@ -390,25 +356,30 @@ class ExpenseViewModel @Inject constructor( return mExpenses } - private fun applyFilterByPaymentMethods( - paymentMethodKeys: List, + fun applyFilterByPaymentMethods( + paymentMethodKeys: List, expenses: List ): List { - val isOtherPaymentMethodKeyPresent = - paymentMethodKeys.contains(Constants.PAYMENT_METHOD_OTHER_KEY) - - val resultExpenses = expenses.filter { expense -> + return expenseRepository.applyExpenseFilterByPaymentMethods(paymentMethodKeys, expenses) + } - if (isOtherPaymentMethodKeyPresent) { - // for other payment method, get all the expenses where payment methods is null as well as payment method is other - expense.paymentMethods == null || expense.paymentMethods!!.any { it in paymentMethodKeys } - } else { - expense.paymentMethods != null && expense.paymentMethods!!.any { it in paymentMethodKeys } - } - } + fun getTotalExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys: List, + date1: Long, + date2: Long + ) = expenseRepository.getTotalExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys, + date1, + date2 + ).asLiveData() - return resultExpenses - } + fun getExpenseByCategoryKeysAndDateRange( + expenseCategoryKeys: List, + date1: Long, + date2: Long + ) = expenseRepository.getExpenseByCategoryKeysAndDateRange(expenseCategoryKeys, date1, date2) + .asLiveData() + fun isAnyExpenseAdded() = expenseRepository.isAnyExpenseAdded().asLiveData() } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/IncomeViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/IncomeViewModel.kt new file mode 100644 index 00000000..9f589da9 --- /dev/null +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/IncomeViewModel.kt @@ -0,0 +1,128 @@ +package com.rohitthebest.manageyourrenters.ui.viewModels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope +import com.rohitthebest.manageyourrenters.database.model.Income +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants +import com.rohitthebest.manageyourrenters.repositories.IncomeRepository +import com.rohitthebest.manageyourrenters.utils.Functions +import com.rohitthebest.manageyourrenters.utils.compareObjects +import com.rohitthebest.manageyourrenters.utils.deleteDocumentFromFireStore +import com.rohitthebest.manageyourrenters.utils.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.updateDocumentOnFireStore +import com.rohitthebest.manageyourrenters.utils.uploadDocumentToFireStore +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class IncomeViewModel @Inject constructor( + app: Application, + private val repository: IncomeRepository +) : AndroidViewModel(app) { + + fun insertIncome(income: Income) = viewModelScope.launch { + + val context = getApplication().applicationContext + + if (context.isInternetAvailable()) { + + income.isSynced = true + + uploadDocumentToFireStore( + context, FirestoreCollectionsConstants.INCOMES, income.key + ) + } else { + + income.isSynced = false + } + + repository.insertIncome(income) + } + + fun insertAllIncome(incomes: List) = viewModelScope.launch { + repository.insertAllIncome(incomes) + } + + fun updateIncome(oldValue: Income, newValue: Income) = viewModelScope.launch { + val context = getApplication().applicationContext + + if (Functions.isInternetAvailable(context)) { + + newValue.isSynced = true + + if (!oldValue.isSynced) { + uploadDocumentToFireStore( + context, + FirestoreCollectionsConstants.INCOMES, + newValue.key + ) + } else { + + val map = compareObjects( + oldData = oldValue, + newData = newValue, + notToCompareFields = listOf("modified", "isSynced") + ) + + if (map.isNotEmpty()) { + + map["modified"] = newValue.modified + + updateDocumentOnFireStore( + context, + map, + FirestoreCollectionsConstants.INCOMES, + oldValue.key + ) + } + } + } else { + newValue.isSynced = false + } + + repository.updateIncome(newValue) + } + + fun deleteIncome(income: Income) = viewModelScope.launch { + + val context = getApplication().applicationContext + + deleteDocumentFromFireStore( + context, + FirestoreCollectionsConstants.INCOMES, + income.key + ) + + repository.deleteIncome(income) + } + + fun deleteAllIncomes() = viewModelScope.launch { + repository.deleteAllIncomes() + } + + fun getAllIncomes() = repository.getAllIncomes().asLiveData() + + fun getAllIncomesByMonthAndYear(month: Int, year: Int) = repository.getAllIncomesByMonthAndYear( + month, year + ).asLiveData() + + fun getIncomeByKey(incomeKey: String) = + repository.getIncomeByKey(incomeKey = incomeKey).asLiveData() + + fun getTotalIncomeAddedByMonthAndYear(month: Int, year: Int) = + repository.getTotalIncomeAddedByMonthAndYear(month, year).asLiveData() + + fun getAllIncomeSources() = repository.getAllIncomeSources().asLiveData() + + fun applyFilterByPaymentMethods( + paymentMethodKeys: List, + incomes: List + ): List { + + return repository.applyFilterByPaymentMethods(paymentMethodKeys, incomes) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/RenterViewModel.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/RenterViewModel.kt index 1f5d8ccf..ccd05dc0 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/RenterViewModel.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/ui/viewModels/RenterViewModel.kt @@ -2,7 +2,12 @@ package com.rohitthebest.manageyourrenters.ui.viewModels import android.app.Application import android.os.Parcelable -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.asLiveData +import androidx.lifecycle.viewModelScope import com.rohitthebest.manageyourrenters.R import com.rohitthebest.manageyourrenters.data.DocumentType import com.rohitthebest.manageyourrenters.data.SupportingDocument @@ -15,13 +20,21 @@ import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants.R import com.rohitthebest.manageyourrenters.repositories.DeletedRenterRepository import com.rohitthebest.manageyourrenters.repositories.RenterPaymentRepository import com.rohitthebest.manageyourrenters.repositories.RenterRepository -import com.rohitthebest.manageyourrenters.utils.* import com.rohitthebest.manageyourrenters.utils.Functions.Companion.isInternetAvailable +import com.rohitthebest.manageyourrenters.utils.compareRenterModel +import com.rohitthebest.manageyourrenters.utils.convertStringListToJSON +import com.rohitthebest.manageyourrenters.utils.deleteAllDocumentsUsingKeyFromFirestore +import com.rohitthebest.manageyourrenters.utils.deleteDocumentFromFireStore +import com.rohitthebest.manageyourrenters.utils.deleteFileFromFirebaseStorage +import com.rohitthebest.manageyourrenters.utils.updateDocumentOnFireStore +import com.rohitthebest.manageyourrenters.utils.uploadDocumentToFireStore +import com.rohitthebest.manageyourrenters.utils.uploadFileToFirebaseCloudStorage import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.collections.set private const val TAG = "RenterViewModel" @@ -301,6 +314,9 @@ class RenterViewModel @Inject constructor( } } + val renterNameWithTheirDues: LiveData> = + repo.getRentersWithTheirDues().asLiveData() + fun getRentersWithTheirAmountPaidByDateCreated( startDate: Long, endDate: Long diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ExtensionFunctions.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ExtensionFunctions.kt index cf279755..6f5d7bf2 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ExtensionFunctions.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ExtensionFunctions.kt @@ -19,7 +19,11 @@ import android.text.TextWatcher import android.text.style.StrikethroughSpan import android.text.style.UnderlineSpan import android.view.View -import android.widget.* +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.Spinner +import android.widget.TextView import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView @@ -155,6 +159,28 @@ fun RecyclerView.changeVisibilityOfFABOnScrolled(fab: FloatingActionButton) { }) } +fun RecyclerView.changeVisibilityOfViewOnScrolled(view: View) { + + this.addOnScrollListener(object : RecyclerView.OnScrollListener() { + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + try { + if (dy > 0 && view.visibility == View.VISIBLE) { + + view.hide() + } else if (dy < 0 && view.visibility != View.VISIBLE) { + + view.show() + } + } catch (e: Exception) { + e.printStackTrace() + } + } + }) +} + fun Double.format(digits: Int) = "%.${digits}f".format(this) fun TextView.changeTextColor(context: Context, color: Int) { @@ -182,6 +208,8 @@ fun String?.isValid(): Boolean { && this.trim().isNotBlank() } +fun String?.isNotValid() = !this.isValid() + inline fun EditText.onTextChangedListener( crossinline onTextChanged: (s: CharSequence?) -> Unit ) { @@ -209,17 +237,18 @@ inline fun EditText.onTextChangedListener( inline fun showAlertDialogForDeletion( context: Context, crossinline positiveButtonListener: (DialogInterface) -> Unit, - crossinline negativeButtonListener: (DialogInterface) -> Unit + crossinline negativeButtonListener: (DialogInterface) -> Unit, + message: String = "" ) { MaterialAlertDialogBuilder(context) - .setTitle("Are you sure?") - .setMessage(context.getString(R.string.delete_warning_message)) - .setPositiveButton("Delete") { dialogInterface, _ -> + .setTitle(context.getString(R.string.are_you_sure)) + .setMessage(if (message.isNotValid()) context.getString(R.string.delete_warning_message) else message) + .setPositiveButton(context.getString(R.string._delete)) { dialogInterface, _ -> positiveButtonListener(dialogInterface) } - .setNegativeButton("Cancel") { dialogInterface, _ -> + .setNegativeButton(context.getText(R.string.cancel)) { dialogInterface, _ -> negativeButtonListener(dialogInterface) } @@ -432,7 +461,7 @@ fun Bitmap.saveToStorage(context: Context, fileName: String): Uri? { try { - this.compress(Bitmap.CompressFormat.JPEG, 100, fout) + this.compress(Bitmap.CompressFormat.JPEG, 100, fout!!) fout?.close() } catch (e: java.lang.Exception) { @@ -482,6 +511,7 @@ fun TextView.applyStyles(text: String, textStyle: String) { text.replace("-critical", "") ) } + text.startsWith("https") || text.startsWith("http") -> { this.changeTextStyle( diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/FirebaseServiceHelper.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/FirebaseServiceHelper.kt index 1ad6da5a..ef1658b0 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/FirebaseServiceHelper.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/FirebaseServiceHelper.kt @@ -11,8 +11,16 @@ import com.rohitthebest.manageyourrenters.others.Constants import com.rohitthebest.manageyourrenters.others.Constants.COLLECTION_KEY import com.rohitthebest.manageyourrenters.others.Constants.DELETE_FILE_FROM_FIREBASE_KEY import com.rohitthebest.manageyourrenters.others.Constants.DOCUMENT_KEY +import com.rohitthebest.manageyourrenters.others.Constants.KEY_LIST_KEY import com.rohitthebest.manageyourrenters.others.Constants.UPLOAD_DATA_KEY -import com.rohitthebest.manageyourrenters.services.* +import com.rohitthebest.manageyourrenters.others.FirestoreCollectionsConstants +import com.rohitthebest.manageyourrenters.services.DeleteAllDocumentsService +import com.rohitthebest.manageyourrenters.services.DeleteFileFromFirebaseStorageService +import com.rohitthebest.manageyourrenters.services.DeleteService +import com.rohitthebest.manageyourrenters.services.UpdateService +import com.rohitthebest.manageyourrenters.services.UploadDocumentListToFireStoreService +import com.rohitthebest.manageyourrenters.services.UploadFileToCloudStorageService +import com.rohitthebest.manageyourrenters.services.UploadService import kotlinx.coroutines.tasks.await import kotlin.random.Random @@ -63,7 +71,7 @@ fun updateDocumentOnFireStore( foregroundService.putExtra( Constants.UPDATE_DOCUMENT_MAP_KEY, - map + map.convertToJsonString() ) foregroundService.putExtra( @@ -124,8 +132,14 @@ fun uploadListOfDataToFireStore( uploadData ) - ContextCompat.startForegroundService(context, foregroundService) + if (collection != FirestoreCollectionsConstants.PARTIAL_PAYMENTS) { + foregroundService.putExtra( + KEY_LIST_KEY, + uploadData + ) + } + ContextCompat.startForegroundService(context, foregroundService) } fun deleteAllDocumentsUsingKeyFromFirestore( @@ -142,7 +156,7 @@ fun deleteAllDocumentsUsingKeyFromFirestore( ) foregroundService.putExtra( - Constants.KEY_LIST_KEY, + KEY_LIST_KEY, keyList ) diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/Functions.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/Functions.kt index ea4ac3e0..11ca94a5 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/Functions.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/Functions.kt @@ -68,7 +68,6 @@ class Functions { private val mAuth = Firebase.auth - private const val TAG = "Functions" fun showToast(context: Context, message: Any?, duration: Int = Toast.LENGTH_SHORT) { try { Log.d(TAG, message.toString()) @@ -688,16 +687,19 @@ class Functions { onMenuItemClicked(CustomDateRange.THIS_MONTH) true } + R.id.menu_date_range_this_week -> { onMenuItemClicked(CustomDateRange.THIS_WEEK) true } + R.id.menu_date_range_previous_month -> { onMenuItemClicked(CustomDateRange.PREVIOUS_MONTH) true } + R.id.menu_date_range_previous_week -> { onMenuItemClicked(CustomDateRange.PREVIOUS_WEEK) @@ -708,18 +710,22 @@ class Functions { onMenuItemClicked(CustomDateRange.LAST_30_DAYS) true } + R.id.menu_date_range_last_7_days -> { onMenuItemClicked(CustomDateRange.LAST_7_DAYS) true } + R.id.menu_date_range_last_365_days -> { onMenuItemClicked(CustomDateRange.LAST_365_DAYS) true } + R.id.menu_date_range_all_time -> { onMenuItemClicked(CustomDateRange.ALL_TIME) true } + R.id.menu_date_range_custom_range -> { onMenuItemClicked(CustomDateRange.CUSTOM_DATE_RANGE) true @@ -978,12 +984,51 @@ class Functions { ) } else { - PendingIntent.getActivity(context, 0, notificationIntent, 0) + PendingIntent.getActivity( + context, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ) } } } - } + fun getAppropriateBudgetSuggestionOrMessage( + context: Context, + progressInPercent: Int, + budgetExpense: Double, + budgetLimit: Double + ): String { + + val message: List; + + when { + + (progressInPercent in 0..35) -> { + message = + context.resources.getStringArray(R.array.budget_start_messages).toList() + } + + (progressInPercent in 36..68) -> { + message = + context.resources.getStringArray(R.array.budget_middle_messages).toList() + } + else -> { + message = if (budgetExpense > budgetLimit) { + context.resources.getStringArray(R.array.budget_overspent_messages).toList() + } else if (budgetExpense == budgetLimit) { + context.resources.getStringArray(R.array.budget_reachedLimit_messages) + .toList() + } else { + context.resources.getStringArray(R.array.budget_end_messages).toList() + } + } + } + + return message[Random.nextInt(0, message.size - 1)] + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ModelComparator.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ModelComparator.kt index a6139a37..09c51e35 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ModelComparator.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/ModelComparator.kt @@ -1,12 +1,58 @@ package com.rohitthebest.manageyourrenters.utils -import com.rohitthebest.manageyourrenters.database.model.* +import com.rohitthebest.manageyourrenters.database.model.BorrowerPayment +import com.rohitthebest.manageyourrenters.database.model.EMI +import com.rohitthebest.manageyourrenters.database.model.EMIPayment +import com.rohitthebest.manageyourrenters.database.model.Expense +import com.rohitthebest.manageyourrenters.database.model.ExpenseCategory +import com.rohitthebest.manageyourrenters.database.model.MonthlyPayment +import com.rohitthebest.manageyourrenters.database.model.MonthlyPaymentCategory +import com.rohitthebest.manageyourrenters.database.model.PaymentMethod +import com.rohitthebest.manageyourrenters.database.model.Renter +import com.rohitthebest.manageyourrenters.database.model.RenterPayment /** * This class helps to compare the main fields of * old and new model and return a map of unmatched fields */ +// generic +fun compareObjects( + oldData: T, + newData: T, + notToCompareFields: List +): HashMap { + + return try { + + val result = HashMap() + val fields = oldData!!::class.java.declaredFields + + for (field in fields) { + + field.isAccessible = true + + if (field.name != "key" + && field.name != "id" + && field.name != "uid" + && !notToCompareFields.contains(field.name) + ) { + + val oldValue = field.get(oldData) + val newValue = field.get(newData) + + if (oldValue != newValue) { + result[field.name] = newValue + } + } + } + + return result + } catch (e: Exception) { + HashMap() + } +} + fun compareBorrowerPaymentModel( oldData: BorrowerPayment, newData: BorrowerPayment @@ -221,7 +267,6 @@ fun comparePaymentMethod(oldData: PaymentMethod, newData: PaymentMethod): HashMa val map: HashMap = HashMap() - if (oldData.isSynced != newData.isSynced) map["synced"] = newData.isSynced if (oldData.paymentMethod != newData.paymentMethod) map["paymentMethod"] = newData.paymentMethod return map diff --git a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/WorkingWithDateAndTime.kt b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/WorkingWithDateAndTime.kt index 62150358..2a6bae84 100644 --- a/app/src/main/java/com/rohitthebest/manageyourrenters/utils/WorkingWithDateAndTime.kt +++ b/app/src/main/java/com/rohitthebest/manageyourrenters/utils/WorkingWithDateAndTime.kt @@ -5,7 +5,10 @@ import android.util.Log import java.sql.Timestamp import java.text.ParseException import java.text.SimpleDateFormat +import java.time.YearMonth import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern @SuppressLint("SimpleDateFormat") object WorkingWithDateAndTime { @@ -290,4 +293,156 @@ object WorkingWithDateAndTime { return Pair(firstCal.timeInMillis, lastCal.timeInMillis) } + fun getMonthAndYearString(month: Int, year: Int): String { + + return "${month}_${year}" + } + + fun extractMonthAndYearFromMonthAndYearString(monthYearString: String): Pair { + + return try { + + val pattern = "(\\d+)_(\\d+)" + val regex: Pattern = Pattern.compile(pattern) + val matcher: Matcher = regex.matcher(monthYearString) + + if (matcher.find()) { + + val month = matcher.group(1)?.toInt() + val year = matcher.group(2)?.toInt() + + return if (month != null && year != null) { + + Pair(month, year) + } else { + Pair(0, 0) + } + + } else { + return Pair(0, 0) + } + + } catch (e: Exception) { + e.printStackTrace() + Pair(0, 0) + } + + } + + + fun getNextMonth(selectedMonth: Int): Int { + + val cal = Calendar.getInstance() + cal.set(Calendar.MONTH, selectedMonth) + cal.add(Calendar.MONTH, 1) + return cal.get(Calendar.MONTH) + } + + fun getPreviousMonth(selectedMonth: Int): Int { + + val cal = Calendar.getInstance() + cal.set(Calendar.MONTH, selectedMonth) + cal.add(Calendar.MONTH, -1) + return cal.get(Calendar.MONTH) + } + + fun getMillisecondsOfStartAndEndDayOfMonthForGivenMonthAndYear( + month: Int, + year: Int + ): Pair { + + val calendar = Calendar.getInstance() + calendar.set(year, month, 5) + val dateInMillis = calendar.timeInMillis + return getMillisecondsOfStartAndEndDayOfMonth(dateInMillis) + } + + /** + * @param month - month (0-11) + * @param year - year (Ex: 2023) + * + * @return - Returns the number of days in a give month + */ + fun getNumberOfDaysInMonth(month: Int, year: Int): Int { + + val yearMonth = YearMonth.of(year, month + 1) + return yearMonth.lengthOfMonth() + } + + /** + * @param month - month (0-11) + * @param year - year (Ex: 2023) + * + * @return - Returns the number of days left in the month (Int) + */ + fun getNumberOfDaysLeftInAnyMonth(month: Int, year: Int): Int { + + val currentYear = getCurrentYear() + val currentMonth = getCurrentMonth() + val numberOfDaysInMonth = getNumberOfDaysInMonth(month, year) + + if (year < currentYear) return 0 + if (year > currentYear) return numberOfDaysInMonth + + if (month < currentMonth) return 0 + if (month > currentMonth) return numberOfDaysInMonth + + // year = current year and month = current month + val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + + return numberOfDaysInMonth - currentDay + } + + /** + * @param month - month (0-11) + * @param year - year (Ex: 2023) + * + * @return - Returns list of Pair object for which the + * first value is the start milliseconds of the day (example : if the date is 20-09-2023, then it's value will the milliseconds of (20-09-2023 00:00:00)), + * second value is the end milliseconds of the day (example : if the date is 20-09-2023, then it's value will the milliseconds of (20-09-2023 24:00:00)) + */ + fun getStartMillisecondOfAllDaysInMonth(month: Int, year: Int): List> { + + val numberOfDays = getNumberOfDaysInMonth(month, year) + + val startCalendar = Calendar.getInstance() + val endCalendar = Calendar.getInstance() + + val startingMillisOfDays = ArrayList>() + + for (i in 1..numberOfDays) { + + startCalendar.set(year, month, i, 0, 0) + endCalendar.set(year, month, i, 24, 0) + startingMillisOfDays.add(Pair(startCalendar.time.time, endCalendar.time.time)) + } + + return startingMillisOfDays + } + + /** + * @param month - month (0-11) + * @param year - year (Ex: 2023) + * + * @return - Returns the list of a pair object for which the first value is the int value of the DAY_OF_WEEK and the second value is String value i.e., human readable form + */ + fun getDayOfWeeksForEntireMonth(month: Int, year: Int): List> { + + val calendar = Calendar.getInstance() + val numberOfDays = getNumberOfDaysInMonth(month, year) + + val daysList = ArrayList>() + + for (i in 1..numberOfDays) { + + calendar.set(year, month, i) + val dayOfWeekInt = calendar.get(Calendar.DAY_OF_WEEK) + val dayOfWeekString = SimpleDateFormat("EEEE").format(calendar.time) + + daysList.add(Pair(dayOfWeekInt, dayOfWeekString)) + } + + return daysList; + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_check_24.xml b/app/src/main/res/drawable/baseline_check_24.xml new file mode 100644 index 00000000..5623ef04 --- /dev/null +++ b/app/src/main/res/drawable/baseline_check_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/baseline_check_circle_green_24.xml b/app/src/main/res/drawable/baseline_check_circle_green_24.xml new file mode 100644 index 00000000..a7ef1b65 --- /dev/null +++ b/app/src/main/res/drawable/baseline_check_circle_green_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_check_circle_red_24.xml b/app/src/main/res/drawable/baseline_check_circle_red_24.xml new file mode 100644 index 00000000..64a88dff --- /dev/null +++ b/app/src/main/res/drawable/baseline_check_circle_red_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_check_circle_yellow_24.xml b/app/src/main/res/drawable/baseline_check_circle_yellow_24.xml new file mode 100644 index 00000000..c2ddee04 --- /dev/null +++ b/app/src/main/res/drawable/baseline_check_circle_yellow_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/baseline_filter_list_colored_24.xml b/app/src/main/res/drawable/baseline_filter_list_colored_24.xml index b78e8dea..d635aa5c 100644 --- a/app/src/main/res/drawable/baseline_filter_list_colored_24.xml +++ b/app/src/main/res/drawable/baseline_filter_list_colored_24.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_progress_bar_2.xml b/app/src/main/res/drawable/custom_progress_bar_2.xml new file mode 100644 index 00000000..f021c766 --- /dev/null +++ b/app/src/main/res/drawable/custom_progress_bar_2.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_bar_chart_24_white.xml b/app/src/main/res/drawable/ic_baseline_bar_chart_24_white.xml new file mode 100644 index 00000000..136c1e6d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_bar_chart_24_white.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_keyboard_arrow_white_24.xml b/app/src/main/res/drawable/ic_baseline_keyboard_arrow_white_24.xml new file mode 100644 index 00000000..27e7c33a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_keyboard_arrow_white_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_more_vert_white_24.xml b/app/src/main/res/drawable/ic_baseline_more_vert_white_24.xml new file mode 100644 index 00000000..b29b2aef --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_more_vert_white_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/adapter_budget.xml b/app/src/main/res/layout/adapter_budget.xml new file mode 100644 index 00000000..f6df6fd3 --- /dev/null +++ b/app/src/main/res/layout/adapter_budget.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_income_list_item.xml b/app/src/main/res/layout/adapter_income_list_item.xml new file mode 100644 index 00000000..a6490c4e --- /dev/null +++ b/app/src/main/res/layout/adapter_income_list_item.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_select_month_and_year.xml b/app/src/main/res/layout/adapter_select_month_and_year.xml new file mode 100644 index 00000000..c9124383 --- /dev/null +++ b/app/src/main/res/layout/adapter_select_month_and_year.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_set_budget_expense_category.xml b/app/src/main/res/layout/adapter_set_budget_expense_category.xml new file mode 100644 index 00000000..4d73a622 --- /dev/null +++ b/app/src/main/res/layout/adapter_set_budget_expense_category.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + +