diff --git a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/BankingRepository.kt b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/BankingRepository.kt index 45bebfd..4ef7a91 100644 --- a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/BankingRepository.kt +++ b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/BankingRepository.kt @@ -51,4 +51,6 @@ interface BankingRepository { fun getTransactionById(transactionId: Long): AccountTransactionEntity? + suspend fun updateTransaction(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?) + } \ No newline at end of file diff --git a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/InMemoryBankingRepository.kt b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/InMemoryBankingRepository.kt index 22d5e4b..39bd676 100644 --- a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/InMemoryBankingRepository.kt +++ b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/InMemoryBankingRepository.kt @@ -88,6 +88,10 @@ class InMemoryBankingRepository( override fun getTransactionById(transactionId: Long): AccountTransactionEntity? = getAllAccountTransactions().firstOrNull { it.id == transactionId } + override suspend fun updateTransaction(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?) { + // no-op + } + private fun map(bank: BankAccess) = BankAccessEntity( nextId++, diff --git a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/SqliteBankingRepository.kt b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/SqliteBankingRepository.kt index 191c260..953f464 100644 --- a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/SqliteBankingRepository.kt +++ b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/persistence/SqliteBankingRepository.kt @@ -441,6 +441,22 @@ open class SqliteBankingRepository : BankingRepository { return AccountTransactionEntity(getLastInsertedId(), bankId, accountId, transaction) } + override suspend fun updateTransaction(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?) { + accountTransactionQueries.transaction { + if (transaction.userSetOtherPartyName != userSetOtherPartyName) { + accountTransactionQueries.updateAccountTransactionUserSetOtherPartyName(userSetOtherPartyName, transaction.id) + } + + if (transaction.userSetReference != userSetReference) { + accountTransactionQueries.updateAccountTransactionUserSetOReference(userSetReference, transaction.id) + } + + if (transaction.notes != notes) { + accountTransactionQueries.updateAccountTransactionNotes(notes, transaction.id) + } + } + } + private fun getLastInsertedId(): Long = bankQueries.getLastInsertedId().executeAsOne() diff --git a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt index 942191f..59de75c 100644 --- a/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt +++ b/BankingPersistence/src/commonMain/kotlin/net/codinux/banking/ui/model/AccountTransactionViewModel.kt @@ -17,8 +17,8 @@ data class AccountTransactionViewModel( val otherPartyName: String? = null, val postingText: String? = null, - val userSetReference: String? = null, - val userSetOtherPartyName: String? = null + var userSetReference: String? = null, + var userSetOtherPartyName: String? = null ) { constructor(entity: AccountTransactionEntity) : this(entity.id, entity.bankId, entity.accountId, entity) diff --git a/BankingPersistence/src/commonMain/sqldelight/net/codinux/banking/persistence/AccountTransaction.sq b/BankingPersistence/src/commonMain/sqldelight/net/codinux/banking/persistence/AccountTransaction.sq index 54f6f9d..2e75cf2 100644 --- a/BankingPersistence/src/commonMain/sqldelight/net/codinux/banking/persistence/AccountTransaction.sq +++ b/BankingPersistence/src/commonMain/sqldelight/net/codinux/banking/persistence/AccountTransaction.sq @@ -145,6 +145,22 @@ SELECT AccountTransaction.* FROM AccountTransaction WHERE id = ?; +updateAccountTransactionUserSetOtherPartyName: +UPDATE AccountTransaction +SET userSetOtherPartyName = ? +WHERE id = ?; + +updateAccountTransactionUserSetOReference: +UPDATE AccountTransaction +SET userSetReference = ? +WHERE id = ?; + +updateAccountTransactionNotes: +UPDATE AccountTransaction +SET notes = ? +WHERE id = ?; + + deleteTransactionsByBankId { DELETE FROM BankAccount WHERE bankId = :bankId; diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/transactions/TransactionListItem.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/transactions/TransactionListItem.kt index d350a4a..545e702 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/transactions/TransactionListItem.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/transactions/TransactionListItem.kt @@ -51,11 +51,11 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo DI.uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData( DI.uiState.banks.value.firstNotNullOf { it.accounts.firstOrNull { it.id == transaction.accountId } }, - transaction.otherPartyName, + transaction.otherPartyName, // we don't use userSetOtherPartyName here on purpose transactionEntity?.otherPartyBankId, transactionEntity?.otherPartyAccountId, if (withSameData) transaction.amount else null, - if (withSameData) transaction.reference else null + if (withSameData) transaction.reference else null // we don't use userSetReference here on purpose ) } } @@ -83,7 +83,7 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo } Text( - text = transaction.otherPartyName ?: transaction.postingText ?: "", + text = transaction.userSetOtherPartyName ?: transaction.otherPartyName ?: transaction.postingText ?: "", Modifier.fillMaxWidth(), color = Style.ListItemHeaderTextColor, fontWeight = Style.ListItemHeaderWeight, @@ -95,7 +95,7 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo Spacer(modifier = Modifier.height(6.dp)) Text( - text = transaction.reference ?: "", + text = transaction.userSetReference ?: transaction.reference ?: "", Modifier.fillMaxWidth(), maxLines = 1, overflow = TextOverflow.Ellipsis @@ -121,7 +121,7 @@ fun TransactionListItem(bank: BankAccess?, transaction: AccountTransactionViewMo offset = showMenuAt ?: DpOffset.Zero, ) { DropdownMenuItem({ newMoneyTransferToOtherParty(false) }) { - Text("Neue Überweisung an ${transaction.otherPartyName} ...") + Text("Neue Überweisung an ${transaction.userSetOtherPartyName ?: transaction.otherPartyName} ...") // really use userSetOtherPartyName here as we don't use it in ShowTransferMoneyDialogData } DropdownMenuItem({ newMoneyTransferToOtherParty(true) }) { diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/AccountTransactionDetailsScreen.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/AccountTransactionDetailsScreen.kt index 0ef7a5e..ac75d03 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/AccountTransactionDetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/AccountTransactionDetailsScreen.kt @@ -1,16 +1,20 @@ package net.codinux.banking.ui.screens import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import net.codinux.banking.client.model.isNegative import net.codinux.banking.persistence.entities.AccountTransactionEntity import net.codinux.banking.ui.config.DI import net.codinux.banking.ui.extensions.verticalScroll import net.codinux.banking.ui.forms.LabelledValue +import net.codinux.banking.ui.forms.OutlinedTextField import net.codinux.banking.ui.forms.SectionHeader private val formatUtil = DI.formatUtil @@ -35,14 +39,50 @@ fun AccountTransactionDetailsScreen(transaction: AccountTransactionEntity, onClo || transaction.journalNumber != null || transaction.textKeyAddition != null - FullscreenViewBase("Umsatzdetails", onClosed = onClosed) { + var enteredOtherPartyName by remember { mutableStateOf(transaction.userSetOtherPartyName ?: transaction.otherPartyName ?: "") } + + var enteredReference by remember { mutableStateOf(transaction.userSetReference ?: transaction.reference ?: "") } + + var enteredNotes by remember { mutableStateOf(transaction.notes ?: "") } + + val hasDataChanged by remember(enteredOtherPartyName, enteredReference, enteredNotes) { + mutableStateOf( + (enteredOtherPartyName != transaction.userSetOtherPartyName && (transaction.userSetOtherPartyName?.isNotBlank() == true || enteredOtherPartyName.isNotBlank())) + || (enteredReference != transaction.userSetReference && (transaction.userSetReference?.isNotBlank() == true || enteredReference.isNotBlank())) + || (enteredNotes != transaction.notes && enteredNotes.isNotBlank()) + ) + } + + val coroutineScope = rememberCoroutineScope() + + + fun saveChanges() { + coroutineScope.launch { + DI.bankingService.updateAccountTransactionEntity(transaction, enteredOtherPartyName.takeUnless { it.isBlank() }, enteredReference.takeUnless { it.isBlank() }, enteredNotes.takeUnless { it.isBlank() }) + } + } + + + FullscreenViewBase( + "Umsatzdetails", + confirmButtonTitle = "Speichern", + confirmButtonEnabled = hasDataChanged, + showDismissButton = true, + onConfirm = { saveChanges() }, + onClosed = onClosed + ) { SelectionContainer { - Column(Modifier.fillMaxSize().verticalScroll().padding(8.dp)) { + Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(8.dp)) { Column(Modifier.fillMaxWidth()) { SectionHeader(if (isExpense) "Empfänger*in" else "Zahlende*r", false) - LabelledValue("Name", transaction.otherPartyName ?: "") + OutlinedTextField( + label = { Text("Name") }, + value = enteredOtherPartyName, + onValueChange = { enteredOtherPartyName = it }, + modifier = Modifier.fillMaxWidth() + ) LabelledValue("BIC", transaction.otherPartyBankId ?: "") @@ -57,7 +97,12 @@ fun AccountTransactionDetailsScreen(transaction: AccountTransactionEntity, onClo LabelledValue("Buchungstext", transaction.postingText ?: "") - LabelledValue("Verwendungszweck", transaction.reference ?: "") + OutlinedTextField( + label = { Text("Verwendungszweck") }, + value = enteredReference, + onValueChange = { enteredReference = it }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) LabelledValue("Buchungsdatum", formatUtil.formatDate(transaction.bookingDate)) @@ -70,6 +115,16 @@ fun AccountTransactionDetailsScreen(transaction: AccountTransactionEntity, onClo transaction.closingBalance?.let { LabelledValue("Tagesendsaldo", formatUtil.formatAmount(it, accountCurrency)) } + + OutlinedTextField( + label = { Text("Notizen") }, + value = enteredNotes, + onValueChange = { enteredNotes = it }, + singleLine = false, + minLines = 2, + maxLines = 3, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) } if (hasDetailedValues) { diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt index febdfae..94d7f74 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/AccountTransactionsFilterService.kt @@ -46,7 +46,10 @@ class AccountTransactionsFilterService { private fun matchesSearchTerm(transaction: AccountTransactionViewModel, searchTerm: String): Boolean = transaction.reference?.contains(searchTerm, true) == true + || transaction.userSetReference?.contains(searchTerm, true) == true || transaction.otherPartyName?.contains(searchTerm, true) == true + || transaction.userSetOtherPartyName?.contains(searchTerm, true) == true + || transaction.postingText?.contains(searchTerm, true) == true fun filterHoldings(holdings: List, filter: AccountTransactionsFilter): List { diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt index 4f0cc7c..102cbdf 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt @@ -19,6 +19,7 @@ class BankDataImporterAndExporter { transactions.forEach { transaction -> writer.writeRow( // TODO: add bank and bank account + // TODO: also regard userSetOtherPartyName and userSetReference? formatAmount(transaction.amount, decimalSeparator), transaction.currency, transaction.valueDate.toString(), transaction.bookingDate.toString(), transaction.reference, diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt index 5dbe4e2..7c3461a 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt @@ -303,6 +303,31 @@ class BankingService( } + suspend fun updateAccountTransactionEntity(transaction: AccountTransactionEntity, userSetOtherPartyName: String?, userSetReference: String?, notes: String?) { + try { + bankingRepository.updateTransaction(transaction, userSetOtherPartyName, userSetReference, notes) + + val transactionViewModel = uiState.transactions.value.firstOrNull { it.id == transaction.id } // should actually never be null + + if (transaction.userSetOtherPartyName != userSetOtherPartyName) { + transaction.userSetOtherPartyName = userSetOtherPartyName + transactionViewModel?.userSetOtherPartyName = userSetOtherPartyName // also update displayed AccountTransactionViewModel + } + if (transaction.userSetReference != userSetReference) { + transaction.userSetReference = userSetReference + transactionViewModel?.userSetReference = userSetReference + } + if (transaction.notes != notes) { + transaction.notes = notes + } + + notifyAccountTransactionListUpdated() + } catch (e: Throwable) { + showAndLogError(ErroneousAction.SaveToDatabase, "Could not update account transaction $transaction", "Kontoumsatz konnten nicht aktualisisert werden", e) + } + } + + private fun getCurrentUiBanksList() = uiState.banks.value private suspend fun notifyBanksListUpdated() { @@ -321,6 +346,18 @@ class BankingService( uiState.banks.emit(banks) } + private suspend fun notifyAccountTransactionListUpdated() { + val currentTransactionsList = uiState.transactions.value + + if (currentTransactionsList.isNotEmpty()) { + // if we only would call uiState.banks.emit(banks) with the same banks list as currently, nothing would change -> + // update does not get triggered -> for a short time display a different banks list and then return to actual banks list + uiState.transactions.emit(currentTransactionsList.toMutableList().also { it.add(it.last()) }) + try { delay(10) } catch (e: Throwable) { } + uiState.transactions.emit(currentTransactionsList) + } + } + private fun updateTransactionsInUi(addedTransactions: List): List { val transactionsViewModel = addedTransactions.map { AccountTransactionViewModel(it) } diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/RecipientFinder.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/RecipientFinder.kt index c491f1f..7b01b3a 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/RecipientFinder.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/RecipientFinder.kt @@ -32,6 +32,7 @@ class RecipientFinder(private val bankFinder: BankFinder) { suspend fun updateData(transactions: List) { availableRecipients = transactions.mapNotNull { + // TODO: also regard userSetOtherPartyName? if (it.otherPartyName != null && it.otherPartyAccountId != null) { RecipientSuggestion(it.otherPartyName!!, it.otherPartyBankId, it.otherPartyAccountId!!) } else { @@ -45,6 +46,7 @@ class RecipientFinder(private val bankFinder: BankFinder) { transactionsByIban = transactions.filter { it.otherPartyAccountId != null }.groupBy { it.otherPartyAccountId!! } .mapValues { it.value.map { + // TODO: also regard userSetReference? PaymentDataSuggestion(it.reference ?: "", Amount(it.amount.toString().replace("-", "")), it.currency, it.valueDate) }.toSet().sortedByDescending { it.valueDate } } }