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"
kapt "com.google.dagger:dagger-compiler:$dagger"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.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.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
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.AsyncState
import com.wbrawner.budget.R
@ -30,6 +37,24 @@ class OverviewFragment : Fragment() {
): View? = inflater.inflate(R.layout.fragment_overview, container, false)
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 ->
when (state) {
is AsyncState.Loading -> {
@ -43,6 +68,30 @@ class OverviewFragment : Fragment() {
progressBar.visibility = View.GONE
activity?.title = state.data.budget.name
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 -> {
overviewContent.visibility = View.GONE

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.*
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.common.transaction.TransactionRepository
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -14,12 +15,16 @@ class OverviewViewModel : ViewModel() {
@Inject
lateinit var budgetRepo: BudgetRepository
@Inject
lateinit var categoryRepo: CategoryRepository
@Inject
lateinit var transactionRepo: TransactionRepository
fun loadOverview(lifecycleOwner: LifecycleOwner) {
budgetRepo.currentBudget.observe(lifecycleOwner, Observer { budget ->
if (budget == null) {
val budgetId = budget?.id
if (budgetId == null) {
state.postValue(AsyncState.Error("Invalid Budget ID"))
return@Observer
}
@ -27,9 +32,28 @@ class OverviewViewModel : ViewModel() {
state.postValue(AsyncState.Loading)
try {
// 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(
budget,
budgetRepo.getBalance(budget.id!!)
budgetRepo.getBalance(budgetId),
expectedIncome,
expectedExpenses,
actualIncome,
actualExpenses
)))
} catch (e: Exception) {
state.postValue(AsyncState.Error(e))
@ -41,5 +65,9 @@ class OverviewViewModel : ViewModel() {
data class OverviewState(
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"?>
<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"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
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"
<FrameLayout
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" />
android:layout_gravity="center">
<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>
<LinearLayout
android:id="@+id/overviewContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<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="colorTextRed">#d32f2f</color>
<color name="colorTextPrimaryInverted">#FFDADADA</color>
<color name="colorSecondary">#888888</color>
</resources>