From 31cf90dec0e06876ba9df8737e81fbf5ccfe195f Mon Sep 17 00:00:00 2001 From: William Brawner Date: Wed, 31 May 2023 08:48:55 -0600 Subject: [PATCH] Add month selection to budget overview page Signed-off-by: William Brawner --- .../com/wbrawner/budget/ui/OverviewScreen.kt | 154 +++++++++++++++++- .../twigs/shared/budget/BudgetAction.kt | 23 +++ 2 files changed, 176 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/wbrawner/budget/ui/OverviewScreen.kt b/android/src/main/java/com/wbrawner/budget/ui/OverviewScreen.kt index b65bbea..08640a3 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/OverviewScreen.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/OverviewScreen.kt @@ -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) ) { 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 b81b4de..e6f6afa 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,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) : 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()