Compare commits

..

12 Commits

32 changed files with 542 additions and 106 deletions

View File

@ -26,6 +26,13 @@ interface BankingRepository {
suspend fun persistBank(bank: BankAccess): BankAccessEntity
suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, bankName: String?)
suspend fun updateAccount(bank: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean)
suspend fun deleteBank(bank: BankAccessEntity)
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>

View File

@ -48,6 +48,19 @@ class InMemoryBankingRepository(
return entity
}
override suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, bankName: String?) {
// no-op
}
override suspend fun updateAccount(bank: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean) {
// no-op
}
override suspend fun deleteBank(bank: BankAccessEntity) {
this.banks.remove(bank)
}
override suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity> {
throw NotImplementedError("Lazy developer, method is not implemented")
}

View File

@ -95,7 +95,7 @@ open class SqliteBankingRepository : BankingRepository {
val tanMedia = getAllTanMedia().groupBy { it.bankId }.mapValues { it.value.toMutableList() }
val holdings = getAllHoldings().groupBy { it.accountId }
return bankQueries.getAllBanks { id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, countryCode, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
return bankQueries.getAllBanks { id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, countryCode, clientData, userSetDisplayName, displayIndex, iconUrl, wrongCredentialsEntered ->
BankAccessEntity(id, domesticBankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfBank(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: mutableListOf(), selectedTanMediumIdentifier, tanMedia[id] ?: mutableListOf(),
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, countryCode, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
}.executeAsList()
@ -125,6 +125,46 @@ open class SqliteBankingRepository : BankingRepository {
}
}
override suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, userSetDisplayName: String?) {
bankQueries.transaction {
if (bank.loginName != loginName) {
bankQueries.updateBankLoginName(loginName, bank.id)
}
if (bank.password != password) {
bankQueries.updateBankPassword(password, bank.id)
}
if (bank.userSetDisplayName != userSetDisplayName) {
bankQueries.updateBankUserSetDisplayName(userSetDisplayName, bank.id)
}
}
}
override suspend fun updateAccount(bank: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean) {
bankQueries.transaction {
if (bank.userSetDisplayName != userSetDisplayName) {
bankQueries.updateBankAccountUserSetDisplayName(userSetDisplayName, bank.id)
}
if (bank.hideAccount != hideAccount) {
bankQueries.updateBankAccountHideAccount(hideAccount, bank.id)
}
if (bank.includeInAutomaticAccountsUpdate != includeInAutomaticAccountsUpdate) {
bankQueries.updateBankAccountIncludeInAutomaticAccountsUpdate(includeInAutomaticAccountsUpdate, bank.id)
}
}
}
override suspend fun deleteBank(bank: BankAccessEntity) {
bankQueries.transaction {
accountTransactionQueries.deleteTransactionsByBankId(bankId = bank.id)
bankQueries.deleteBank(bank.id)
}
}
fun getAllBankAccounts(): List<BankAccountEntity> = bankQueries.getAllBankAccounts { id, bankId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
BankAccountEntity(
@ -337,8 +377,8 @@ open class SqliteBankingRepository : BankingRepository {
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
accountTransactionQueries.getAllTransactionsAsViewModel { id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName ->
AccountTransactionViewModel(id, bankId, accountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, userSetOtherPartyName)
accountTransactionQueries.getAllTransactionsAsViewModel { id, bankId, accountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetReference, userSetOtherPartyName ->
AccountTransactionViewModel(id, bankId, accountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetReference, userSetOtherPartyName)
}.executeAsList()
override fun getAllAccountTransactions(): List<AccountTransactionEntity> {

View File

@ -145,6 +145,15 @@ SELECT AccountTransaction.*
FROM AccountTransaction WHERE id = ?;
deleteTransactionsByBankId {
DELETE FROM BankAccount
WHERE bankId = :bankId;
DELETE FROM Holding
WHERE bankId = :bankId;
}
CREATE TABLE IF NOT EXISTS Holding (

View File

@ -79,6 +79,38 @@ SELECT BankAccess.*
FROM BankAccess;
updateBankLoginName:
UPDATE BankAccess
SET loginName = ?
WHERE id = ?;
updateBankPassword:
UPDATE BankAccess
SET password = ?
WHERE id = ?;
updateBankUserSetDisplayName:
UPDATE BankAccess
SET userSetDisplayName = ?
WHERE id = ?;
deleteBank {
DELETE FROM TanMethod
WHERE bankId = :bankId;
DELETE FROM TanMedium
WHERE bankId = :bankId;
DELETE FROM BankAccount
WHERE bankId = :bankId;
DELETE FROM BankAccess
WHERE id = :bankId;
}
CREATE TABLE IF NOT EXISTS BankAccount (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -155,6 +187,22 @@ SELECT BankAccount.*
FROM BankAccount;
updateBankAccountUserSetDisplayName:
UPDATE BankAccount
SET userSetDisplayName = ?
WHERE id = ?;
updateBankAccountHideAccount:
UPDATE BankAccount
SET hideAccount = ?
WHERE id = ?;
updateBankAccountIncludeInAutomaticAccountsUpdate:
UPDATE BankAccount
SET includeInAutomaticAccountsUpdate = ?
WHERE id = ?;
CREATE TABLE IF NOT EXISTS TanMethod (
id INTEGER PRIMARY KEY AUTOINCREMENT,

View File

@ -6,8 +6,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.fragment.app.FragmentActivity
import net.codinux.banking.persistence.AndroidContext
import net.codinux.banking.persistence.SqliteBankingRepository
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.service.AuthenticationService
import net.codinux.banking.ui.service.BiometricAuthenticationService
import net.codinux.banking.ui.service.ImageService

View File

@ -3,6 +3,7 @@ package net.codinux.banking.ui
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineDispatcher
@ -11,6 +12,10 @@ import kotlinx.coroutines.Dispatchers
actual val Dispatchers.IOorDefault: CoroutineDispatcher
get() = Dispatchers.IO
actual fun KeyEvent.isBackButtonPressedEvent(): Boolean =
this.nativeKeyEvent.keyCode == android.view.KeyEvent.KEYCODE_BACK
@Composable
actual fun rememberScreenSizeInfo(): ScreenSizeInfo {
val config = LocalConfiguration.current

View File

@ -1,6 +1,7 @@
package net.codinux.banking.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineDispatcher
@ -9,6 +10,9 @@ import kotlinx.coroutines.Dispatchers
expect val Dispatchers.IOorDefault: CoroutineDispatcher
expect fun KeyEvent.isBackButtonPressedEvent(): Boolean
@Composable
expect fun rememberScreenSizeInfo(): ScreenSizeInfo

View File

@ -1,18 +1,24 @@
package net.codinux.banking.ui.appskeleton
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.forms.RoundedCornersCard
import net.codinux.banking.ui.forms.Select
import net.codinux.banking.ui.isBackButtonPressedEvent
import net.codinux.banking.ui.model.settings.TransactionsGrouping
private val uiState = DI.uiState
@ -37,10 +43,20 @@ fun FilterBar() {
val months = listOf("Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember" /*, "1. Quartal", "2. Quartal", "3. Quartal", "4. Quartal" */, null)
val filterBarFocus = remember { FocusRequester() }
Box(
contentAlignment = Alignment.BottomEnd,
modifier = Modifier.fillMaxSize().zIndex(100f)
.padding(bottom = 64.dp, end = 74.dp)
.padding(bottom = 64.dp, end = 74.dp).focusable(true).focusRequester(filterBarFocus).focusTarget().onKeyEvent { event ->
if (event.isBackButtonPressedEvent() || event.key == Key.Escape) {
DI.uiState.showFilterBar.value = false
true
} else {
false
}
}
) {
Column(Modifier.height(230.dp).width(390.dp)) {
RoundedCornersCard(cornerSize = 4.dp, shadowElevation = 24.dp) {
@ -90,4 +106,9 @@ fun FilterBar() {
}
}
LaunchedEffect(filterBarFocus) {
filterBarFocus.requestFocus() // focus filter bar so that it receives key events to handle e.g. Escape button press
}
}

View File

@ -44,7 +44,7 @@ fun BanksList(
accountSelected?.invoke(bank, null)
}
bank.accountsSorted.forEach { account ->
bank.accountsSorted.filterNot { it.hideAccount }.forEach { account ->
NavigationMenuItem(itemModifier, account.displayName, textColor, iconSize, IconTextSpacing, itemHorizontalPadding, bankAccount = account) {
accountSelected?.invoke(bank, account)
}

View File

@ -13,8 +13,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.securitiesaccount.Holding
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.persistence.entities.HoldingEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Style
@ -31,7 +31,7 @@ private val formatUtil = DI.formatUtil
fun GroupedTransactionsListItems(
modifier: Modifier,
transactionsToDisplay: List<AccountTransactionViewModel>,
holdingsToDisplay: List<Holding>,
holdingsToDisplay: List<HoldingEntity>,
banksById: Map<Long, BankAccessEntity>,
transactionsGrouping: TransactionsGrouping
) {
@ -65,9 +65,9 @@ fun GroupedTransactionsListItems(
RoundedCornersCard {
Column(Modifier.background(Color.White)) {
holdingsToDisplay.forEachIndexed { index, holding ->
// key(statementOfHoldings.id) {
key(holding.id) {
HoldingListItem(holding, index % 2 == 1, index < holdingsToDisplay.size - 1)
// }
}
}
}
}
@ -76,49 +76,51 @@ fun GroupedTransactionsListItems(
}
items(groupedByDate.keys.sortedDescending()) { groupingDate ->
Column(Modifier.fillMaxWidth()) {
Text(
text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping),
color = Style.ListItemHeaderTextColor,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp),
)
key(groupingDate.toEpochDays()) {
Column(Modifier.fillMaxWidth()) {
Text(
text = DI.formatUtil.formatGroupingDate(groupingDate, transactionsGrouping),
color = Style.ListItemHeaderTextColor,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp),
)
Spacer(Modifier.height(4.dp))
Spacer(Modifier.height(4.dp))
val monthTransactions = groupedByDate[groupingDate].orEmpty().sortedByDescending { it.valueDate }
val monthTransactions = groupedByDate[groupingDate].orEmpty().sortedByDescending { it.valueDate }
RoundedCornersCard {
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
monthTransactions.forEachIndexed { index, transaction ->
key(transaction.id) {
TransactionListItem(banksById[transaction.bankId], transaction, index, monthTransactions.size)
RoundedCornersCard {
Column(Modifier.background(Color.White)) { // LazyColumn inside LazyColumn is not allowed
monthTransactions.forEachIndexed { index, transaction ->
key(transaction.id) {
TransactionListItem(banksById[transaction.bankId], transaction, index, monthTransactions.size)
}
}
}
}
}
Column(
Modifier.fillMaxWidth().padding(top = 10.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = formatUtil.formatAmount(calculator.sumIncome(monthTransactions),
calculator.getTransactionsCurrency(monthTransactions)),
color = formatUtil.getColorForAmount(Amount.Zero, showColoredAmounts)
)
}
Column(
Modifier.fillMaxWidth().padding(top = 10.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = formatUtil.formatAmount(calculator.sumIncome(monthTransactions),
calculator.getTransactionsCurrency(monthTransactions)),
color = formatUtil.getColorForAmount(Amount.Zero, showColoredAmounts)
)
}
Column(
Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 16.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = formatUtil.formatAmount(calculator.sumExpenses(monthTransactions),
calculator.getTransactionsCurrency(monthTransactions)),
color = formatUtil.getColorForAmount(Amount("-1"), showColoredAmounts)
)
Column(
Modifier.fillMaxWidth().padding(top = 2.dp, bottom = 16.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = formatUtil.formatAmount(calculator.sumExpenses(monthTransactions),
calculator.getTransactionsCurrency(monthTransactions)),
color = formatUtil.getColorForAmount(Amount("-1"), showColoredAmounts)
)
}
}
}
}

View File

@ -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) }) {

View File

@ -69,9 +69,9 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
} else {
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
itemsIndexed(holdingsToDisplay) { index, holding ->
// key(holding.isin) {
key(holding.id) {
HoldingListItem(holding, index % 2 == 1, index < holdingsToDisplay.size - 1)
// }
}
}
itemsIndexed(transactionsToDisplay) { index, transaction ->

View File

@ -13,6 +13,8 @@ object Internationalization {
const val ErrorTransferMoney = "Überweisung konnte nicht ausgeführt werden"
const val SaveToDatabase = "Daten konnten nicht in der Datenbank gespeichert werden"
const val ErrorBiometricAuthentication = "Biometrische Authentifizierung fehlgeschlagen"

View File

@ -11,6 +11,7 @@ fun ApplicationErrorDialog(error: ApplicationError, onDismiss: (() -> Unit)? = n
ErroneousAction.AddAccount -> Internationalization.ErrorAddAccount
ErroneousAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
ErroneousAction.TransferMoney -> Internationalization.ErrorTransferMoney
ErroneousAction.SaveToDatabase -> Internationalization.SaveToDatabase
ErroneousAction.BiometricAuthentication -> Internationalization.ErrorBiometricAuthentication
}

View File

@ -22,8 +22,10 @@ import net.codinux.banking.ui.forms.*
@Composable
fun BaseDialog(
title: String,
centerTitle: Boolean = false,
confirmButtonTitle: String = "OK",
confirmButtonEnabled: Boolean = true,
dismissButtonTitle: String = "Abbrechen",
showProgressIndicatorOnConfirmButton: Boolean = false,
useMoreThanPlatformDefaultWidthOnMobile: Boolean = false,
onDismiss: () -> Unit,
@ -38,7 +40,7 @@ fun BaseDialog(
Column(Modifier.background(Color.White).padding(8.dp)) {
Row(Modifier.fillMaxWidth()) {
HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f))
HeaderText(title, Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp).weight(1f), textAlign = if (centerTitle) TextAlign.Center else TextAlign.Start)
if (DI.platform.isDesktop) {
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
@ -49,9 +51,9 @@ fun BaseDialog(
content()
Row(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth().padding(top = 8.dp)) {
TextButton(onClick = onDismiss, Modifier.weight(0.5f)) {
Text("Abbrechen", color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
Text(dismissButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())
}
TextButton(
@ -61,7 +63,7 @@ fun BaseDialog(
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
if (showProgressIndicatorOnConfirmButton) {
CircularProgressIndicator(Modifier.padding(end = 6.dp), color = Colors.CodinuxSecondaryColor)
CircularProgressIndicator(Modifier.padding(end = 6.dp).size(36.dp), color = Colors.CodinuxSecondaryColor)
}
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth())

View File

@ -0,0 +1,32 @@
package net.codinux.banking.ui.dialogs
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ConfirmDialog(
text: String,
title: String? = null,
confirmButtonTitle: String = "Ja",
dismissButtonTitle: String = "Nein",
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
BaseDialog(
title = title ?: "",
centerTitle = true,
confirmButtonTitle = confirmButtonTitle,
dismissButtonTitle = dismissButtonTitle,
onDismiss = { onDismiss() },
onConfirm = { onConfirm(); onDismiss() }
) {
Text(text, textAlign = TextAlign.Center, lineHeight = 22.sp, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
}
}

View File

@ -11,6 +11,7 @@ import androidx.compose.ui.window.DialogProperties
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.model.Config.NewLine
@Composable
fun ErrorDialog(
@ -22,7 +23,7 @@ fun ErrorDialog(
) {
val effectiveText = if (exception == null) text else {
"$text\r\n\r\nFehlermeldung:\r\n${exception.stackTraceToString()}"
"$text${NewLine}${NewLine}Fehlermeldung:${NewLine}${exception.stackTraceToString()}"
}

View File

@ -50,7 +50,7 @@ private fun FormListItemImpl(
}
}
Text(label, color = Colors.FormListItemTextColor, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxHeight())
Text(label, color = Colors.FormListItemTextColor, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}

View File

@ -7,5 +7,7 @@ enum class ErroneousAction {
TransferMoney,
SaveToDatabase,
BiometricAuthentication
}

View File

@ -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) {

View File

@ -6,7 +6,9 @@ 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.persistence.entities.BankAccountEntity
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.config.Internationalization
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.*
@ -16,20 +18,36 @@ fun BankAccountSettingsScreen(account: BankAccountEntity, onClosed: () -> Unit)
var enteredAccountName by remember { mutableStateOf(account.displayName) }
var selectedIncludeInAutomaticAccountsUpdate by remember { mutableStateOf(account.includeInAutomaticAccountsUpdate) }
var selectedHideAccount by remember { mutableStateOf(account.hideAccount) }
val hasDataChanged by remember(enteredAccountName) {
var selectedIncludeInAutomaticAccountsUpdate by remember { mutableStateOf(account.includeInAutomaticAccountsUpdate) }
val hasDataChanged by remember(enteredAccountName, selectedHideAccount, selectedIncludeInAutomaticAccountsUpdate) {
mutableStateOf(
enteredAccountName != account.displayName
|| selectedIncludeInAutomaticAccountsUpdate != account.includeInAutomaticAccountsUpdate
|| selectedHideAccount != account.hideAccount
|| selectedIncludeInAutomaticAccountsUpdate != account.includeInAutomaticAccountsUpdate
)
}
val coroutineScope = rememberCoroutineScope()
FullscreenViewBase(account.displayName, onClosed = onClosed) {
fun saveChanges() {
coroutineScope.launch {
DI.bankingService.updateAccount(account, enteredAccountName, selectedHideAccount, selectedIncludeInAutomaticAccountsUpdate)
}
}
FullscreenViewBase(
account.displayName,
confirmButtonTitle = "Speichern",
confirmButtonEnabled = hasDataChanged,
showDismissButton = true,
onConfirm = { saveChanges() },
onClosed = onClosed
) {
Column(Modifier.fillMaxSize().verticalScroll().padding(8.dp)) {
Column {
SectionHeader("Einstellungen", false)
@ -41,9 +59,9 @@ fun BankAccountSettingsScreen(account: BankAccountEntity, onClosed: () -> Unit)
modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp)
)
// BooleanOption("Bei Kontoaktualisierung einbeziehen", selectedIncludeInAutomaticAccountsUpdate) { selectedIncludeInAutomaticAccountsUpdate = it }
//
// BooleanOption("Konto ausblenden", selectedHideAccount) { selectedHideAccount = it }
BooleanOption("Bei Kontoaktualisierung einbeziehen (autom. Kontoaktualisierung noch nicht umgesetzt)", selectedIncludeInAutomaticAccountsUpdate) { selectedIncludeInAutomaticAccountsUpdate = it }
BooleanOption("Konto ausblenden", selectedHideAccount) { selectedHideAccount = it }
}
SelectionContainer {

View File

@ -2,13 +2,20 @@ package net.codinux.banking.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import net.codinux.banking.persistence.entities.BankAccessEntity
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.dialogs.ConfirmDialog
import net.codinux.banking.ui.extensions.verticalScroll
import net.codinux.banking.ui.forms.*
import net.codinux.banking.ui.model.Config.NewLine
@Composable
fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
@ -19,15 +26,48 @@ fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
var enteredPassword by remember { mutableStateOf(bank.password ?: "") }
val hasDataChanged by remember(enteredBankName) {
var showDeleteBankAccessConfirmationDialog by remember { mutableStateOf(false) }
val hasDataChanged by remember(enteredBankName, enteredLoginName, enteredPassword) {
mutableStateOf(
(enteredBankName != bank.bankName && (bank.userSetDisplayName == null || enteredBankName != bank.userSetDisplayName))
|| enteredLoginName != bank.loginName || enteredPassword != bank.password
|| (enteredLoginName != bank.loginName && enteredLoginName.isNotBlank())
|| (enteredPassword != bank.password && enteredPassword.isNotBlank())
)
}
val coroutineScope = rememberCoroutineScope()
fun saveChanges() {
coroutineScope.launch {
DI.bankingService.updateBank(bank, enteredLoginName, enteredPassword, enteredBankName.takeUnless { it.isBlank() })
}
}
if (showDeleteBankAccessConfirmationDialog) {
ConfirmDialog(
title = "${bank.displayName} wirklich löschen?",
text = "Dadurch werden auch alle zum Konto gehörenden Daten wie seine Kontoumsätze unwiderruflich gelöscht.${NewLine}Die Daten können nicht widerhergestellt werden.",
onDismiss = { showDeleteBankAccessConfirmationDialog = false },
onConfirm = {
coroutineScope.launch {
DI.bankingService.deleteBank(bank)
}
onClosed()
}
)
}
FullscreenViewBase(bank.displayName, onClosed = onClosed) {
FullscreenViewBase(
bank.displayName,
confirmButtonTitle = "Speichern",
confirmButtonEnabled = hasDataChanged,
showDismissButton = true,
onConfirm = { saveChanges() },
onClosed = onClosed
) {
Column(Modifier.fillMaxSize().verticalScroll().padding(8.dp)) {
Column {
OutlinedTextField(
@ -98,13 +138,13 @@ fun BankSettingsScreen(bank: BankAccessEntity, onClosed: () -> Unit) {
}
}
// Spacer(Modifier.weight(1f))
//
// Column(Modifier.padding(top = 18.dp, bottom = 18.dp)) {
// TextButton(modifier = Modifier.fillMaxWidth().height(50.dp), onClick = { }, enabled = false) {
// Text("Konto löschen", color = Colors.DestructiveColor, textAlign = TextAlign.Center)
// }
// }
Spacer(Modifier.weight(1f))
Column(Modifier.padding(top = 24.dp, bottom = 18.dp)) {
TextButton(modifier = Modifier.fillMaxWidth().height(50.dp), onClick = { showDeleteBankAccessConfirmationDialog = true }) {
Text("Konto löschen", fontSize = 15.sp, color = Colors.DestructiveColor, textAlign = TextAlign.Center)
}
}
}
}
}

View File

@ -23,7 +23,10 @@ fun FullscreenViewBase(
title: String,
confirmButtonTitle: String = "OK",
confirmButtonEnabled: Boolean = true,
dismissButtonTitle: String = "Abbrechen",
showDismissButton: Boolean = false,
showButtonBar: Boolean = true,
onConfirm: (() -> Unit)? = null,
onClosed: () -> Unit,
content: @Composable () -> Unit
) {
@ -49,16 +52,18 @@ fun FullscreenViewBase(
if (showButtonBar) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
// TextButton(onClick = onClosed, Modifier.width(Style.DialogButtonWidth)) {
// Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
// }
//
// Spacer(Modifier.width(8.dp))
if (showDismissButton) {
TextButton(onClick = onClosed, Modifier.weight(1f)) {
Text(dismissButtonTitle, color = Colors.CodinuxSecondaryColor)
}
Spacer(Modifier.width(8.dp))
}
TextButton(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.weight(1f),
enabled = confirmButtonEnabled,
onClick = { /* onConfirm?.invoke() ?: */ onClosed() }
onClick = { onConfirm?.invoke(); onClosed() }
) {
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center)
}

View File

@ -82,9 +82,9 @@ fun ProtectAppSettingsDialog(appSettings: AppSettings, onClosed: () -> Unit) {
Spacer(Modifier.weight(1f))
if (selectedAuthenticationMethod == AppAuthenticationMethod.None) {
Row(Modifier.fillMaxWidth()) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
if (currentAuthenticationMethod == AppAuthenticationMethod.None) {
Text("Appzugangsschutz ist bereits ungeschützt", fontSize = 18.sp, textAlign = TextAlign.Center)
Text("Appzugang ist bereits ungeschützt", fontSize = 18.sp, textAlign = TextAlign.Center)
} else {
Text("Möchten Sie den Appzugangsschutz wirklich entfernen?", fontSize = 18.sp, textAlign = TextAlign.Center)
}

View File

@ -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<HoldingEntity>, filter: AccountTransactionsFilter): List<HoldingEntity> {

View File

@ -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,

View File

@ -72,10 +72,10 @@ class BankingService(
updateOnChanges(uiSettings)
uiState.banks.value = getAllBanks()
updateBanksInUi(getAllBanks())
uiState.transactions.value = getAllAccountTransactionsAsViewModel()
uiState.holdings.value = uiState.banks.value.flatMap { it.accounts }.flatMap { it.holdings }
uiState.holdings.value = getCurrentUiBanksList().flatMap { it.accounts }.flatMap { it.holdings }
} catch (e: Throwable) {
log.error(e) { "Could not read all banks and account transactions from repository" }
}
@ -93,6 +93,60 @@ class BankingService(
fun getAllBanks() = bankingRepository.getAllBanks()
suspend fun updateBank(bank: BankAccessEntity, loginName: String, password: String, bankName: String?) {
try {
bankingRepository.updateBank(bank, loginName, password, bankName)
if (bank.loginName != loginName) {
bank.loginName = loginName
}
if (bank.password != password) {
bank.password = password
}
if (bank.userSetDisplayName != bankName) {
bank.userSetDisplayName = bankName
}
notifyBanksListUpdated()
} catch (e: Throwable) {
showAndLogError(ErroneousAction.SaveToDatabase, "Could not update bank $bank", "Bankzugangsdaten konnten nicht aktualisisert werden", e)
}
}
suspend fun updateAccount(account: BankAccountEntity, userSetDisplayName: String?, hideAccount: Boolean, includeInAutomaticAccountsUpdate: Boolean) {
try {
bankingRepository.updateAccount(account, userSetDisplayName, hideAccount, includeInAutomaticAccountsUpdate)
if (account.userSetDisplayName != userSetDisplayName) {
account.userSetDisplayName = userSetDisplayName
}
if (account.hideAccount != hideAccount) {
account.hideAccount = hideAccount
}
if (account.includeInAutomaticAccountsUpdate != includeInAutomaticAccountsUpdate) {
account.includeInAutomaticAccountsUpdate = includeInAutomaticAccountsUpdate
}
notifyBanksListUpdated()
} catch (e: Throwable) {
showAndLogError(ErroneousAction.SaveToDatabase, "Could not update bank $account", "Bankzugangsdaten konnten nicht aktualisisert werden", e)
}
}
suspend fun deleteBank(bank: BankAccessEntity) {
try {
bankingRepository.deleteBank(bank)
uiState.transactions.value = uiState.transactions.value.filterNot { it.bankId == bank.id }
uiState.holdings.value = uiState.holdings.value.filterNot { it.bankId == bank.id }
updateBanksInUi(uiState.banks.value.toMutableList().also { it.remove(bank) })
} catch (e: Throwable) {
log.error(e) { "Could not delete bank ${bank.displayName}" }
showAndLogError(ErroneousAction.SaveToDatabase, "Could not delete bank ${bank.displayName}", "Fehler beim Löschen der Bankzugangsdaten für ${bank.displayName}", e)
}
}
fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
fun getAllTransactionsForBank(bank: BankAccessEntity) = bankingRepository.getAllTransactionsForBank(bank)
@ -113,14 +167,12 @@ class BankingService(
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulGetAccountDataResponse(response.data!!)
} else {
handleUnsuccessfulBankingClientResponse(BankingClientAction.AddAccount, response)
handleUnsuccessfulBankingClientResponse(BankingClientAction.AddAccount, bank.name, response)
}
return response.type == ResponseType.Success
} catch (e: Throwable) {
log.error(e) { "Could not add account for ${bank.name} $loginName" }
uiState.applicationErrorOccurred(ErroneousAction.AddAccount, null, e)
showAndLogError(ErroneousAction.AddAccount, "Could not add account for ${bank.name} $loginName", "Konto für ${bank.name} konnte nicht hinzugefügt werden", e)
return false
}
@ -144,9 +196,7 @@ class BankingService(
log.info { "Saved bank $newBankEntity with ${newBankEntity.accounts.flatMap { it.bookedTransactions }.size} transactions" }
val banks = uiState.banks.value.toMutableList()
banks.add(newBankEntity)
uiState.banks.value = banks
updateBanksInUi(uiState.banks.value.toMutableList().also { it.add(newBankEntity) })
updateTransactionsInUi(newBankEntity.accounts.flatMap { it.bookedTransactions })
updateHoldingsInUi(newBankEntity.accounts.flatMap { it.holdings }, emptyList())
@ -161,7 +211,8 @@ class BankingService(
if (selectedAccount != null) {
updateAccountTransactions(selectedAccount.bank, selectedAccount.bankAccount)
} else {
uiState.banks.value.forEach { bank ->
getCurrentUiBanksList().forEach { bank ->
// TODO: when implementing automatic account transactions update, filter out accounts with includeInAutomaticAccountsUpdate == false
updateAccountTransactions(bank)
}
}
@ -174,7 +225,7 @@ class BankingService(
if (response.type == ResponseType.Success && response.data != null) {
handleSuccessfulUpdateAccountTransactionsResponse(bank, response.data!!)
} else {
handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, response)
handleUnsuccessfulBankingClientResponse(BankingClientAction.UpdateAccountTransactions, bankAccount?.displayName ?: bank.displayName, response)
}
} catch (e: Throwable) {
log.error(e) { "Could not update account transactions for $bank" }
@ -251,6 +302,62 @@ 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() {
val currentBanksList = getCurrentUiBanksList()
if (currentBanksList.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
updateBanksInUi(currentBanksList.toMutableList().also { it.add(it.last()) })
try { delay(10) } catch (e: Throwable) { }
updateBanksInUi(currentBanksList)
}
}
private suspend fun updateBanksInUi(banks: List<BankAccessEntity>) {
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<AccountTransactionEntity>): List<AccountTransactionViewModel> {
val transactionsViewModel = addedTransactions.map { AccountTransactionViewModel(it) }
@ -281,7 +388,7 @@ class BankingService(
))
if (response.error != null) {
handleUnsuccessfulBankingClientResponse(BankingClientAction.TransferMoney, response)
handleUnsuccessfulBankingClientResponse(BankingClientAction.TransferMoney, account.displayName, response)
} else if (response.type == ResponseType.Success) {
uiState.dispatchTransferredMoney(TransferredMoneyEvent(recipientName, amount, currency))
@ -292,8 +399,8 @@ class BankingService(
}
private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, response: Response<*>) {
log.error { "$action was not successful: $response" }
private fun handleUnsuccessfulBankingClientResponse(action: BankingClientAction, accountOrBankName: String, response: Response<*>) {
log.error { "$action was not successful for $accountOrBankName: $response" }
response.error?.let { error ->
if (error.type != ErrorType.UserCancelledAction) { // the user knows that she cancelled the action
@ -320,6 +427,12 @@ class BankingService(
}
}
private fun showAndLogError(action: ErroneousAction, logMessage: String, messageToShowToUser: String? = null, exception: Throwable? = null) {
log.error(exception) { logMessage }
uiState.applicationErrorOccurred(action, messageToShowToUser, exception)
}
private suspend fun readTransactionsFromCsv(): List<AccountTransaction> {
val csv = Res.readBytes("files/transactions.csv").decodeToString()

View File

@ -32,6 +32,7 @@ class RecipientFinder(private val bankFinder: BankFinder) {
suspend fun updateData(transactions: List<AccountTransaction>) {
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 } }
}

View File

@ -3,6 +3,7 @@ package net.codinux.banking.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import kotlinx.coroutines.CoroutineDispatcher
@ -11,6 +12,9 @@ import kotlinx.coroutines.Dispatchers
actual val Dispatchers.IOorDefault: CoroutineDispatcher
get() = Dispatchers.IO
actual fun KeyEvent.isBackButtonPressedEvent(): Boolean = false
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun rememberScreenSizeInfo(): ScreenSizeInfo {

View File

@ -2,6 +2,7 @@ package net.codinux.banking.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import kotlinx.cinterop.ExperimentalForeignApi
@ -16,6 +17,9 @@ import platform.UIKit.UIScreen
actual val Dispatchers.IOorDefault: CoroutineDispatcher
get() = Dispatchers.IO
actual fun KeyEvent.isBackButtonPressedEvent(): Boolean = false // TODO
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun rememberScreenSizeInfo(): ScreenSizeInfo {

View File

@ -3,6 +3,7 @@ package net.codinux.banking.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import kotlinx.coroutines.CoroutineDispatcher
@ -11,6 +12,9 @@ import kotlinx.coroutines.Dispatchers
actual val Dispatchers.IOorDefault: CoroutineDispatcher
get() = Dispatchers.Default
actual fun KeyEvent.isBackButtonPressedEvent(): Boolean = false
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun rememberScreenSizeInfo(): ScreenSizeInfo {