Work on Transaction form
Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
parent
95e7361907
commit
477e311a5e
8 changed files with 160 additions and 24 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}"
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
|
@ -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())
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue