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()