diff --git a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionForm.kt b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionForm.kt index 1bad149..e178dc6 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionForm.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionForm.kt @@ -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 { diff --git a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt index e34a6e3..7db9e08 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/transaction/TransactionsScreen.kt @@ -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)) } } } diff --git a/android/src/main/java/com/wbrawner/budget/ui/util/DatePicker.kt b/android/src/main/java/com/wbrawner/budget/ui/util/DatePicker.kt index c5a2832..770acb8 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/util/DatePicker.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/util/DatePicker.kt @@ -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() ) - } diff --git a/android/src/main/java/com/wbrawner/budget/ui/util/TimePicker.kt b/android/src/main/java/com/wbrawner/budget/ui/util/TimePicker.kt new file mode 100644 index 0000000..4fb05af --- /dev/null +++ b/android/src/main/java/com/wbrawner/budget/ui/util/TimePicker.kt @@ -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()) diff --git a/android/src/main/res/drawable/ic_twigs_outline.xml b/android/src/main/res/drawable/ic_twigs_outline.xml index 864e99e..cfdbf76 100644 --- a/android/src/main/res/drawable/ic_twigs_outline.xml +++ b/android/src/main/res/drawable/ic_twigs_outline.xml @@ -4,7 +4,7 @@ android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index d258ccc..4a5dfd0 100644 --- a/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index d258ccc..4a5dfd0 100644 --- a/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt index bf65ec9..26e5595 100644 --- a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/transaction/TransactionAction.kt @@ -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()