Work on Transaction form

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2023-01-22 21:17:17 -07:00
parent 95e7361907
commit 477e311a5e
8 changed files with 160 additions and 24 deletions

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.wbrawner.budget.ui.base.TwigsApp import com.wbrawner.budget.ui.base.TwigsApp
import com.wbrawner.budget.ui.util.DatePicker import com.wbrawner.budget.ui.util.DatePicker
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.transaction.Transaction import com.wbrawner.twigs.shared.transaction.Transaction
@ -251,6 +252,14 @@ fun TransactionForm(
dialogVisible = datePickerVisible, dialogVisible = datePickerVisible,
setDialogVisible = setDatePickerVisible setDialogVisible = setDatePickerVisible
) )
val (timePickerVisible, setTimePickerVisible) = remember { mutableStateOf(false) }
TimePicker(
modifier = Modifier.fillMaxWidth(),
date = date,
setDate = setDate,
dialogVisible = timePickerVisible,
setDialogVisible = setTimePickerVisible
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = spacedBy(8.dp)) {
Row( Row(
modifier = Modifier.clickable { modifier = Modifier.clickable {

View file

@ -6,9 +6,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material3.Divider import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -51,29 +50,25 @@ fun TransactionsScreen(store: Store) {
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(it) .padding(it)
.padding(horizontal = 8.dp)
) { ) {
transactionGroups.forEach { (timestamp, transactions) -> transactionGroups.forEach { (timestamp, transactions) ->
item(timestamp) { item(timestamp) {
Text( Text(
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(8.dp),
text = timestamp.toInstant().format(LocalContext.current), text = timestamp.toInstant().format(LocalContext.current),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
itemsIndexed(transactions) { i, transaction -> item(transactions) {
TransactionListItem(transaction) { Card {
store.dispatch(TransactionAction.SelectTransaction(transaction.id)) transactions.forEach { transaction ->
TransactionListItem(transaction) {
store.dispatch(TransactionAction.SelectTransaction(transaction.id))
}
}
} }
if (i != transactions.lastIndex) {
Divider(modifier = Modifier.padding(horizontal = 8.dp))
}
}
item {
Spacer(
Modifier
.fillMaxWidth()
.height(32.dp))
} }
} }
} }

View file

@ -21,7 +21,11 @@ import androidx.fragment.app.FragmentManager
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.wbrawner.budget.R import com.wbrawner.budget.R
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import java.util.TimeZone import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -52,13 +56,19 @@ fun DatePicker(
} }
) )
val dialog = remember { val dialog = remember {
val localTime = date.toLocalDateTime(TimeZone.UTC).time
MaterialDatePicker.Builder.datePicker() MaterialDatePicker.Builder.datePicker()
.setSelection(date.toEpochMilliseconds()) .setSelection(date.toEpochMilliseconds())
.setTheme(R.style.DateTimePickerDialogTheme) .setTheme(R.style.DateTimePickerDialogTheme)
.build() .build()
.also { picker -> .also { picker ->
picker.addOnPositiveButtonClickListener { picker.addOnPositiveButtonClickListener {
setDate(Instant.fromEpochMilliseconds(it)) setDate(
LocalDateTime(
Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date,
localTime
).toInstant(TimeZone.UTC)
)
} }
picker.addOnDismissListener { picker.addOnDismissListener {
setDialogVisible(false) setDialogVisible(false)
@ -93,9 +103,7 @@ val Context.fragmentManager: FragmentManager?
fun Instant.format(context: Context): String = fun Instant.format(context: Context): String =
DateFormat.getDateFormat(context) DateFormat.getDateFormat(context)
.format(this.toEpochMilliseconds() - TimeZone.getDefault().rawOffset).also { .format(
Log.d( this.toLocalDateTime(TimeZone.currentSystemDefault()).date.atStartOfDayIn(TimeZone.currentSystemDefault())
"DatePicker", .toEpochMilliseconds()
"offset: ${TimeZone.getDefault().rawOffset} adjusted time: ${this.toEpochMilliseconds() - TimeZone.getDefault().rawOffset}"
) )
}

View file

@ -0,0 +1,90 @@
package com.wbrawner.budget.ui.util
import android.content.Context
import android.text.format.DateFormat
import androidx.compose.foundation.clickable
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.timepicker.MaterialTimePicker
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePicker(
modifier: Modifier,
date: Instant,
setDate: (Instant) -> Unit,
dialogVisible: Boolean,
setDialogVisible: (Boolean) -> Unit
) {
val context = LocalContext.current
OutlinedTextField(
modifier = modifier
.clickable {
setDialogVisible(true)
}
.focusRequester(FocusRequester())
.onFocusChanged {
setDialogVisible(it.hasFocus)
},
value = date.formatTime(context),
onValueChange = {},
readOnly = true,
label = {
Text("Time")
}
)
val dialog = remember {
val localTime = date.toLocalDateTime(TimeZone.currentSystemDefault())
MaterialTimePicker.Builder()
.setHour(localTime.hour)
.setMinute(localTime.minute)
.build()
.also { picker ->
picker.addOnPositiveButtonClickListener {
setDate(
LocalDateTime(
localTime.date,
LocalTime(picker.hour, picker.minute)
).toInstant(TimeZone.UTC)
.minus(java.util.TimeZone.getDefault().rawOffset.milliseconds)
)
}
picker.addOnDismissListener {
setDialogVisible(false)
}
}
}
DisposableEffect(key1 = dialogVisible) {
if (dialogVisible) {
context.fragmentManager?.let {
dialog.show(it, null)
}
} else if (dialog.isVisible) {
dialog.dismiss()
}
onDispose {
if (dialog.isVisible) {
dialog.dismiss()
}
}
}
}
fun Instant.formatTime(context: Context): String =
DateFormat.getTimeFormat(context).format(this.toEpochMilliseconds())

View file

@ -4,7 +4,7 @@
android:width="48dp" android:width="48dp"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<path <path
android:fillColor="#ffffff" android:fillColor="#FFffffff"
android:pathData="M74.798,72.259L74.798,87.183L74.798,94.176C74.798,117.78 93.703,137.158 117.127,137.925L117.127,137.952L124.59,137.952L126.035,137.952L128.006,137.952L128.006,141.826L128.006,156.282L128.006,167.749C128.006,177.634 132.744,186.941 140.737,192.757C148.729,198.573 159.042,200.217 168.448,197.177L163.857,182.976C158.98,184.553 153.662,183.704 149.519,180.688C145.374,177.672 142.931,172.874 142.931,167.749L142.931,156.282L146.342,156.282L152.361,156.282L153.806,156.282L153.806,156.259C177.231,155.491 196.136,136.113 196.136,112.509L196.136,105.046L196.136,90.59L188.673,90.59L179.243,90.59L177.798,90.59L170.336,90.59L170.336,90.614C158.944,90.988 148.622,95.762 141.023,103.284C135.669,85.812 119.624,72.91 100.596,72.286L100.596,72.262L99.153,72.262L93.134,72.262L82.261,72.262L74.798,72.259zM171.781,105.512L177.798,105.512L179.243,105.512L181.211,105.512L181.211,112.506C181.211,128.527 168.382,141.355 152.361,141.355L146.342,141.355L142.931,141.355L142.931,134.363C142.931,118.342 155.759,105.512 171.781,105.512z" android:pathData="M74.798,72.259L74.798,87.183L74.798,94.176C74.798,117.78 93.703,137.158 117.127,137.925L117.127,137.952L124.59,137.952L126.035,137.952L128.006,137.952L128.006,141.826L128.006,156.282L128.006,167.749C128.006,177.634 132.744,186.941 140.737,192.757C148.729,198.573 159.042,200.217 168.448,197.177L163.857,182.976C158.98,184.553 153.662,183.704 149.519,180.688C145.374,177.672 142.931,172.874 142.931,167.749L142.931,156.282L146.342,156.282L152.361,156.282L153.806,156.282L153.806,156.259C177.231,155.491 196.136,136.113 196.136,112.509L196.136,105.046L196.136,90.59L188.673,90.59L179.243,90.59L177.798,90.59L170.336,90.59L170.336,90.614C158.944,90.988 148.622,95.762 141.023,103.284C135.669,85.812 119.624,72.91 100.596,72.286L100.596,72.262L99.153,72.262L93.134,72.262L82.261,72.262L74.798,72.259zM171.781,105.512L177.798,105.512L179.243,105.512L181.211,105.512L181.211,112.506C181.211,128.527 168.382,141.355 152.361,141.355L146.342,141.355L142.931,141.355L142.931,134.363C142.931,118.342 155.759,105.512 171.781,105.512z"
android:strokeWidth="2.9615941" /> android:strokeWidth="2.9615941" />
</vector> </vector>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" /> <background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_twigs_outline" />
</adaptive-icon> </adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background" /> <background android:drawable="@mipmap/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_twigs_outline" />
</adaptive-icon> </adaptive-icon>

View file

@ -89,7 +89,18 @@ class TransactionReducer(
is TransactionAction.TransactionsClicked -> state().copy(route = Route.Transactions(null)) is TransactionAction.TransactionsClicked -> state().copy(route = Route.Transactions(null))
is TransactionAction.LoadTransactionsSuccess -> state().copy(transactions = action.transactions) is TransactionAction.LoadTransactionsSuccess -> state().copy(transactions = action.transactions)
is TransactionAction.NewTransactionClicked -> state().copy(editingTransaction = true) is TransactionAction.NewTransactionClicked -> state().copy(editingTransaction = true)
is TransactionAction.CancelEditTransaction -> state().copy(editingTransaction = false) is TransactionAction.CancelEditTransaction -> {
val currentState = state()
currentState.copy(
editingTransaction = false,
selectedTransaction = if (currentState.route is Route.Transactions && !currentState.route.selected.isNullOrBlank()) {
currentState.selectedTransaction
} else {
null
}
)
}
is TransactionAction.CreateTransaction -> { is TransactionAction.CreateTransaction -> {
launch { launch {
val transaction = transactionRepository.create( val transaction = transactionRepository.create(
@ -109,6 +120,27 @@ class TransactionReducer(
state().copy(loading = true) state().copy(loading = true)
} }
is TransactionAction.UpdateTransaction -> {
val createdBy = state().selectedTransactionCreatedBy!!
launch {
val transaction = transactionRepository.update(
Transaction(
id = action.id,
title = action.title,
description = action.description,
amount = action.amount,
date = action.date,
expense = action.expense,
categoryId = action.category?.id,
budgetId = action.budget.id!!,
createdBy = createdBy.id!!
)
)
dispatch(TransactionAction.SaveTransactionSuccess(transaction))
}
state().copy(loading = true)
}
is TransactionAction.SaveTransactionSuccess -> { is TransactionAction.SaveTransactionSuccess -> {
val currentState = state() val currentState = state()
val transactions = currentState.transactions?.toMutableList() ?: mutableListOf() val transactions = currentState.transactions?.toMutableList() ?: mutableListOf()