Add ability to create/edit categories
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
7b1b088080
commit
badafaffc7
12 changed files with 395 additions and 128 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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 ""
|
||||
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 {
|
||||
""
|
||||
}
|
|
@ -16,3 +16,11 @@ interface Repository<T> {
|
|||
interface Identifiable {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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<UserPermission> = emptyList()
|
||||
)
|
||||
) : Identifiable
|
||||
|
||||
data class NewBudgetRequest(
|
||||
val name: String,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
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
|
|
@ -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<Category>) : 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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue