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,
|
||||
createdBy = createdBy
|
||||
)
|
||||
if (state.editingTransaction) {
|
||||
if (state.editingRecurringTransaction) {
|
||||
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.util.DatePicker
|
||||
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.category.Category
|
||||
import com.wbrawner.twigs.shared.recurringtransaction.Frequency
|
||||
import com.wbrawner.twigs.shared.recurringtransaction.RecurringTransaction
|
||||
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.transaction.TransactionAction
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import java.util.*
|
||||
|
@ -49,7 +46,7 @@ import java.util.*
|
|||
@Composable
|
||||
fun RecurringTransactionFormDialog(store: Store) {
|
||||
Dialog(
|
||||
onDismissRequest = { store.dispatch(TransactionAction.CancelEditTransaction) },
|
||||
onDismissRequest = { store.dispatch(RecurringTransactionAction.CancelEditRecurringTransaction) },
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false
|
||||
|
@ -94,7 +91,7 @@ fun RecurringTransactionForm(store: Store) {
|
|||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { store.dispatch(TransactionAction.CancelEditTransaction) }) {
|
||||
IconButton(onClick = { store.dispatch(RecurringTransactionAction.CancelEditRecurringTransaction) }) {
|
||||
Icon(Icons.Default.Close, "Cancel")
|
||||
}
|
||||
},
|
||||
|
@ -267,24 +264,66 @@ fun RecurringTransactionForm(
|
|||
}),
|
||||
)
|
||||
FrequencyPicker(frequency, setFrequency)
|
||||
val (datePickerVisible, setDatePickerVisible) = remember { mutableStateOf(false) }
|
||||
val (startPickerVisible, setStartPickerVisible) = remember { mutableStateOf(false) }
|
||||
DatePicker(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
date = start,
|
||||
setDate = setStart,
|
||||
dialogVisible = datePickerVisible,
|
||||
setDialogVisible = setDatePickerVisible
|
||||
)
|
||||
val (timePickerVisible, setTimePickerVisible) = remember { mutableStateOf(false) }
|
||||
TimePicker(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
time = start.time(),
|
||||
setTime = {
|
||||
|
||||
},
|
||||
dialogVisible = timePickerVisible,
|
||||
setDialogVisible = setTimePickerVisible
|
||||
label = "Start Date",
|
||||
dialogVisible = startPickerVisible,
|
||||
setDialogVisible = setStartPickerVisible
|
||||
)
|
||||
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.clickable {
|
||||
|
|
|
@ -37,10 +37,9 @@ fun RecurringTransactionsScreen(store: Store) {
|
|||
TwigsScaffold(
|
||||
store = store,
|
||||
title = "Recurring Transactions",
|
||||
// TODO: Implement RecurringTransaction creation/editing
|
||||
// onClickFab = {
|
||||
// store.dispatch(RecurringTransactionAction.NewRecurringTransactionClicked)
|
||||
// }
|
||||
onClickFab = {
|
||||
store.dispatch(RecurringTransactionAction.NewRecurringTransactionClicked)
|
||||
}
|
||||
) {
|
||||
val state by store.state.collectAsState()
|
||||
state.recurringTransactions?.let { transactions ->
|
||||
|
@ -78,9 +77,9 @@ fun RecurringTransactionsScreen(store: Store) {
|
|||
} ?: Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
// if (state.editingTransaction) {
|
||||
// RecurringTransactionFormDialog(store = store)
|
||||
// }
|
||||
if (state.editingRecurringTransaction) {
|
||||
RecurringTransactionFormDialog(store = store)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ fun DatePicker(
|
|||
modifier: Modifier,
|
||||
date: Instant,
|
||||
setDate: (Instant) -> Unit,
|
||||
label: String = "Date",
|
||||
dialogVisible: Boolean,
|
||||
setDialogVisible: (Boolean) -> Unit
|
||||
) {
|
||||
|
@ -52,7 +53,7 @@ fun DatePicker(
|
|||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = {
|
||||
Text("Date")
|
||||
Text(label)
|
||||
}
|
||||
)
|
||||
val dialog = remember {
|
||||
|
|
|
@ -1,9 +1,398 @@
|
|||
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.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.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
|
||||
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"
|
||||
material = "1.3.0"
|
||||
maxSdk = "33"
|
||||
minSdk = "23"
|
||||
minSdk = "26"
|
||||
navigation = "2.4.1"
|
||||
okhttp = "4.2.2"
|
||||
settings = "0.8.1"
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.wbrawner.twigs.shared.startOfMonth
|
|||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.serialization.KSerializer
|
||||
|
@ -33,9 +34,11 @@ data class RecurringTransaction(
|
|||
sealed class Frequency {
|
||||
abstract val count: Int
|
||||
abstract val time: Time
|
||||
abstract val name: String
|
||||
|
||||
data class Daily(override val count: Int, override val time: Time) : Frequency() {
|
||||
override fun toString(): String = "D;$count;$time"
|
||||
override val name: String = "Daily"
|
||||
|
||||
companion object {
|
||||
fun parse(s: String): Daily {
|
||||
|
@ -56,6 +59,7 @@ sealed class Frequency {
|
|||
override val time: Time
|
||||
) : Frequency() {
|
||||
override fun toString(): String = "W;$count;${daysOfWeek.joinToString(",")};$time"
|
||||
override val name: String = "Weekly"
|
||||
|
||||
companion object {
|
||||
fun parse(s: String): Weekly {
|
||||
|
@ -77,6 +81,7 @@ sealed class Frequency {
|
|||
override val time: Time
|
||||
) : Frequency() {
|
||||
override fun toString(): String = "M;$count;$dayOfMonth;$time"
|
||||
override val name: String = "Monthly"
|
||||
|
||||
companion object {
|
||||
fun parse(s: String): Monthly {
|
||||
|
@ -97,6 +102,8 @@ sealed class Frequency {
|
|||
override fun toString(): String =
|
||||
"Y;$count;${dayOfYear.month.padStart(2, '0')}-${dayOfYear.day.padStart(2, '0')};$time"
|
||||
|
||||
override val name: String = "Yearly"
|
||||
|
||||
companion object {
|
||||
fun parse(s: String): Yearly {
|
||||
require(s[0] == 'Y') { "Invalid format for Yearly: $s" }
|
||||
|
@ -114,6 +121,13 @@ sealed class Frequency {
|
|||
fun instant(now: Instant): Instant =
|
||||
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 {
|
||||
fun parse(s: String): Frequency = when (s[0]) {
|
||||
'D' -> Daily.parse(s)
|
||||
|
@ -160,6 +174,9 @@ enum class Ordinal {
|
|||
LAST
|
||||
}
|
||||
|
||||
val Enum<*>.capitalizedName: String
|
||||
get() = name.lowercase().replaceFirstChar { it.uppercaseChar() }
|
||||
|
||||
class DayOfYear private constructor(val month: Int, val day: Int) {
|
||||
|
||||
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) {
|
||||
override fun toString(): String {
|
||||
val s = StringBuilder()
|
||||
|
|
|
@ -81,7 +81,7 @@ class RecurringTransactionReducer(
|
|||
is Action.Back -> {
|
||||
val currentState = state()
|
||||
currentState.copy(
|
||||
editingTransaction = false,
|
||||
editingRecurringTransaction = false,
|
||||
selectedRecurringTransaction = if (currentState.editingRecurringTransaction) currentState.selectedRecurringTransaction else null,
|
||||
route = if (currentState.route is Route.RecurringTransactions && !currentState.route.selected.isNullOrBlank() && !currentState.editingRecurringTransaction) {
|
||||
Route.RecurringTransactions()
|
||||
|
@ -113,7 +113,7 @@ class RecurringTransactionReducer(
|
|||
is RecurringTransactionAction.CancelEditRecurringTransaction -> {
|
||||
val currentState = state()
|
||||
currentState.copy(
|
||||
editingTransaction = false,
|
||||
editingRecurringTransaction = false,
|
||||
selectedRecurringTransaction = if (currentState.route is Route.RecurringTransactions && !currentState.route.selected.isNullOrBlank()) {
|
||||
currentState.selectedRecurringTransaction
|
||||
} else {
|
||||
|
@ -178,7 +178,7 @@ class RecurringTransactionReducer(
|
|||
recurringTransactions = transactions.toList(),
|
||||
selectedRecurringTransaction = action.transaction.id,
|
||||
selectedRecurringTransactionCreatedBy = currentState.user,
|
||||
editingTransaction = false
|
||||
editingRecurringTransaction = false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -202,11 +202,11 @@ class RecurringTransactionReducer(
|
|||
is ConfigAction.Logout -> state().copy(
|
||||
transactions = null,
|
||||
selectedRecurringTransaction = null,
|
||||
editingTransaction = false
|
||||
editingRecurringTransaction = false
|
||||
)
|
||||
|
||||
is RecurringTransactionAction.EditRecurringTransaction -> state().copy(
|
||||
editingTransaction = true,
|
||||
editingRecurringTransaction = true,
|
||||
selectedRecurringTransaction = action.id
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue