Add CategoryDetailsScreen
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
afe12c3e5d
commit
70401bccc4
6 changed files with 187 additions and 11 deletions
|
@ -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)
|
||||
// }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue