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 com.wbrawner.budget.ui.base.TwigsApp
|
||||
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.category.Category
|
||||
import com.wbrawner.twigs.shared.transaction.Transaction
|
||||
|
@ -251,6 +252,14 @@ fun TransactionForm(
|
|||
dialogVisible = datePickerVisible,
|
||||
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.clickable {
|
||||
|
|
|
@ -6,9 +6,8 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -51,29 +50,25 @@ fun TransactionsScreen(store: Store) {
|
|||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(it)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
transactionGroups.forEach { (timestamp, transactions) ->
|
||||
item(timestamp) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
modifier = Modifier.padding(8.dp),
|
||||
text = timestamp.toInstant().format(LocalContext.current),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
itemsIndexed(transactions) { i, transaction ->
|
||||
TransactionListItem(transaction) {
|
||||
store.dispatch(TransactionAction.SelectTransaction(transaction.id))
|
||||
item(transactions) {
|
||||
Card {
|
||||
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.wbrawner.budget.R
|
||||
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)
|
||||
@Composable
|
||||
|
@ -52,13 +56,19 @@ fun DatePicker(
|
|||
}
|
||||
)
|
||||
val dialog = remember {
|
||||
val localTime = date.toLocalDateTime(TimeZone.UTC).time
|
||||
MaterialDatePicker.Builder.datePicker()
|
||||
.setSelection(date.toEpochMilliseconds())
|
||||
.setTheme(R.style.DateTimePickerDialogTheme)
|
||||
.build()
|
||||
.also { picker ->
|
||||
picker.addOnPositiveButtonClickListener {
|
||||
setDate(Instant.fromEpochMilliseconds(it))
|
||||
setDate(
|
||||
LocalDateTime(
|
||||
Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date,
|
||||
localTime
|
||||
).toInstant(TimeZone.UTC)
|
||||
)
|
||||
}
|
||||
picker.addOnDismissListener {
|
||||
setDialogVisible(false)
|
||||
|
@ -93,9 +103,7 @@ val Context.fragmentManager: FragmentManager?
|
|||
|
||||
fun Instant.format(context: Context): String =
|
||||
DateFormat.getDateFormat(context)
|
||||
.format(this.toEpochMilliseconds() - TimeZone.getDefault().rawOffset).also {
|
||||
Log.d(
|
||||
"DatePicker",
|
||||
"offset: ${TimeZone.getDefault().rawOffset} adjusted time: ${this.toEpochMilliseconds() - TimeZone.getDefault().rawOffset}"
|
||||
.format(
|
||||
this.toLocalDateTime(TimeZone.currentSystemDefault()).date.atStartOfDayIn(TimeZone.currentSystemDefault())
|
||||
.toEpochMilliseconds()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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:strokeWidth="2.9615941" />
|
||||
</vector>
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_twigs_outline" />
|
||||
</adaptive-icon>
|
|
@ -2,4 +2,5 @@
|
|||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_twigs_outline" />
|
||||
</adaptive-icon>
|
|
@ -89,7 +89,18 @@ class TransactionReducer(
|
|||
is TransactionAction.TransactionsClicked -> state().copy(route = Route.Transactions(null))
|
||||
is TransactionAction.LoadTransactionsSuccess -> state().copy(transactions = action.transactions)
|
||||
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 -> {
|
||||
launch {
|
||||
val transaction = transactionRepository.create(
|
||||
|
@ -109,6 +120,27 @@ class TransactionReducer(
|
|||
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 -> {
|
||||
val currentState = state()
|
||||
val transactions = currentState.transactions?.toMutableList() ?: mutableListOf()
|
||||
|
|
Loading…
Reference in a new issue