Add ability to create/edit categories

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2023-01-31 20:52:39 -07:00
parent 7b1b088080
commit badafaffc7
12 changed files with 395 additions and 128 deletions

View file

@ -33,7 +33,13 @@ import kotlin.math.max
@Composable @Composable
fun CategoriesScreen(store: Store) { fun CategoriesScreen(store: Store) {
val scrollState = rememberLazyListState() val scrollState = rememberLazyListState()
TwigsScaffold(store = store, title = "Categories") { TwigsScaffold(
store = store,
title = "Categories",
onClickFab = {
store.dispatch(CategoryAction.NewCategoryClicked)
}
) {
val state by store.state.collectAsState() val state by store.state.collectAsState()
state.categories?.let { categories -> state.categories?.let { categories ->
LazyColumn( LazyColumn(
@ -51,6 +57,9 @@ fun CategoriesScreen(store: Store) {
} ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { } ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator() CircularProgressIndicator()
} }
if (state.editingCategory) {
CategoryFormDialog(store = store)
}
} }
} }

View file

@ -21,12 +21,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.wbrawner.budget.ui.TwigsScaffold import com.wbrawner.budget.ui.TwigsScaffold
import com.wbrawner.budget.ui.base.TwigsApp import com.wbrawner.budget.ui.base.TwigsApp
import com.wbrawner.budget.ui.transaction.TransactionFormDialog
import com.wbrawner.budget.ui.transaction.TransactionListItem import com.wbrawner.budget.ui.transaction.TransactionListItem
import com.wbrawner.budget.ui.transaction.toCurrencyString import com.wbrawner.budget.ui.transaction.toCurrencyString
import com.wbrawner.budget.ui.util.format import com.wbrawner.budget.ui.util.format
import com.wbrawner.twigs.shared.Action import com.wbrawner.twigs.shared.Action
import com.wbrawner.twigs.shared.Store import com.wbrawner.twigs.shared.Store
import com.wbrawner.twigs.shared.category.Category import com.wbrawner.twigs.shared.category.Category
import com.wbrawner.twigs.shared.category.CategoryAction
import com.wbrawner.twigs.shared.transaction.Transaction import com.wbrawner.twigs.shared.transaction.Transaction
import com.wbrawner.twigs.shared.transaction.TransactionAction import com.wbrawner.twigs.shared.transaction.TransactionAction
import com.wbrawner.twigs.shared.transaction.groupByDate import com.wbrawner.twigs.shared.transaction.groupByDate
@ -38,7 +40,8 @@ import kotlin.math.abs
@Composable @Composable
fun CategoryDetailsScreen(store: Store) { fun CategoryDetailsScreen(store: Store) {
val state by store.state.collectAsState() val state by store.state.collectAsState()
val category = remember { state.categories!!.first { it.id == state.selectedCategory } } val category =
remember(state.editingCategory) { state.categories!!.first { it.id == state.selectedCategory } }
TwigsScaffold( TwigsScaffold(
store = store, store = store,
title = category.title, title = category.title,
@ -48,21 +51,27 @@ fun CategoryDetailsScreen(store: Store) {
} }
}, },
actions = { actions = {
IconButton({ store.dispatch(TransactionAction.EditTransaction(requireNotNull(category.id))) }) { IconButton({ store.dispatch(CategoryAction.EditCategory(requireNotNull(category.id))) }) {
Icon(Icons.Default.Edit, "Edit") Icon(Icons.Default.Edit, "Edit")
} }
},
onClickFab = {
store.dispatch(TransactionAction.NewTransactionClicked)
} }
) { padding -> ) { padding ->
CategoryDetails( CategoryDetails(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
category = category, category = category,
balance = state.categoryBalances!![category.id!!]!!, balance = state.categoryBalances!![category.id!!] ?: 0,
transactions = state.transactions!!.filter { it.categoryId == category.id }, transactions = state.transactions!!.filter { it.categoryId == category.id },
onTransactionClicked = { store.dispatch(TransactionAction.SelectTransaction(it.id)) } onTransactionClicked = { store.dispatch(TransactionAction.SelectTransaction(it.id)) }
) )
// if (state.editingCategory) { if (state.editingTransaction) {
// CategoryFormDialog(store = store) TransactionFormDialog(store = store)
// } }
if (state.editingCategory) {
CategoryFormDialog(store = store)
}
} }
} }

View file

@ -0,0 +1,258 @@
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.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.wbrawner.budget.ui.base.TwigsApp
import com.wbrawner.budget.ui.transaction.toDecimalString
import com.wbrawner.twigs.shared.Store
import com.wbrawner.twigs.shared.category.Category
import com.wbrawner.twigs.shared.category.CategoryAction
import java.util.*
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CategoryFormDialog(store: Store) {
Dialog(
onDismissRequest = { store.dispatch(CategoryAction.CancelEditCategory) },
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false
)
) {
CategoryForm(store)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CategoryForm(store: Store) {
val state by store.state.collectAsState()
val category = remember {
val defaultCategory = Category(
title = "",
amount = 0L,
expense = true,
budgetId = state.selectedBudget!!,
)
if (state.selectedCategory.isNullOrBlank()) {
defaultCategory
} else {
state.categories?.first { it.id == state.selectedCategory } ?: defaultCategory
}
}
val (title, setTitle) = remember { mutableStateOf(category.title) }
val (description, setDescription) = remember { mutableStateOf(category.description ?: "") }
val (amount, setAmount) = remember { mutableStateOf(category.amount.toDecimalString()) }
val (expense, setExpense) = remember { mutableStateOf(category.expense) }
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { store.dispatch(CategoryAction.CancelEditCategory) }) {
Icon(Icons.Default.Close, "Cancel")
}
},
title = {
Text(if (category.id.isNullOrBlank()) "New Category" else "Edit Category")
}
)
}
) {
CategoryForm(
modifier = Modifier.padding(it),
title = title,
setTitle = setTitle,
description = description,
setDescription = setDescription,
amount = amount,
setAmount = setAmount,
expense = expense,
setExpense = setExpense,
) {
store.dispatch(
category.id?.let { id ->
CategoryAction.UpdateCategory(
id = id,
title = title,
description = description,
amount = (amount.toDouble() * 100).toLong(),
expense = expense,
)
} ?: CategoryAction.CreateCategory(
title = title,
description = description,
amount = (amount.toDouble() * 100).toLong(),
expense = expense,
)
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
fun CategoryForm(
modifier: Modifier,
title: String,
setTitle: (String) -> Unit,
description: String,
setDescription: (String) -> Unit,
amount: String,
setAmount: (String) -> Unit,
expense: Boolean,
setExpense: (Boolean) -> Unit,
save: () -> Unit
) {
val scrollState = rememberScrollState()
val (titleInput, descriptionInput, amountInput) = FocusRequester.createRefs()
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = spacedBy(8.dp, Alignment.Top),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// if (error.isNotBlank()) {
// Text(text = error, color = Color.Red)
// }
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(titleInput)
.onPreviewKeyEvent {
if (it.key == Key.Tab && !it.isShiftPressed) {
descriptionInput.requestFocus()
true
} else {
false
}
},
value = title,
onValueChange = setTitle,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
label = { Text("Title") },
keyboardActions = KeyboardActions(onNext = {
descriptionInput.requestFocus()
}),
maxLines = 1
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(descriptionInput)
.onPreviewKeyEvent {
if (it.key == Key.Tab) {
if (it.isShiftPressed) {
titleInput.requestFocus()
} else {
amountInput.requestFocus()
}
true
} else {
false
}
},
value = description,
onValueChange = setDescription,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Next
),
label = { Text("Description") },
keyboardActions = KeyboardActions(onNext = {
amountInput.requestFocus()
}),
)
val keyboardController = LocalSoftwareKeyboardController.current
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(amountInput)
.onPreviewKeyEvent {
if (it.key == Key.Tab && it.isShiftPressed) {
descriptionInput.requestFocus()
true
} else {
false
}
},
value = amount,
onValueChange = setAmount,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Next
),
label = { Text("Amount") },
keyboardActions = KeyboardActions(onNext = {
keyboardController?.hide()
}),
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = spacedBy(8.dp)) {
Row(
modifier = Modifier.clickable {
setExpense(true)
},
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = expense, onClick = { setExpense(true) })
Text(text = "Expense")
}
Row(
modifier = Modifier.clickable {
setExpense(false)
},
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = !expense, onClick = { setExpense(false) })
Text(text = "Income")
}
}
Button(
modifier = Modifier.fillMaxWidth(),
onClick = save
) {
Text("Save")
}
}
}
@Composable
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
fun CategoryForm_Preview() {
TwigsApp {
CategoryForm(store = Store(reducers = emptyList()))
}
}

View file

@ -37,7 +37,8 @@ import kotlinx.datetime.Clock
@Composable @Composable
fun TransactionDetailsScreen(store: Store) { fun TransactionDetailsScreen(store: Store) {
val state by store.state.collectAsState() val state by store.state.collectAsState()
val transaction = remember { state.transactions!!.first { it.id == state.selectedTransaction } } val transaction =
remember(state.editingTransaction) { state.transactions!!.first { it.id == state.selectedTransaction } }
val createdBy = state.selectedTransactionCreatedBy ?: run { val createdBy = state.selectedTransactionCreatedBy ?: run {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator() CircularProgressIndicator()

View file

@ -133,4 +133,13 @@ fun TransactionListItem_Preview() {
fun Long.toCurrencyString(): String = fun Long.toCurrencyString(): String =
NumberFormat.getCurrencyInstance().format(this.toDouble() / 100.0) NumberFormat.getCurrencyInstance().format(this.toDouble() / 100.0)
fun Long.toDecimalString(): String = if (this > 0) (this.toDouble() / 100.0).toString() else "" fun Long.toDecimalString(): String = if (this > 0) {
val decimal = (this.toDouble() / 100.0).toString()
if (decimal.length - decimal.lastIndexOf('.') == 2) {
decimal + '0'
} else {
decimal
}
} else {
""
}

View file

@ -16,3 +16,11 @@ interface Repository<T> {
interface Identifiable { interface Identifiable {
val id: String? val id: String?
} }
inline fun <T : Identifiable> MutableList<T>.replace(item: T) {
val index = indexOf(item)
if (index > -1) {
removeAt(index)
}
add(index.coerceAtLeast(0), item)
}

View file

@ -1,15 +1,16 @@
package com.wbrawner.twigs.shared.budget package com.wbrawner.twigs.shared.budget
import com.wbrawner.twigs.shared.Identifiable
import com.wbrawner.twigs.shared.user.UserPermission import com.wbrawner.twigs.shared.user.UserPermission
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Budget( data class Budget(
val id: String? = null, override val id: String? = null,
val name: String, val name: String,
val description: String? = null, val description: String? = null,
val users: List<UserPermission> = emptyList() val users: List<UserPermission> = emptyList()
) ) : Identifiable
data class NewBudgetRequest( data class NewBudgetRequest(
val name: String, val name: String,

View file

@ -6,6 +6,7 @@ import com.wbrawner.twigs.shared.Action
import com.wbrawner.twigs.shared.Reducer import com.wbrawner.twigs.shared.Reducer
import com.wbrawner.twigs.shared.Route import com.wbrawner.twigs.shared.Route
import com.wbrawner.twigs.shared.State import com.wbrawner.twigs.shared.State
import com.wbrawner.twigs.shared.replace
import com.wbrawner.twigs.shared.user.ConfigAction import com.wbrawner.twigs.shared.user.ConfigAction
import com.wbrawner.twigs.shared.user.UserPermission import com.wbrawner.twigs.shared.user.UserPermission
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -82,7 +83,7 @@ class BudgetReducer(
is BudgetAction.SaveBudgetSuccess -> { is BudgetAction.SaveBudgetSuccess -> {
val currentState = state() val currentState = state()
val budgets = currentState.budgets?.toMutableList() ?: mutableListOf() val budgets = currentState.budgets?.toMutableList() ?: mutableListOf()
budgets.add(action.budget) budgets.replace(action.budget)
budgets.sortBy { it.name } budgets.sortBy { it.name }
currentState.copy( currentState.copy(
loading = false, loading = false,

View file

@ -1,14 +1,15 @@
package com.wbrawner.twigs.shared.category package com.wbrawner.twigs.shared.category
import com.wbrawner.twigs.shared.Identifiable
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Category( data class Category(
val budgetId: String, val budgetId: String,
val id: String? = null, override val id: String? = null,
val title: String, val title: String,
val description: String? = null, val description: String? = null,
val amount: Long, val amount: Long,
val expense: Boolean = true, val expense: Boolean = true,
val archived: Boolean = false val archived: Boolean = false
) ) : Identifiable

View file

@ -5,6 +5,7 @@ import com.wbrawner.twigs.shared.Reducer
import com.wbrawner.twigs.shared.Route import com.wbrawner.twigs.shared.Route
import com.wbrawner.twigs.shared.State import com.wbrawner.twigs.shared.State
import com.wbrawner.twigs.shared.budget.BudgetAction import com.wbrawner.twigs.shared.budget.BudgetAction
import com.wbrawner.twigs.shared.replace
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
sealed interface CategoryAction : Action { sealed interface CategoryAction : Action {
@ -16,8 +17,11 @@ sealed interface CategoryAction : Action {
data class LoadCategoriesSuccess(val categories: List<Category>) : CategoryAction data class LoadCategoriesSuccess(val categories: List<Category>) : CategoryAction
data class LoadCategoriesFailed(val error: Exception) : CategoryAction data class LoadCategoriesFailed(val error: Exception) : CategoryAction
object NewCategoryClicked : CategoryAction
object CancelEditCategory : CategoryAction
data class CreateCategory( data class CreateCategory(
val name: String, val title: String,
val description: String? = null, val description: String? = null,
val amount: Long, val amount: Long,
val expense: Boolean val expense: Boolean
@ -27,7 +31,7 @@ sealed interface CategoryAction : Action {
data class SaveCategoryFailure( data class SaveCategoryFailure(
val id: String? = null, val id: String? = null,
val name: String, val title: String,
val description: String? = null, val description: String? = null,
val amount: Long, val amount: Long,
val expense: Boolean, val expense: Boolean,
@ -38,14 +42,12 @@ sealed interface CategoryAction : Action {
data class SelectCategory(val id: String?) : CategoryAction data class SelectCategory(val id: String?) : CategoryAction
data class CategorySelected(val id: String) : CategoryAction
data class UpdateCategory( data class UpdateCategory(
val id: String, val id: String,
val name: String, val title: String,
val description: String? = null, val description: String? = null,
val amount: Long, val amount: Long,
val expense: Boolean val expense: Boolean,
) : CategoryAction ) : CategoryAction
data class DeleteCategory(val id: String) : CategoryAction data class DeleteCategory(val id: String) : CategoryAction
@ -102,95 +104,62 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu
budgetBalance = action.budgetBalance, budgetBalance = action.budgetBalance,
categoryBalances = action.categoryBalances categoryBalances = action.categoryBalances
) )
// is BudgetAction.CreateBudget -> {
// launch { is CategoryAction.CancelEditCategory -> state().copy(editingCategory = false)
// val budget = budgetRepository.create(
// Budget( is CategoryAction.NewCategoryClicked -> state().copy(editingCategory = true)
// name = action.name,
// description = action.description, is CategoryAction.CreateCategory -> {
// users = action.users val currentState = state()
// ) val budgetId = requireNotNull(currentState.selectedBudget)
// ) launch {
// dispatch(BudgetAction.SaveBudgetSuccess(budget)) val category = categoryRepository.create(
// } Category(
// state().copy(loading = true) title = action.title,
// } description = action.description,
// is BudgetAction.SaveBudgetSuccess -> { amount = action.amount,
// val currentState = state() budgetId = budgetId
// val budgets = currentState.budgets?.toMutableList() ?: mutableListOf() )
// budgets.add(action.budget) )
// budgets.sortBy { it.name } dispatch(CategoryAction.SaveCategorySuccess(category))
// currentState.copy( }
// loading = false, currentState.copy(loading = true)
// budgets = budgets.toList(), }
// selectedBudget = action.budget.id,
// editingBudget = false is CategoryAction.SaveCategorySuccess -> {
// ) val currentState = state()
// } val categories = currentState.categories?.toMutableList() ?: mutableListOf()
// is ConfigAction.LoginSuccess -> { categories.replace(action.category)
// launch { categories.sortBy { it.title }
// try { currentState.copy(
// val budgets = budgetRepository.findAll() loading = false,
// dispatch(BudgetAction.LoadBudgetsSuccess(budgets)) categories = categories.toList(),
// } catch (e: Exception) { selectedCategory = action.category.id,
// dispatch(BudgetAction.LoadBudgetsFailed(e)) editingCategory = false,
// } route = Route.Categories(action.category.id)
// } )
// state().copy(loading = true) }
// }
// is ConfigAction.Logout -> state().copy( is CategoryAction.EditCategory -> state().copy(
// budgets = null, editingCategory = true,
// selectedBudget = null, selectedCategory = action.id
// editingBudget = false )
// )
//// is BudgetAction.EditBudget -> state.copy( is CategoryAction.UpdateCategory -> {
//// editingBudget = true, launch {
//// selectedBudget = action.id val oldCategory = categoryRepository.findById(action.id)
//// ) val category = categoryRepository.update(
// is BudgetAction.SelectBudget -> { oldCategory.copy(
// val currentState = state() title = action.title,
// val budgetId = currentState.budgets description = action.description,
// ?.firstOrNull { it.id == action.id } amount = action.amount
// ?.id )
// ?: currentState.budgets?.firstOrNull()?.id )
// settings[KEY_LAST_BUDGET] = budgetId dispatch(CategoryAction.SaveCategorySuccess(category))
// dispatch(BudgetAction.BudgetSelected(budgetId!!)) }
// state() state().copy(loading = true)
// } }
// is BudgetAction.BudgetSelected -> state().copy(selectedBudget = action.id)
//
//// is BudgetAction.UpdateBudget -> state.copy(loading = true).also {
//// dispatch(action.async())
//// }
//// is BudgetAction.DeleteBudget -> state.copy(loading = true).also {
//// dispatch(action.async())
//// }
////
//// is BudgetAsyncAction.UpdateBudgetAsync -> {
//// budgetRepository.update(
//// Budget(
//// id = action.id,
//// name = action.name,
//// description = action.description,
//// users = action.users
//// )
//// )
//// state().copy(
//// loading = false,
//// editingBudget = false,
//// )
//// }
//// is BudgetAsyncAction.DeleteBudgetAsync -> {
//// budgetRepository.delete(action.id)
//// val currentState = state()
//// val budgets = currentState.budgets?.filterNot { it.id == action.id }
//// currentState.copy(
//// loading = false,
//// budgets = budgets,
//// editingBudget = false,
//// selectedBudget = null
//// )
//// }
else -> state() else -> state()
} }
} }

View file

@ -1,5 +1,6 @@
package com.wbrawner.twigs.shared.transaction package com.wbrawner.twigs.shared.transaction
import com.wbrawner.twigs.shared.Identifiable
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -11,17 +12,17 @@ import kotlinx.serialization.encoding.Encoder
@Serializable @Serializable
data class Transaction( data class Transaction(
val id: String? = null, override val id: String? = null,
val title: String, val title: String,
@Serializable(with = DateSerializer::class) @Serializable(with = DateSerializer::class)
val date: Instant, val date: Instant,
val description: String? = null, val description: String? = null,
val amount: Long, val amount: Long,
val categoryId: String? = null, val categoryId: String? = null,
val budgetId: String, val budgetId: String,
val expense: Boolean, val expense: Boolean,
val createdBy: String val createdBy: String
) ) : Identifiable
@Serializable @Serializable
data class BalanceResponse(val balance: Long) data class BalanceResponse(val balance: Long)

View file

@ -7,6 +7,7 @@ import com.wbrawner.twigs.shared.State
import com.wbrawner.twigs.shared.budget.Budget import com.wbrawner.twigs.shared.budget.Budget
import com.wbrawner.twigs.shared.budget.BudgetAction import com.wbrawner.twigs.shared.budget.BudgetAction
import com.wbrawner.twigs.shared.category.Category import com.wbrawner.twigs.shared.category.Category
import com.wbrawner.twigs.shared.replace
import com.wbrawner.twigs.shared.user.ConfigAction import com.wbrawner.twigs.shared.user.ConfigAction
import com.wbrawner.twigs.shared.user.User import com.wbrawner.twigs.shared.user.User
import com.wbrawner.twigs.shared.user.UserPermission import com.wbrawner.twigs.shared.user.UserPermission
@ -144,8 +145,7 @@ class TransactionReducer(
is TransactionAction.SaveTransactionSuccess -> { is TransactionAction.SaveTransactionSuccess -> {
val currentState = state() val currentState = state()
val transactions = currentState.transactions?.toMutableList() ?: mutableListOf() val transactions = currentState.transactions?.toMutableList() ?: mutableListOf()
transactions.removeAll { it.id == action.transaction.id } transactions.replace(action.transaction)
transactions.add(action.transaction)
transactions.sortByDescending { it.date } transactions.sortByDescending { it.date }
currentState.copy( currentState.copy(
loading = false, loading = false,