Add month selection to budget overview page
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
83e2135478
commit
31cf90dec0
2 changed files with 176 additions and 1 deletions
|
@ -1,6 +1,7 @@
|
|||
package com.wbrawner.budget.ui
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
@ -14,24 +15,42 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.wbrawner.budget.ui.base.TwigsColors
|
||||
import com.wbrawner.budget.ui.base.TwigsTheme
|
||||
import com.wbrawner.budget.ui.transaction.toCurrencyString
|
||||
import com.wbrawner.twigs.shared.Store
|
||||
import com.wbrawner.twigs.shared.budget.BudgetAction
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
@ -68,6 +87,23 @@ fun OverviewScreen(store: Store) {
|
|||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val month =
|
||||
remember(state.from) { state.from.toLocalDateTime(TimeZone.UTC).month }
|
||||
val year =
|
||||
remember(state.from) { state.from.toLocalDateTime(TimeZone.UTC).year }
|
||||
var showMonthPicker by remember { mutableStateOf(false) }
|
||||
LabeledField(
|
||||
modifier = Modifier.clickable {
|
||||
showMonthPicker = true
|
||||
},
|
||||
label = "Month",
|
||||
value = "${
|
||||
month.getDisplayName(
|
||||
TextStyle.FULL,
|
||||
LocalConfiguration.current.locales[0]
|
||||
)
|
||||
} $year"
|
||||
)
|
||||
LabeledField(
|
||||
label = "Cash Flow",
|
||||
value = state.budgetBalance?.toCurrencyString() ?: "-"
|
||||
|
@ -76,6 +112,121 @@ fun OverviewScreen(store: Store) {
|
|||
label = "Transactions",
|
||||
value = state.transactions?.size?.toString() ?: "-"
|
||||
)
|
||||
if (showMonthPicker) {
|
||||
var monthField by remember { mutableStateOf(month) }
|
||||
var yearField by remember { mutableStateOf(year.toString()) }
|
||||
var yearError by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = { showMonthPicker = false },
|
||||
confirmButton = {
|
||||
TextButton({
|
||||
if (!yearError) {
|
||||
showMonthPicker = false
|
||||
store.dispatch(
|
||||
BudgetAction.SetDateRange(
|
||||
monthField,
|
||||
yearField.toInt()
|
||||
)
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Text("Change")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton({
|
||||
showMonthPicker = false
|
||||
}) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text("Select a month to view")
|
||||
},
|
||||
text = {
|
||||
val (monthExpanded, setMonthExpanded) = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(8.dp)
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
expanded = monthExpanded,
|
||||
onExpandedChange = setMonthExpanded,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
value = month.getDisplayName(
|
||||
TextStyle.FULL,
|
||||
Locale.getDefault()
|
||||
),
|
||||
label = {
|
||||
Text("Month")
|
||||
},
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = monthExpanded)
|
||||
}
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = monthExpanded,
|
||||
onDismissRequest = {
|
||||
setMonthExpanded(false)
|
||||
}) {
|
||||
Month.values().forEach { m ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
m.getDisplayName(
|
||||
TextStyle.FULL,
|
||||
Locale.getDefault()
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
monthField = m
|
||||
setMonthExpanded(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.weight(1f),
|
||||
value = yearField,
|
||||
onValueChange = { value ->
|
||||
yearField = value
|
||||
value.toIntOrNull()?.let {
|
||||
yearError = false
|
||||
} ?: run {
|
||||
yearError = true
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
|
||||
label = {
|
||||
Text("Year")
|
||||
},
|
||||
isError = yearError,
|
||||
supportingText = {
|
||||
if (yearError) {
|
||||
Text(
|
||||
"Invalid year",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
CashFlowChart(
|
||||
|
@ -173,8 +324,9 @@ fun CashFlowProgressBar(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun LabeledField(label: String, value: String) {
|
||||
fun LabeledField(modifier: Modifier = Modifier, label: String, value: String) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = spacedBy(4.dp)
|
||||
) {
|
||||
|
|
|
@ -6,14 +6,23 @@ 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.endOfMonth
|
||||
import com.wbrawner.twigs.shared.replace
|
||||
import com.wbrawner.twigs.shared.startOfMonth
|
||||
import com.wbrawner.twigs.shared.transaction.TransactionAction
|
||||
import com.wbrawner.twigs.shared.user.ConfigAction
|
||||
import com.wbrawner.twigs.shared.user.UserPermission
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
sealed interface BudgetAction : Action {
|
||||
object OverviewClicked : BudgetAction
|
||||
data class SetDateRange(val month: Month, val year: Int) : BudgetAction
|
||||
data class LoadBudgetsSuccess(val budgets: List<Budget>) : BudgetAction
|
||||
data class LoadBudgetsFailed(val error: Exception) : BudgetAction
|
||||
data class CreateBudget(
|
||||
|
@ -81,6 +90,20 @@ class BudgetReducer(
|
|||
state().copy(loading = true)
|
||||
}
|
||||
|
||||
is BudgetAction.SetDateRange -> {
|
||||
val now = Clock.System.now().toLocalDateTime(TimeZone.UTC)
|
||||
val month = LocalDateTime(action.year, action.month, now.dayOfMonth, 0, 0)
|
||||
.toInstant(TimeZone.UTC)
|
||||
val from = startOfMonth(month)
|
||||
val to = endOfMonth(month)
|
||||
state().copy(
|
||||
from = from,
|
||||
to = to
|
||||
).also {
|
||||
dispatch(BudgetAction.BudgetSelected(it.selectedBudget!!))
|
||||
}
|
||||
}
|
||||
|
||||
is BudgetAction.SaveBudgetSuccess -> {
|
||||
val currentState = state()
|
||||
val budgets = currentState.budgets?.toMutableList() ?: mutableListOf()
|
||||
|
|
Loading…
Reference in a new issue