Implement editing of recurring transactions
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
badafaffc7
commit
6e32b5f6a3
8 changed files with 480 additions and 33 deletions
|
@ -78,7 +78,7 @@ fun RecurringTransactionDetailsScreen(store: Store) {
|
||||||
budget = budget,
|
budget = budget,
|
||||||
createdBy = createdBy
|
createdBy = createdBy
|
||||||
)
|
)
|
||||||
if (state.editingTransaction) {
|
if (state.editingRecurringTransaction) {
|
||||||
RecurringTransactionFormDialog(store = store)
|
RecurringTransactionFormDialog(store = store)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,15 +32,12 @@ import com.wbrawner.budget.ui.base.TwigsApp
|
||||||
import com.wbrawner.budget.ui.transaction.toDecimalString
|
import com.wbrawner.budget.ui.transaction.toDecimalString
|
||||||
import com.wbrawner.budget.ui.util.DatePicker
|
import com.wbrawner.budget.ui.util.DatePicker
|
||||||
import com.wbrawner.budget.ui.util.FrequencyPicker
|
import com.wbrawner.budget.ui.util.FrequencyPicker
|
||||||
import com.wbrawner.budget.ui.util.TimePicker
|
|
||||||
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.recurringtransaction.Frequency
|
import com.wbrawner.twigs.shared.recurringtransaction.Frequency
|
||||||
import com.wbrawner.twigs.shared.recurringtransaction.RecurringTransaction
|
import com.wbrawner.twigs.shared.recurringtransaction.RecurringTransaction
|
||||||
import com.wbrawner.twigs.shared.recurringtransaction.RecurringTransactionAction
|
import com.wbrawner.twigs.shared.recurringtransaction.RecurringTransactionAction
|
||||||
import com.wbrawner.twigs.shared.recurringtransaction.Time
|
import com.wbrawner.twigs.shared.recurringtransaction.Time
|
||||||
import com.wbrawner.twigs.shared.recurringtransaction.time
|
|
||||||
import com.wbrawner.twigs.shared.transaction.TransactionAction
|
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -49,7 +46,7 @@ import java.util.*
|
||||||
@Composable
|
@Composable
|
||||||
fun RecurringTransactionFormDialog(store: Store) {
|
fun RecurringTransactionFormDialog(store: Store) {
|
||||||
Dialog(
|
Dialog(
|
||||||
onDismissRequest = { store.dispatch(TransactionAction.CancelEditTransaction) },
|
onDismissRequest = { store.dispatch(RecurringTransactionAction.CancelEditRecurringTransaction) },
|
||||||
properties = DialogProperties(
|
properties = DialogProperties(
|
||||||
usePlatformDefaultWidth = false,
|
usePlatformDefaultWidth = false,
|
||||||
decorFitsSystemWindows = false
|
decorFitsSystemWindows = false
|
||||||
|
@ -94,7 +91,7 @@ fun RecurringTransactionForm(store: Store) {
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { store.dispatch(TransactionAction.CancelEditTransaction) }) {
|
IconButton(onClick = { store.dispatch(RecurringTransactionAction.CancelEditRecurringTransaction) }) {
|
||||||
Icon(Icons.Default.Close, "Cancel")
|
Icon(Icons.Default.Close, "Cancel")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -267,24 +264,66 @@ fun RecurringTransactionForm(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
FrequencyPicker(frequency, setFrequency)
|
FrequencyPicker(frequency, setFrequency)
|
||||||
val (datePickerVisible, setDatePickerVisible) = remember { mutableStateOf(false) }
|
val (startPickerVisible, setStartPickerVisible) = remember { mutableStateOf(false) }
|
||||||
DatePicker(
|
DatePicker(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
date = start,
|
date = start,
|
||||||
setDate = setStart,
|
setDate = setStart,
|
||||||
dialogVisible = datePickerVisible,
|
label = "Start Date",
|
||||||
setDialogVisible = setDatePickerVisible
|
dialogVisible = startPickerVisible,
|
||||||
)
|
setDialogVisible = setStartPickerVisible
|
||||||
val (timePickerVisible, setTimePickerVisible) = remember { mutableStateOf(false) }
|
|
||||||
TimePicker(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
time = start.time(),
|
|
||||||
setTime = {
|
|
||||||
|
|
||||||
},
|
|
||||||
dialogVisible = timePickerVisible,
|
|
||||||
setDialogVisible = setTimePickerVisible
|
|
||||||
)
|
)
|
||||||
|
val (endExpanded, setEndExpanded) = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
expanded = endExpanded,
|
||||||
|
onExpandedChange = setEndExpanded,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
value = end?.let { "On Date" } ?: "Never",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = {
|
||||||
|
Text("End Criteria")
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = endExpanded)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = endExpanded, onDismissRequest = {
|
||||||
|
setEndExpanded(false)
|
||||||
|
}) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Never") },
|
||||||
|
onClick = {
|
||||||
|
setEnd(null)
|
||||||
|
setEndExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("On Date") },
|
||||||
|
onClick = {
|
||||||
|
setEnd(Clock.System.now())
|
||||||
|
setEndExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end?.let {
|
||||||
|
val (endPickerVisible, setEndPickerVisible) = remember { mutableStateOf(false) }
|
||||||
|
DatePicker(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
date = end,
|
||||||
|
setDate = setEnd,
|
||||||
|
label = "End Date",
|
||||||
|
dialogVisible = endPickerVisible,
|
||||||
|
setDialogVisible = setEndPickerVisible
|
||||||
|
)
|
||||||
|
}
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = spacedBy(8.dp)) {
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = spacedBy(8.dp)) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
|
|
|
@ -37,10 +37,9 @@ fun RecurringTransactionsScreen(store: Store) {
|
||||||
TwigsScaffold(
|
TwigsScaffold(
|
||||||
store = store,
|
store = store,
|
||||||
title = "Recurring Transactions",
|
title = "Recurring Transactions",
|
||||||
// TODO: Implement RecurringTransaction creation/editing
|
onClickFab = {
|
||||||
// onClickFab = {
|
store.dispatch(RecurringTransactionAction.NewRecurringTransactionClicked)
|
||||||
// store.dispatch(RecurringTransactionAction.NewRecurringTransactionClicked)
|
}
|
||||||
// }
|
|
||||||
) {
|
) {
|
||||||
val state by store.state.collectAsState()
|
val state by store.state.collectAsState()
|
||||||
state.recurringTransactions?.let { transactions ->
|
state.recurringTransactions?.let { transactions ->
|
||||||
|
@ -78,9 +77,9 @@ fun RecurringTransactionsScreen(store: Store) {
|
||||||
} ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
} ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
// if (state.editingTransaction) {
|
if (state.editingRecurringTransaction) {
|
||||||
// RecurringTransactionFormDialog(store = store)
|
RecurringTransactionFormDialog(store = store)
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ fun DatePicker(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
date: Instant,
|
date: Instant,
|
||||||
setDate: (Instant) -> Unit,
|
setDate: (Instant) -> Unit,
|
||||||
|
label: String = "Date",
|
||||||
dialogVisible: Boolean,
|
dialogVisible: Boolean,
|
||||||
setDialogVisible: (Boolean) -> Unit
|
setDialogVisible: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
|
@ -52,7 +53,7 @@ fun DatePicker(
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
label = {
|
label = {
|
||||||
Text("Date")
|
Text(label)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val dialog = remember {
|
val dialog = remember {
|
||||||
|
|
|
@ -1,9 +1,398 @@
|
||||||
package com.wbrawner.budget.ui.util
|
package com.wbrawner.budget.ui.util
|
||||||
|
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.InputChip
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.wbrawner.twigs.shared.recurringtransaction.DayOfMonth
|
||||||
|
import com.wbrawner.twigs.shared.recurringtransaction.DayOfYear
|
||||||
import com.wbrawner.twigs.shared.recurringtransaction.Frequency
|
import com.wbrawner.twigs.shared.recurringtransaction.Frequency
|
||||||
|
import com.wbrawner.twigs.shared.recurringtransaction.Ordinal
|
||||||
|
import com.wbrawner.twigs.shared.recurringtransaction.capitalizedName
|
||||||
|
import com.wbrawner.twigs.shared.recurringtransaction.toMonth
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.DayOfWeek
|
||||||
|
import kotlinx.datetime.Month
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FrequencyPicker(frequency: Frequency, setFrequency: (Frequency) -> Unit) {
|
fun FrequencyPicker(frequency: Frequency, setFrequency: (Frequency) -> Unit) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
value = frequency.count.toString(),
|
||||||
|
onValueChange = { setFrequency(frequency.update(count = it.toInt())) },
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
label = { Text("Repeat Every") },
|
||||||
|
)
|
||||||
|
|
||||||
|
val (unitExpanded, setUnitExpanded) = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
expanded = unitExpanded,
|
||||||
|
onExpandedChange = setUnitExpanded,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
value = frequency.name,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = {
|
||||||
|
Text("Time Unit")
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = unitExpanded)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = unitExpanded, onDismissRequest = {
|
||||||
|
setUnitExpanded(false)
|
||||||
|
}) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Daily") },
|
||||||
|
onClick = {
|
||||||
|
setFrequency(Frequency.Daily(frequency.count, frequency.time))
|
||||||
|
setUnitExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Weekly") },
|
||||||
|
onClick = {
|
||||||
|
setFrequency(Frequency.Weekly(frequency.count, setOf(), frequency.time))
|
||||||
|
setUnitExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Monthly") },
|
||||||
|
onClick = {
|
||||||
|
setFrequency(
|
||||||
|
Frequency.Monthly(
|
||||||
|
frequency.count,
|
||||||
|
DayOfMonth.FixedDayOfMonth(
|
||||||
|
Clock.System.now().toLocalDateTime(
|
||||||
|
TimeZone.UTC
|
||||||
|
).dayOfMonth
|
||||||
|
),
|
||||||
|
frequency.time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setUnitExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Yearly") },
|
||||||
|
onClick = {
|
||||||
|
val today = Clock.System.now().toLocalDateTime(
|
||||||
|
TimeZone.UTC
|
||||||
|
)
|
||||||
|
setFrequency(
|
||||||
|
Frequency.Yearly(
|
||||||
|
frequency.count,
|
||||||
|
DayOfYear.of(today.monthNumber, today.dayOfMonth),
|
||||||
|
frequency.time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setUnitExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (frequency) {
|
||||||
|
is Frequency.Daily -> {
|
||||||
|
// No additional config needed
|
||||||
|
}
|
||||||
|
|
||||||
|
is Frequency.Weekly -> {
|
||||||
|
WeeklyFrequencyPicker(frequency, setFrequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Frequency.Monthly -> {
|
||||||
|
MonthlyFrequencyPicker(frequency, setFrequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Frequency.Yearly -> {
|
||||||
|
YearlyFrequencyPicker(frequency, setFrequency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun WeeklyFrequencyPicker(frequency: Frequency.Weekly, setFrequency: (Frequency) -> Unit) {
|
||||||
|
val daysOfWeek = remember { DayOfWeek.values() }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
daysOfWeek.forEach {
|
||||||
|
val label = remember(it) { it.getDisplayName(TextStyle.SHORT, Locale.getDefault()) }
|
||||||
|
InputChip(
|
||||||
|
selected = frequency.daysOfWeek.contains(it),
|
||||||
|
onClick = {
|
||||||
|
val selection = frequency.daysOfWeek.toMutableSet()
|
||||||
|
if (selection.contains(it)) {
|
||||||
|
selection.remove(it)
|
||||||
|
} else {
|
||||||
|
selection.add(it)
|
||||||
|
}
|
||||||
|
setFrequency(frequency.copy(daysOfWeek = selection))
|
||||||
|
},
|
||||||
|
label = { Text(label) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MonthlyFrequencyPicker(frequency: Frequency.Monthly, setFrequency: (Frequency) -> Unit) {
|
||||||
|
val (fixedDay, setFixedDay) = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
(frequency.dayOfMonth as? DayOfMonth.FixedDayOfMonth)?.day ?: 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val (ordinal, setOrdinal) = remember { mutableStateOf((frequency.dayOfMonth as? DayOfMonth.OrdinalDayOfMonth)?.ordinal) }
|
||||||
|
val (dayOfWeek, setDayOfWeek) = remember {
|
||||||
|
mutableStateOf(
|
||||||
|
(frequency.dayOfMonth as? DayOfMonth.OrdinalDayOfMonth)?.dayOfWeek ?: DayOfWeek.SUNDAY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
val (ordinalExpanded, setOrdinalExpanded) = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
expanded = ordinalExpanded,
|
||||||
|
onExpandedChange = setOrdinalExpanded,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
value = ordinal?.capitalizedName ?: "Day",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = ordinalExpanded)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = ordinalExpanded, onDismissRequest = {
|
||||||
|
setOrdinalExpanded(false)
|
||||||
|
}) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Day") },
|
||||||
|
onClick = {
|
||||||
|
setOrdinal(null)
|
||||||
|
setFrequency(
|
||||||
|
frequency.copy(
|
||||||
|
dayOfMonth = DayOfMonth.FixedDayOfMonth(
|
||||||
|
fixedDay
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setOrdinalExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Ordinal.values().forEach { ordinal ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(ordinal.capitalizedName) },
|
||||||
|
onClick = {
|
||||||
|
setOrdinal(ordinal)
|
||||||
|
setFrequency(
|
||||||
|
frequency.copy(
|
||||||
|
dayOfMonth = DayOfMonth.OrdinalDayOfMonth(
|
||||||
|
ordinal,
|
||||||
|
dayOfWeek
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setOrdinalExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
val (dayExpanded, setDayExpanded) = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
expanded = dayExpanded,
|
||||||
|
onExpandedChange = setDayExpanded,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
value = ordinal?.let { dayOfWeek.capitalizedName } ?: fixedDay.toString(),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dayExpanded)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = dayExpanded, onDismissRequest = {
|
||||||
|
setDayExpanded(false)
|
||||||
|
}) {
|
||||||
|
if (ordinal == null) {
|
||||||
|
for (day in 1..31) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(day.toString()) },
|
||||||
|
onClick = {
|
||||||
|
setFixedDay(day)
|
||||||
|
setFrequency(
|
||||||
|
frequency.copy(
|
||||||
|
dayOfMonth = DayOfMonth.FixedDayOfMonth(
|
||||||
|
day
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setDayExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DayOfWeek.values().forEach { dayOfWeek ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(dayOfWeek.capitalizedName) },
|
||||||
|
onClick = {
|
||||||
|
setDayOfWeek(dayOfWeek)
|
||||||
|
setFrequency(
|
||||||
|
frequency.copy(
|
||||||
|
dayOfMonth = (frequency.dayOfMonth as DayOfMonth.OrdinalDayOfMonth).copy(
|
||||||
|
dayOfWeek = dayOfWeek
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setDayExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun YearlyFrequencyPicker(frequency: Frequency.Yearly, setFrequency: (Frequency) -> Unit) {
|
||||||
|
val (month, setMonth) = remember { mutableStateOf(frequency.dayOfYear.month) }
|
||||||
|
val (day, setDay) = remember { mutableStateOf(frequency.dayOfYear.day) }
|
||||||
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
val (monthExpanded, setMonthExpanded) = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
expanded = monthExpanded,
|
||||||
|
onExpandedChange = setMonthExpanded,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
value = Month.of(month).getDisplayName(TextStyle.FULL, Locale.getDefault()),
|
||||||
|
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 = {
|
||||||
|
setMonth(m.value)
|
||||||
|
setFrequency(
|
||||||
|
frequency.copy(
|
||||||
|
dayOfYear = DayOfYear.of(m.value, min(day, m.maxLength()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setMonthExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
val (dayExpanded, setDayExpanded) = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
expanded = dayExpanded,
|
||||||
|
onExpandedChange = setDayExpanded,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor(),
|
||||||
|
value = day.toString(),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dayExpanded)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(expanded = dayExpanded, onDismissRequest = {
|
||||||
|
setDayExpanded(false)
|
||||||
|
}) {
|
||||||
|
for (d in 1..month.toMonth().maxLength()) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(d.toString()) },
|
||||||
|
onClick = {
|
||||||
|
setDay(d)
|
||||||
|
setFrequency(frequency.copy(dayOfYear = DayOfYear.of(month, d)))
|
||||||
|
setDayExpanded(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -16,7 +16,7 @@ kotlinx-datetime = "0.4.0"
|
||||||
ktor = "2.1.2"
|
ktor = "2.1.2"
|
||||||
material = "1.3.0"
|
material = "1.3.0"
|
||||||
maxSdk = "33"
|
maxSdk = "33"
|
||||||
minSdk = "23"
|
minSdk = "26"
|
||||||
navigation = "2.4.1"
|
navigation = "2.4.1"
|
||||||
okhttp = "4.2.2"
|
okhttp = "4.2.2"
|
||||||
settings = "0.8.1"
|
settings = "0.8.1"
|
||||||
|
|
|
@ -4,6 +4,7 @@ import com.wbrawner.twigs.shared.startOfMonth
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.datetime.Month
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
|
@ -33,9 +34,11 @@ data class RecurringTransaction(
|
||||||
sealed class Frequency {
|
sealed class Frequency {
|
||||||
abstract val count: Int
|
abstract val count: Int
|
||||||
abstract val time: Time
|
abstract val time: Time
|
||||||
|
abstract val name: String
|
||||||
|
|
||||||
data class Daily(override val count: Int, override val time: Time) : Frequency() {
|
data class Daily(override val count: Int, override val time: Time) : Frequency() {
|
||||||
override fun toString(): String = "D;$count;$time"
|
override fun toString(): String = "D;$count;$time"
|
||||||
|
override val name: String = "Daily"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(s: String): Daily {
|
fun parse(s: String): Daily {
|
||||||
|
@ -56,6 +59,7 @@ sealed class Frequency {
|
||||||
override val time: Time
|
override val time: Time
|
||||||
) : Frequency() {
|
) : Frequency() {
|
||||||
override fun toString(): String = "W;$count;${daysOfWeek.joinToString(",")};$time"
|
override fun toString(): String = "W;$count;${daysOfWeek.joinToString(",")};$time"
|
||||||
|
override val name: String = "Weekly"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(s: String): Weekly {
|
fun parse(s: String): Weekly {
|
||||||
|
@ -77,6 +81,7 @@ sealed class Frequency {
|
||||||
override val time: Time
|
override val time: Time
|
||||||
) : Frequency() {
|
) : Frequency() {
|
||||||
override fun toString(): String = "M;$count;$dayOfMonth;$time"
|
override fun toString(): String = "M;$count;$dayOfMonth;$time"
|
||||||
|
override val name: String = "Monthly"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(s: String): Monthly {
|
fun parse(s: String): Monthly {
|
||||||
|
@ -97,6 +102,8 @@ sealed class Frequency {
|
||||||
override fun toString(): String =
|
override fun toString(): String =
|
||||||
"Y;$count;${dayOfYear.month.padStart(2, '0')}-${dayOfYear.day.padStart(2, '0')};$time"
|
"Y;$count;${dayOfYear.month.padStart(2, '0')}-${dayOfYear.day.padStart(2, '0')};$time"
|
||||||
|
|
||||||
|
override val name: String = "Yearly"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(s: String): Yearly {
|
fun parse(s: String): Yearly {
|
||||||
require(s[0] == 'Y') { "Invalid format for Yearly: $s" }
|
require(s[0] == 'Y') { "Invalid format for Yearly: $s" }
|
||||||
|
@ -114,6 +121,13 @@ sealed class Frequency {
|
||||||
fun instant(now: Instant): Instant =
|
fun instant(now: Instant): Instant =
|
||||||
Instant.parse(now.toString().split("T")[0] + "T" + time.toString() + "Z")
|
Instant.parse(now.toString().split("T")[0] + "T" + time.toString() + "Z")
|
||||||
|
|
||||||
|
fun update(count: Int = this.count, time: Time = this.time): Frequency = when (this) {
|
||||||
|
is Daily -> copy(count = count, time = time)
|
||||||
|
is Weekly -> copy(count = count, time = time)
|
||||||
|
is Monthly -> copy(count = count, time = time)
|
||||||
|
is Yearly -> copy(count = count, time = time)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(s: String): Frequency = when (s[0]) {
|
fun parse(s: String): Frequency = when (s[0]) {
|
||||||
'D' -> Daily.parse(s)
|
'D' -> Daily.parse(s)
|
||||||
|
@ -160,6 +174,9 @@ enum class Ordinal {
|
||||||
LAST
|
LAST
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Enum<*>.capitalizedName: String
|
||||||
|
get() = name.lowercase().replaceFirstChar { it.uppercaseChar() }
|
||||||
|
|
||||||
class DayOfYear private constructor(val month: Int, val day: Int) {
|
class DayOfYear private constructor(val month: Int, val day: Int) {
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
@ -186,6 +203,8 @@ class DayOfYear private constructor(val month: Int, val day: Int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Int.toMonth(): Month = Month(this)
|
||||||
|
|
||||||
data class Time(val hours: Int, val minutes: Int, val seconds: Int) {
|
data class Time(val hours: Int, val minutes: Int, val seconds: Int) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val s = StringBuilder()
|
val s = StringBuilder()
|
||||||
|
|
|
@ -81,7 +81,7 @@ class RecurringTransactionReducer(
|
||||||
is Action.Back -> {
|
is Action.Back -> {
|
||||||
val currentState = state()
|
val currentState = state()
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
editingTransaction = false,
|
editingRecurringTransaction = false,
|
||||||
selectedRecurringTransaction = if (currentState.editingRecurringTransaction) currentState.selectedRecurringTransaction else null,
|
selectedRecurringTransaction = if (currentState.editingRecurringTransaction) currentState.selectedRecurringTransaction else null,
|
||||||
route = if (currentState.route is Route.RecurringTransactions && !currentState.route.selected.isNullOrBlank() && !currentState.editingRecurringTransaction) {
|
route = if (currentState.route is Route.RecurringTransactions && !currentState.route.selected.isNullOrBlank() && !currentState.editingRecurringTransaction) {
|
||||||
Route.RecurringTransactions()
|
Route.RecurringTransactions()
|
||||||
|
@ -113,7 +113,7 @@ class RecurringTransactionReducer(
|
||||||
is RecurringTransactionAction.CancelEditRecurringTransaction -> {
|
is RecurringTransactionAction.CancelEditRecurringTransaction -> {
|
||||||
val currentState = state()
|
val currentState = state()
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
editingTransaction = false,
|
editingRecurringTransaction = false,
|
||||||
selectedRecurringTransaction = if (currentState.route is Route.RecurringTransactions && !currentState.route.selected.isNullOrBlank()) {
|
selectedRecurringTransaction = if (currentState.route is Route.RecurringTransactions && !currentState.route.selected.isNullOrBlank()) {
|
||||||
currentState.selectedRecurringTransaction
|
currentState.selectedRecurringTransaction
|
||||||
} else {
|
} else {
|
||||||
|
@ -178,7 +178,7 @@ class RecurringTransactionReducer(
|
||||||
recurringTransactions = transactions.toList(),
|
recurringTransactions = transactions.toList(),
|
||||||
selectedRecurringTransaction = action.transaction.id,
|
selectedRecurringTransaction = action.transaction.id,
|
||||||
selectedRecurringTransactionCreatedBy = currentState.user,
|
selectedRecurringTransactionCreatedBy = currentState.user,
|
||||||
editingTransaction = false
|
editingRecurringTransaction = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,11 +202,11 @@ class RecurringTransactionReducer(
|
||||||
is ConfigAction.Logout -> state().copy(
|
is ConfigAction.Logout -> state().copy(
|
||||||
transactions = null,
|
transactions = null,
|
||||||
selectedRecurringTransaction = null,
|
selectedRecurringTransaction = null,
|
||||||
editingTransaction = false
|
editingRecurringTransaction = false
|
||||||
)
|
)
|
||||||
|
|
||||||
is RecurringTransactionAction.EditRecurringTransaction -> state().copy(
|
is RecurringTransactionAction.EditRecurringTransaction -> state().copy(
|
||||||
editingTransaction = true,
|
editingRecurringTransaction = true,
|
||||||
selectedRecurringTransaction = action.id
|
selectedRecurringTransaction = action.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue