Compare commits

...

2 commits
main ... graphs

5 changed files with 141 additions and 46 deletions

View file

@ -54,6 +54,9 @@ dependencies {
implementation "com.google.dagger:dagger:$dagger" implementation "com.google.dagger:dagger:$dagger"
kapt "com.google.dagger:dagger-compiler:$dagger" kapt "com.google.dagger:dagger-compiler:$dagger"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

View file

@ -6,9 +6,16 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.github.mikephil.charting.components.AxisBase
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.ValueFormatter
import com.wbrawner.budget.AllowanceApplication import com.wbrawner.budget.AllowanceApplication
import com.wbrawner.budget.AsyncState import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.R import com.wbrawner.budget.R
@ -30,6 +37,24 @@ class OverviewFragment : Fragment() {
): View? = inflater.inflate(R.layout.fragment_overview, container, false) ): View? = inflater.inflate(R.layout.fragment_overview, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
expectedActualChart.apply {
setFitBars(true)
xAxis.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return if (value == 0f) "Income" else "Expenses"
}
override fun getBarLabel(barEntry: BarEntry?): String {
return super.getBarLabel(barEntry)
}
}
xAxis.position = XAxis.XAxisPosition.BOTTOM
xAxis.granularity = 1f
xAxis.isGranularityEnabled = true
setDrawGridBackground(false)
axisLeft.axisMinimum = 0f
axisRight.axisMinimum = 0f
}
viewModel.state.observe(viewLifecycleOwner, Observer { state -> viewModel.state.observe(viewLifecycleOwner, Observer { state ->
when (state) { when (state) {
is AsyncState.Loading -> { is AsyncState.Loading -> {
@ -43,6 +68,30 @@ class OverviewFragment : Fragment() {
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
activity?.title = state.data.budget.name activity?.title = state.data.budget.name
balance.text = state.data.balance.toAmountSpannable(view.context) balance.text = state.data.balance.toAmountSpannable(view.context)
val expectedIncome = (state.data.expectedIncome / 100).toFloat()
val expectedExpenses = (state.data.expectedExpenses / 100).toFloat()
val actualIncome = (state.data.actualIncome / 100).toFloat()
val actualExpenses = (state.data.actualExpenses / 100).toFloat()
val max = maxOf(expectedIncome, expectedExpenses, actualIncome, actualExpenses)
expectedActualChart.axisLeft.axisMaximum = max
expectedActualChart.axisRight.axisMaximum = max
expectedActualChart.data = BarData(
BarDataSet(
listOf(BarEntry(0f, expectedIncome), BarEntry(1f, expectedExpenses)),
"Expected"
).apply {
color = ResourcesCompat.getColor(resources, R.color.colorSecondary, requireContext().theme)
},
BarDataSet(
listOf(BarEntry(0f, actualIncome), BarEntry(1f, actualExpenses)),
"Actual"
).apply {
color = ResourcesCompat.getColor(resources, R.color.colorAccent, requireContext().theme)
}
).apply {
barWidth = 0.25f
}
expectedActualChart.groupBars(0f, 0.06f, 0.01f)
} }
is AsyncState.Error -> { is AsyncState.Error -> {
overviewContent.visibility = View.GONE overviewContent.visibility = View.GONE

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.*
import com.wbrawner.budget.AsyncState import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.common.budget.Budget import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.common.transaction.TransactionRepository import com.wbrawner.budget.common.transaction.TransactionRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -14,12 +15,16 @@ class OverviewViewModel : ViewModel() {
@Inject @Inject
lateinit var budgetRepo: BudgetRepository lateinit var budgetRepo: BudgetRepository
@Inject
lateinit var categoryRepo: CategoryRepository
@Inject @Inject
lateinit var transactionRepo: TransactionRepository lateinit var transactionRepo: TransactionRepository
fun loadOverview(lifecycleOwner: LifecycleOwner) { fun loadOverview(lifecycleOwner: LifecycleOwner) {
budgetRepo.currentBudget.observe(lifecycleOwner, Observer { budget -> budgetRepo.currentBudget.observe(lifecycleOwner, Observer { budget ->
if (budget == null) { val budgetId = budget?.id
if (budgetId == null) {
state.postValue(AsyncState.Error("Invalid Budget ID")) state.postValue(AsyncState.Error("Invalid Budget ID"))
return@Observer return@Observer
} }
@ -27,9 +32,28 @@ class OverviewViewModel : ViewModel() {
state.postValue(AsyncState.Loading) state.postValue(AsyncState.Loading)
try { try {
// TODO: Load expected and actual income/expense amounts as well // TODO: Load expected and actual income/expense amounts as well
var expectedExpenses = 0L
var expectedIncome = 0L
var actualExpenses = 0L
var actualIncome = 0L
categoryRepo.findAll(arrayOf(budgetId)).forEach { category ->
val categoryId = category.id ?: return@forEach
val balance = categoryRepo.getBalance(categoryId)
if (category.expense) {
expectedExpenses += category.amount
actualExpenses += (balance * -1)
} else {
expectedIncome += category.amount
actualIncome += balance
}
}
state.postValue(AsyncState.Success(OverviewState( state.postValue(AsyncState.Success(OverviewState(
budget, budget,
budgetRepo.getBalance(budget.id!!) budgetRepo.getBalance(budgetId),
expectedIncome,
expectedExpenses,
actualIncome,
actualExpenses
))) )))
} catch (e: Exception) { } catch (e: Exception) {
state.postValue(AsyncState.Error(e)) state.postValue(AsyncState.Error(e))
@ -41,5 +65,9 @@ class OverviewViewModel : ViewModel() {
data class OverviewState( data class OverviewState(
val budget: Budget, val budget: Budget,
val balance: Long val balance: Long,
val expectedIncome: Long,
val expectedExpenses: Long,
val actualIncome: Long,
val actualExpenses: Long,
) )

View file

@ -1,50 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout <FrameLayout
android:id="@+id/overviewContent"
android:orientation="vertical"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/balanceLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/label_current_balance" />
<TextView
android:id="@+id/balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textSize="36sp" />
</LinearLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/noData"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center">
android:layout_margin="8dp"
android:text="@string/overview_no_data"
android:textAlignment="center"
android:textColor="@color/colorTextPrimary"
android:textSize="24sp"
android:visibility="gone"
tools:visibility="visible" />
<ProgressBar <LinearLayout
android:id="@+id/progressBar" android:id="@+id/overviewContent"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone" android:gravity="center"
tools:visibility="visible" /> android:orientation="vertical">
</FrameLayout>
<TextView
android:id="@+id/balanceLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/label_current_balance" />
<TextView
android:id="@+id/balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textSize="36sp" />
<com.github.mikephil.charting.charts.HorizontalBarChart
android:id="@+id/expectedActualChart"
android:layout_width="match_parent"
android:layout_height="200dp" />
</LinearLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/noData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="8dp"
android:text="@string/overview_no_data"
android:textAlignment="center"
android:textColor="@color/colorTextPrimary"
android:textSize="24sp"
android:visibility="gone"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</ScrollView>

View file

@ -8,4 +8,5 @@
<color name="colorTextGreen">#388e3c</color> <color name="colorTextGreen">#388e3c</color>
<color name="colorTextRed">#d32f2f</color> <color name="colorTextRed">#d32f2f</color>
<color name="colorTextPrimaryInverted">#FFDADADA</color> <color name="colorTextPrimaryInverted">#FFDADADA</color>
<color name="colorSecondary">#888888</color>
</resources> </resources>