Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
91f2ef3680 | |||
c7cdf2fe9c |
5 changed files with 141 additions and 46 deletions
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue