Add CategoryDetailsScreen

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2023-01-22 21:57:48 -07:00
parent afe12c3e5d
commit 70401bccc4
6 changed files with 187 additions and 11 deletions

View file

@ -28,6 +28,8 @@ import androidx.navigation.navArgument
import com.wbrawner.budget.R
import com.wbrawner.budget.ui.auth.LoginScreen
import com.wbrawner.budget.ui.base.TwigsApp
import com.wbrawner.budget.ui.category.CategoriesScreen
import com.wbrawner.budget.ui.category.CategoryDetailsScreen
import com.wbrawner.budget.ui.transaction.TransactionDetailsScreen
import com.wbrawner.budget.ui.transaction.TransactionsScreen
import com.wbrawner.twigs.shared.Route
@ -63,7 +65,7 @@ class MainActivity : AppCompatActivity() {
composable(Route.Overview.path) {
OverviewScreen(store = store)
}
composable(Route.Transactions(selected = null).path) {
composable(Route.Transactions().path) {
TransactionsScreen(store = store)
}
composable(
@ -75,9 +77,18 @@ class MainActivity : AppCompatActivity() {
) {
TransactionDetailsScreen(store = store)
}
composable(Route.Categories(selected = null).path) {
composable(Route.Categories().path) {
CategoriesScreen(store = store)
}
composable(
Route.Categories(selected = "{id}").path,
arguments = listOf(navArgument("id") {
type = NavType.StringType
nullable = false
})
) {
CategoryDetailsScreen(store = store)
}
// composable(Route.RECURRING_TRANSACTIONS.path) {
// RecurringTransactionsScreen(store = store)
// }

View file

@ -1,4 +1,4 @@
package com.wbrawner.budget.ui
package com.wbrawner.budget.ui.category
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -7,7 +7,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material3.*
@ -20,10 +20,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.wbrawner.budget.ui.TwigsScaffold
import com.wbrawner.budget.ui.base.TwigsApp
import com.wbrawner.twigs.shared.Store
import com.wbrawner.twigs.shared.category.Category
import com.wbrawner.twigs.shared.transaction.TransactionAction
import com.wbrawner.twigs.shared.category.CategoryAction
import java.util.*
import kotlin.math.abs
import kotlin.math.max
@ -41,9 +42,9 @@ fun CategoriesScreen(store: Store) {
.padding(it),
state = scrollState
) {
itemsIndexed(categories, key = { _, t -> t.id!! }) { index, category ->
items(categories, key = { c -> c.id!! }) { category ->
CategoryListItem(category, state.categoryBalances?.get(category.id!!)) {
store.dispatch(TransactionAction.SelectTransaction(category.id))
store.dispatch(CategoryAction.SelectCategory(category.id))
}
}
}

View file

@ -0,0 +1,154 @@
package com.wbrawner.budget.ui.category
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.wbrawner.budget.ui.TwigsScaffold
import com.wbrawner.budget.ui.base.TwigsApp
import com.wbrawner.budget.ui.transaction.TransactionListItem
import com.wbrawner.budget.ui.transaction.toCurrencyString
import com.wbrawner.budget.ui.util.format
import com.wbrawner.twigs.shared.Action
import com.wbrawner.twigs.shared.Store
import com.wbrawner.twigs.shared.category.Category
import com.wbrawner.twigs.shared.transaction.Transaction
import com.wbrawner.twigs.shared.transaction.TransactionAction
import com.wbrawner.twigs.shared.transaction.groupByDate
import kotlinx.datetime.Clock
import kotlinx.datetime.toInstant
import kotlin.math.abs
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CategoryDetailsScreen(store: Store) {
val state by store.state.collectAsState()
val category = remember { state.categories!!.first { it.id == state.selectedCategory } }
TwigsScaffold(
store = store,
title = category.title,
navigationIcon = {
IconButton(onClick = { store.dispatch(Action.Back) }) {
Icon(Icons.Default.ArrowBack, "Go back")
}
},
actions = {
IconButton({ store.dispatch(TransactionAction.EditTransaction(requireNotNull(category.id))) }) {
Icon(Icons.Default.Edit, "Edit")
}
}
) { padding ->
CategoryDetails(
modifier = Modifier.padding(padding),
category = category,
balance = state.categoryBalances!![category.id!!]!!,
transactions = state.transactions!!.filter { it.categoryId == category.id },
onTransactionClicked = { store.dispatch(TransactionAction.SelectTransaction(it.id)) }
)
// if (state.editingCategory) {
// CategoryFormDialog(store = store)
// }
}
}
@Composable
fun CategoryDetails(
modifier: Modifier = Modifier,
category: Category,
balance: Long,
transactions: List<Transaction>,
onTransactionClicked: (Transaction) -> Unit
) {
val transactionGroups = remember { transactions.groupByDate() }
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
) {
category.description?.let {
item {
Text(modifier = Modifier.padding(8.dp), text = it)
}
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
LabeledCounter("Planned", category.amount.toCurrencyString())
LabeledCounter("Actual", abs(balance).toCurrencyString())
LabeledCounter("Remaining", (category.amount - abs(balance)).toCurrencyString())
}
}
transactionGroups.forEach { (timestamp, transactions) ->
item(timestamp) {
Text(
modifier = Modifier.padding(8.dp),
text = timestamp.toInstant().format(LocalContext.current),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
item(transactions) {
Card {
transactions.forEach { transaction ->
TransactionListItem(transaction, onTransactionClicked)
}
}
}
}
}
}
@Composable
fun LabeledCounter(label: String, counter: String) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = spacedBy(4.dp)
) {
Text(text = label, style = MaterialTheme.typography.labelMedium)
Text(text = counter, style = MaterialTheme.typography.bodyLarge)
}
}
@Composable
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
fun CategoryDetails_Preview() {
TwigsApp {
CategoryDetails(
category = Category(title = "Coffee", budgetId = "budget", amount = 1000),
balance = 500,
transactions = listOf(
Transaction(
title = "DAZBOG",
description = "Chokolat Cappuccino",
date = Clock.System.now(),
amount = 550,
categoryId = "coffee",
budgetId = "budget",
createdBy = "user",
expense = true
)
),
onTransactionClicked = {}
)
}
}

View file

@ -89,7 +89,7 @@ fun TransactionDetails(
.fillMaxSize()
.scrollable(scrollState, Orientation.Vertical)
.padding(16.dp),
verticalArrangement = spacedBy(8.dp)
verticalArrangement = spacedBy(16.dp)
) {
Text(
text = transaction.title,
@ -114,7 +114,7 @@ fun TransactionDetails(
@Composable
fun LabeledField(label: String, field: String) {
Column(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = spacedBy(4.dp)) {
Text(text = label, style = MaterialTheme.typography.bodySmall)
Text(text = field, style = MaterialTheme.typography.bodyLarge)
}

View file

@ -48,7 +48,7 @@ data class State(
val initialRoute: Route = Route.Login
) {
override fun toString(): String {
return "State(budget=$selectedBudget, selectedTransaction=$selectedTransaction, route=$route)"
return "State(category=$selectedCategory, route=$route)"
}
}

View file

@ -57,7 +57,12 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu
val currentState = state()
currentState.copy(
editingCategory = false,
selectedCategory = if (currentState.editingCategory) currentState.selectedCategory else null
selectedCategory = if (currentState.editingCategory) currentState.selectedCategory else null,
route = if (currentState.route is Route.Categories && !currentState.route.selected.isNullOrBlank() && !currentState.editingCategory) {
Route.Categories()
} else {
currentState.route
}
)
}
@ -74,6 +79,11 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu
state().copy(categories = null)
}
is CategoryAction.SelectCategory -> state().copy(
selectedCategory = action.id,
route = Route.Categories(action.id)
).also { newState -> println("Category selected state update: $newState") }
is CategoryAction.LoadCategoriesSuccess -> state().copy(categories = action.categories)
.also {
launch {