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
|
@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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
@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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
""
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue