From badafaffc7732083caf7706026571b2a921bd017 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Tue, 31 Jan 2023 20:52:39 -0700 Subject: [PATCH] Add ability to create/edit categories Signed-off-by: William Brawner --- .../budget/ui/category/CategoriesScreen.kt | 11 +- .../ui/category/CategoryDetailsScreen.kt | 21 +- .../budget/ui/category/CategoryForm.kt | 258 ++++++++++++++++++ .../transaction/TransactionDetailsScreen.kt | 3 +- .../ui/transaction/TransactionsScreen.kt | 11 +- .../com/wbrawner/twigs/shared/Repository.kt | 8 + .../wbrawner/twigs/shared/budget/Budget.kt | 5 +- .../twigs/shared/budget/BudgetAction.kt | 3 +- .../twigs/shared/category/Category.kt | 17 +- .../twigs/shared/category/CategoryAction.kt | 159 +++++------ .../twigs/shared/transaction/Transaction.kt | 23 +- .../shared/transaction/TransactionAction.kt | 4 +- 12 files changed, 395 insertions(+), 128 deletions(-) create mode 100644 android/src/main/java/com/wbrawner/budget/ui/category/CategoryForm.kt diff --git a/android/src/main/java/com/wbrawner/budget/ui/category/CategoriesScreen.kt b/android/src/main/java/com/wbrawner/budget/ui/category/CategoriesScreen.kt index ac8de86..282c8fd 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/category/CategoriesScreen.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/category/CategoriesScreen.kt @@ -33,7 +33,13 @@ import kotlin.math.max @Composable fun CategoriesScreen(store: Store) { val scrollState = rememberLazyListState() - TwigsScaffold(store = store, title = "Categories") { + TwigsScaffold( + store = store, + title = "Categories", + onClickFab = { + store.dispatch(CategoryAction.NewCategoryClicked) + } + ) { val state by store.state.collectAsState() state.categories?.let { categories -> LazyColumn( @@ -51,6 +57,9 @@ fun CategoriesScreen(store: Store) { } ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } + if (state.editingCategory) { + CategoryFormDialog(store = store) + } } } diff --git a/android/src/main/java/com/wbrawner/budget/ui/category/CategoryDetailsScreen.kt b/android/src/main/java/com/wbrawner/budget/ui/category/CategoryDetailsScreen.kt index 92d7ef3..2fbfc68 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/category/CategoryDetailsScreen.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/category/CategoryDetailsScreen.kt @@ -21,12 +21,14 @@ 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.TransactionFormDialog 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.category.CategoryAction import com.wbrawner.twigs.shared.transaction.Transaction import com.wbrawner.twigs.shared.transaction.TransactionAction import com.wbrawner.twigs.shared.transaction.groupByDate @@ -38,7 +40,8 @@ import kotlin.math.abs @Composable fun CategoryDetailsScreen(store: Store) { 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( store = store, title = category.title, @@ -48,21 +51,27 @@ fun CategoryDetailsScreen(store: Store) { } }, actions = { - IconButton({ store.dispatch(TransactionAction.EditTransaction(requireNotNull(category.id))) }) { + IconButton({ store.dispatch(CategoryAction.EditCategory(requireNotNull(category.id))) }) { Icon(Icons.Default.Edit, "Edit") } + }, + onClickFab = { + store.dispatch(TransactionAction.NewTransactionClicked) } ) { padding -> CategoryDetails( modifier = Modifier.padding(padding), category = category, - balance = state.categoryBalances!![category.id!!]!!, + balance = state.categoryBalances!![category.id!!] ?: 0, transactions = state.transactions!!.filter { it.categoryId == category.id }, onTransactionClicked = { store.dispatch(TransactionAction.SelectTransaction(it.id)) } ) -// if (state.editingCategory) { -// CategoryFormDialog(store = store) -// } + if (state.editingTransaction) { + TransactionFormDialog(store = store) + } + if (state.editingCategory) { + CategoryFormDialog(store = store) + } } } diff --git a/android/src/main/java/com/wbrawner/budget/ui/category/CategoryForm.kt b/android/src/main/java/com/wbrawner/budget/ui/category/CategoryForm.kt new file mode 100644 index 0000000..c328bd7 --- /dev/null +++ b/android/src/main/java/com/wbrawner/budget/ui/category/CategoryForm.kt @@ -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())) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionDetailsScreen.kt b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionDetailsScreen.kt index f1c7261..1db9ee7 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionDetailsScreen.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionDetailsScreen.kt @@ -37,7 +37,8 @@ import kotlinx.datetime.Clock @Composable fun TransactionDetailsScreen(store: Store) { 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 { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() diff --git a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt index 7db9e08..31a2a96 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt @@ -133,4 +133,13 @@ fun TransactionListItem_Preview() { fun Long.toCurrencyString(): String = NumberFormat.getCurrencyInstance().format(this.toDouble() / 100.0) -fun Long.toDecimalString(): String = if (this > 0) (this.toDouble() / 100.0).toString() else "" \ No newline at end of file +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 { + "" +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Repository.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Repository.kt index 84c17d1..c76476c 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Repository.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Repository.kt @@ -16,3 +16,11 @@ interface Repository { interface Identifiable { val id: String? } + +inline fun MutableList.replace(item: T) { + val index = indexOf(item) + if (index > -1) { + removeAt(index) + } + add(index.coerceAtLeast(0), item) +} diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/Budget.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/Budget.kt index 93dad9c..4adc833 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/Budget.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/Budget.kt @@ -1,15 +1,16 @@ package com.wbrawner.twigs.shared.budget +import com.wbrawner.twigs.shared.Identifiable import com.wbrawner.twigs.shared.user.UserPermission import kotlinx.serialization.Serializable @Serializable data class Budget( - val id: String? = null, + override val id: String? = null, val name: String, val description: String? = null, val users: List = emptyList() -) +) : Identifiable data class NewBudgetRequest( val name: String, diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/BudgetAction.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/BudgetAction.kt index b5515e1..1b3db09 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/BudgetAction.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/budget/BudgetAction.kt @@ -6,6 +6,7 @@ import com.wbrawner.twigs.shared.Action import com.wbrawner.twigs.shared.Reducer import com.wbrawner.twigs.shared.Route 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.UserPermission import kotlinx.coroutines.launch @@ -82,7 +83,7 @@ class BudgetReducer( is BudgetAction.SaveBudgetSuccess -> { val currentState = state() val budgets = currentState.budgets?.toMutableList() ?: mutableListOf() - budgets.add(action.budget) + budgets.replace(action.budget) budgets.sortBy { it.name } currentState.copy( loading = false, diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/Category.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/Category.kt index abc0133..517d202 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/Category.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/Category.kt @@ -1,14 +1,15 @@ package com.wbrawner.twigs.shared.category +import com.wbrawner.twigs.shared.Identifiable import kotlinx.serialization.Serializable @Serializable data class Category( - val budgetId: String, - val id: String? = null, - val title: String, - val description: String? = null, - val amount: Long, - val expense: Boolean = true, - val archived: Boolean = false -) \ No newline at end of file + val budgetId: String, + override val id: String? = null, + val title: String, + val description: String? = null, + val amount: Long, + val expense: Boolean = true, + val archived: Boolean = false +) : Identifiable \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/CategoryAction.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/CategoryAction.kt index 163e608..93d1893 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/CategoryAction.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/category/CategoryAction.kt @@ -5,6 +5,7 @@ import com.wbrawner.twigs.shared.Reducer import com.wbrawner.twigs.shared.Route import com.wbrawner.twigs.shared.State import com.wbrawner.twigs.shared.budget.BudgetAction +import com.wbrawner.twigs.shared.replace import kotlinx.coroutines.launch sealed interface CategoryAction : Action { @@ -16,8 +17,11 @@ sealed interface CategoryAction : Action { data class LoadCategoriesSuccess(val categories: List) : CategoryAction data class LoadCategoriesFailed(val error: Exception) : CategoryAction + object NewCategoryClicked : CategoryAction + object CancelEditCategory : CategoryAction + data class CreateCategory( - val name: String, + val title: String, val description: String? = null, val amount: Long, val expense: Boolean @@ -27,7 +31,7 @@ sealed interface CategoryAction : Action { data class SaveCategoryFailure( val id: String? = null, - val name: String, + val title: String, val description: String? = null, val amount: Long, val expense: Boolean, @@ -38,14 +42,12 @@ sealed interface CategoryAction : Action { data class SelectCategory(val id: String?) : CategoryAction - data class CategorySelected(val id: String) : CategoryAction - data class UpdateCategory( val id: String, - val name: String, + val title: String, val description: String? = null, val amount: Long, - val expense: Boolean + val expense: Boolean, ) : CategoryAction data class DeleteCategory(val id: String) : CategoryAction @@ -102,95 +104,62 @@ class CategoryReducer(private val categoryRepository: CategoryRepository) : Redu budgetBalance = action.budgetBalance, categoryBalances = action.categoryBalances ) -// is BudgetAction.CreateBudget -> { -// launch { -// val budget = budgetRepository.create( -// Budget( -// name = action.name, -// description = action.description, -// users = action.users -// ) -// ) -// dispatch(BudgetAction.SaveBudgetSuccess(budget)) -// } -// state().copy(loading = true) -// } -// is BudgetAction.SaveBudgetSuccess -> { -// val currentState = state() -// val budgets = currentState.budgets?.toMutableList() ?: mutableListOf() -// budgets.add(action.budget) -// budgets.sortBy { it.name } -// currentState.copy( -// loading = false, -// budgets = budgets.toList(), -// selectedBudget = action.budget.id, -// editingBudget = false -// ) -// } -// is ConfigAction.LoginSuccess -> { -// launch { -// try { -// val budgets = budgetRepository.findAll() -// dispatch(BudgetAction.LoadBudgetsSuccess(budgets)) -// } catch (e: Exception) { -// dispatch(BudgetAction.LoadBudgetsFailed(e)) -// } -// } -// state().copy(loading = true) -// } -// is ConfigAction.Logout -> state().copy( -// budgets = null, -// selectedBudget = null, -// editingBudget = false -// ) -//// is BudgetAction.EditBudget -> state.copy( -//// editingBudget = true, -//// selectedBudget = action.id -//// ) -// is BudgetAction.SelectBudget -> { -// val currentState = state() -// val budgetId = currentState.budgets -// ?.firstOrNull { it.id == action.id } -// ?.id -// ?: currentState.budgets?.firstOrNull()?.id -// settings[KEY_LAST_BUDGET] = budgetId -// dispatch(BudgetAction.BudgetSelected(budgetId!!)) -// state() -// } -// 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 -//// ) -//// } + + is CategoryAction.CancelEditCategory -> state().copy(editingCategory = false) + + is CategoryAction.NewCategoryClicked -> state().copy(editingCategory = true) + + is CategoryAction.CreateCategory -> { + val currentState = state() + val budgetId = requireNotNull(currentState.selectedBudget) + launch { + val category = categoryRepository.create( + Category( + title = action.title, + description = action.description, + amount = action.amount, + budgetId = budgetId + ) + ) + dispatch(CategoryAction.SaveCategorySuccess(category)) + } + currentState.copy(loading = true) + } + + is CategoryAction.SaveCategorySuccess -> { + val currentState = state() + val categories = currentState.categories?.toMutableList() ?: mutableListOf() + categories.replace(action.category) + categories.sortBy { it.title } + currentState.copy( + loading = false, + categories = categories.toList(), + selectedCategory = action.category.id, + editingCategory = false, + route = Route.Categories(action.category.id) + ) + } + + is CategoryAction.EditCategory -> state().copy( + editingCategory = true, + selectedCategory = action.id + ) + + is CategoryAction.UpdateCategory -> { + launch { + val oldCategory = categoryRepository.findById(action.id) + val category = categoryRepository.update( + oldCategory.copy( + title = action.title, + description = action.description, + amount = action.amount + ) + ) + dispatch(CategoryAction.SaveCategorySuccess(category)) + } + state().copy(loading = true) + } + else -> state() } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/Transaction.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/Transaction.kt index 17e1787..d9f06c7 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/Transaction.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/Transaction.kt @@ -1,5 +1,6 @@ package com.wbrawner.twigs.shared.transaction +import com.wbrawner.twigs.shared.Identifiable import kotlinx.datetime.Instant import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -11,17 +12,17 @@ import kotlinx.serialization.encoding.Encoder @Serializable data class Transaction( - val id: String? = null, - val title: String, - @Serializable(with = DateSerializer::class) - val date: Instant, - val description: String? = null, - val amount: Long, - val categoryId: String? = null, - val budgetId: String, - val expense: Boolean, - val createdBy: String -) + override val id: String? = null, + val title: String, + @Serializable(with = DateSerializer::class) + val date: Instant, + val description: String? = null, + val amount: Long, + val categoryId: String? = null, + val budgetId: String, + val expense: Boolean, + val createdBy: String +) : Identifiable @Serializable data class BalanceResponse(val balance: Long) diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt index 240fae6..e0520ad 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt @@ -7,6 +7,7 @@ import com.wbrawner.twigs.shared.State import com.wbrawner.twigs.shared.budget.Budget import com.wbrawner.twigs.shared.budget.BudgetAction 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.User import com.wbrawner.twigs.shared.user.UserPermission @@ -144,8 +145,7 @@ class TransactionReducer( is TransactionAction.SaveTransactionSuccess -> { val currentState = state() val transactions = currentState.transactions?.toMutableList() ?: mutableListOf() - transactions.removeAll { it.id == action.transaction.id } - transactions.add(action.transaction) + transactions.replace(action.transaction) transactions.sortByDescending { it.date } currentState.copy( loading = false,