Compare commits
No commits in common. "41586b01488ac9b9d96b8bcbe4656289984edbab" and "1890fd915145c29bbb3350078fe2bf2add007666" have entirely different histories.
41586b0148
...
1890fd9151
|
@ -9,14 +9,11 @@ import app.cash.sqldelight.async.coroutines.synchronous
|
||||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||||
import net.codinux.banking.dataaccess.BankmeisterDb
|
import net.codinux.banking.dataaccess.BankmeisterDb
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
import net.codinux.banking.ui.service.ImageService
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
ImageService.context = this.applicationContext
|
|
||||||
|
|
||||||
DI.setRepository(AndroidSqliteDriver(BankmeisterDb.Schema.synchronous(), this, "Bankmeister.db"))
|
DI.setRepository(AndroidSqliteDriver(BankmeisterDb.Schema.synchronous(), this, "Bankmeister.db"))
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
package net.codinux.banking.ui.composables.transactions
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import net.codinux.banking.client.model.Amount
|
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.ui.forms.RoundedCornersCard
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun HoldingListItemPreview() {
|
|
||||||
val holding1 = Holding("MUL Amundi MSCI World V", null, null, 1693, "EUR", Amount("18578.04"), Amount("16.888"), -0.35f, Amount("17944.48"), Amount("16.828"))
|
|
||||||
val holding2 = Holding("NVIDIA Corp.", null, null, 214, "EUR", Amount("21455.36"), Amount("100.18"), 8.8f, Amount("19872.04"), Amount("92.04"))
|
|
||||||
|
|
||||||
RoundedCornersCard {
|
|
||||||
Column {
|
|
||||||
HoldingListItem(holding1, false, false)
|
|
||||||
|
|
||||||
HoldingListItem(holding2, true, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -57,13 +57,3 @@ fun EnterTanDialogPreview_WithMultipleTanMedia() { // shows that dialog is reall
|
||||||
|
|
||||||
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
|
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun EnterTanDialogPreview_Flickercode() {
|
|
||||||
val tanMethods = listOf(TanMethod("chipTAN Flickercode", TanMethodType.ChipTanFlickercode, "902"))
|
|
||||||
val user = BankViewInfo("12345678", "SupiDupiNutzer", "Abzockbank", BankingGroup.Postbank)
|
|
||||||
val tanChallenge = TanChallenge(TanChallengeType.Flickercode, ActionRequiringTan.GetAccountInfo, "Geben Sie die TAN ein", tanMethods.first().identifier, tanMethods, user = user, flickerCode = FlickerCode("", ""))
|
|
||||||
|
|
||||||
EnterTanDialog(TanChallengeReceived(tanChallenge) { }) { }
|
|
||||||
}
|
|
|
@ -1,45 +1,12 @@
|
||||||
package net.codinux.banking.ui.service
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import net.codinux.log.Log
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
|
|
||||||
object ImageService {
|
|
||||||
|
|
||||||
lateinit var context: Context
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private val cacheDir by lazy { File(ImageService.context.cacheDir, "imageCache").also { it.mkdirs() } }
|
|
||||||
|
|
||||||
private val messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
|
|
||||||
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
||||||
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
|
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap()
|
||||||
|
|
||||||
actual suspend fun fetchBytesFromUrl(url: String): ByteArray {
|
actual suspend fun fetchBytesFromUrl(url: String): ByteArray =
|
||||||
var imageFile: File? = null
|
URL(url).openStream().buffered().use { it.readBytes() }
|
||||||
try {
|
|
||||||
val urlHash = messageDigest.digest(url.toByteArray())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
imageFile = File(cacheDir, urlHash)
|
|
||||||
|
|
||||||
if (imageFile.exists()) {
|
|
||||||
return imageFile.readBytes()
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.error(e) { "Could not create SHA-256 or read image bytes from file for url '$url'" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageBytes = URL(url).openStream().buffered().use { it.readBytes() }
|
|
||||||
|
|
||||||
imageFile?.writeBytes(imageBytes)
|
|
||||||
|
|
||||||
return imageBytes
|
|
||||||
}
|
|
|
@ -2,10 +2,8 @@ package net.codinux.banking.dataaccess
|
||||||
|
|
||||||
import net.codinux.banking.client.model.AccountTransaction
|
import net.codinux.banking.client.model.AccountTransaction
|
||||||
import net.codinux.banking.client.model.User
|
import net.codinux.banking.client.model.User
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
|
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
|
||||||
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
||||||
import net.codinux.banking.dataaccess.entities.HoldingEntity
|
|
||||||
import net.codinux.banking.dataaccess.entities.UserEntity
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
|
|
||||||
|
@ -18,13 +16,6 @@ interface BankingRepository {
|
||||||
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
|
suspend fun persistTransactions(bankAccount: BankAccountEntity, transactions: List<AccountTransaction>): List<AccountTransactionEntity>
|
||||||
|
|
||||||
|
|
||||||
suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity>
|
|
||||||
|
|
||||||
suspend fun updateHoldings(holdings: List<HoldingEntity>)
|
|
||||||
|
|
||||||
suspend fun deleteHoldings(holdings: List<HoldingEntity>)
|
|
||||||
|
|
||||||
|
|
||||||
fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel>
|
fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel>
|
||||||
|
|
||||||
fun getAllAccountTransactions(): List<AccountTransactionEntity>
|
fun getAllAccountTransactions(): List<AccountTransactionEntity>
|
||||||
|
|
|
@ -2,10 +2,8 @@ package net.codinux.banking.dataaccess
|
||||||
|
|
||||||
import net.codinux.banking.client.model.AccountTransaction
|
import net.codinux.banking.client.model.AccountTransaction
|
||||||
import net.codinux.banking.client.model.User
|
import net.codinux.banking.client.model.User
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
|
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
|
||||||
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
||||||
import net.codinux.banking.dataaccess.entities.HoldingEntity
|
|
||||||
import net.codinux.banking.dataaccess.entities.UserEntity
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
|
|
||||||
|
@ -34,17 +32,6 @@ class InMemoryBankingRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> = emptyList() // no-op
|
|
||||||
|
|
||||||
override suspend fun updateHoldings(holdings: List<HoldingEntity>) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteHoldings(holdings: List<HoldingEntity>) {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
|
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
|
||||||
transactions.map { AccountTransactionViewModel(it) }
|
transactions.map { AccountTransactionViewModel(it) }
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import app.cash.sqldelight.db.SqlDriver
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import net.codinux.banking.client.model.*
|
import net.codinux.banking.client.model.*
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.client.model.tan.*
|
import net.codinux.banking.client.model.tan.*
|
||||||
import net.codinux.banking.dataaccess.entities.*
|
import net.codinux.banking.dataaccess.entities.*
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
|
@ -30,20 +29,13 @@ open class SqliteBankingRepository(
|
||||||
val bankAccounts = getAllBankAccounts().groupBy { it.userId }
|
val bankAccounts = getAllBankAccounts().groupBy { it.userId }
|
||||||
val tanMethods = getAllTanMethods().groupBy { it.userId }
|
val tanMethods = getAllTanMethods().groupBy { it.userId }
|
||||||
val tanMedia = getAllTanMedia().groupBy { it.userId }
|
val tanMedia = getAllTanMedia().groupBy { it.userId }
|
||||||
val holdings = getAllHoldings().groupBy { it.bankAccountId }
|
|
||||||
|
|
||||||
return userQueries.selectAllUsers { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
|
return userQueries.selectAllUsers { id, bankCode, loginName, password, bankName, bic, customerName, userId, selectedTanMethodIdentifier, selectedTanMediumIdentifier, bankingGroup, serverAddress, userSetDisplayName, clientData, displayIndex, iconUrl, wrongCredentialsEntered ->
|
||||||
UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, getAccountsOfUser(id, bankAccounts, holdings), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(),
|
UserEntity(id, bankCode, loginName, password, bankName, bic, customerName, userId, bankAccounts[id] ?: emptyList(), selectedTanMethodIdentifier, tanMethods[id] ?: emptyList(), selectedTanMediumIdentifier, tanMedia[id] ?: emptyList(),
|
||||||
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
|
bankingGroup?.let { BankingGroup.valueOf(it) }, serverAddress, userSetDisplayName, displayIndex.toInt(), iconUrl, wrongCredentialsEntered)
|
||||||
}.executeAsList()
|
}.executeAsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getAccountsOfUser(userId: Long, bankAccounts: Map<Long, List<BankAccountEntity>>, holdings: Map<Long, List<HoldingEntity>>): List<BankAccountEntity> {
|
|
||||||
return bankAccounts[userId].orEmpty().onEach {
|
|
||||||
it.holdings = holdings[it.id].orEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun persistUser(user: User): UserEntity {
|
override suspend fun persistUser(user: User): UserEntity {
|
||||||
return userQueries.transactionWithResult {
|
return userQueries.transactionWithResult {
|
||||||
userQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic,
|
userQueries.insertUser(user.bankCode, user.loginName, user.password, user.bankName, user.bic,
|
||||||
|
@ -63,7 +55,7 @@ open class SqliteBankingRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getAllBankAccounts(): List<BankAccountEntity> = userQueries.selectAllBankAccounts { id, userId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
|
fun getAllBankAccounts(): List<BankAccountEntity> = userQueries.selectAllBankAccounts { id, userId, identifier, subAccountNumber, iban, productName, accountHolderName, type, currency, accountLimit, isAccountTypeSupportedByApplication, features, balance, serverTransactionsRetentionDays, lastTransactionsRetrievalTime, retrievedTransactionsFrom, userSetDisplayName, displayIndex, hideAccount, includeInAutomaticAccountsUpdate ->
|
||||||
BankAccountEntity(
|
BankAccountEntity(
|
||||||
id, userId,
|
id, userId,
|
||||||
|
|
||||||
|
@ -77,9 +69,9 @@ open class SqliteBankingRepository(
|
||||||
mapToAmount(balance),
|
mapToAmount(balance),
|
||||||
|
|
||||||
mapToInt(serverTransactionsRetentionDays),
|
mapToInt(serverTransactionsRetentionDays),
|
||||||
mapToInstant(lastAccountUpdateTime), mapToDate(retrievedTransactionsFrom),
|
mapToInstant(lastTransactionsRetrievalTime), mapToDate(retrievedTransactionsFrom),
|
||||||
|
|
||||||
mutableListOf(), mutableListOf(), emptyList(),
|
mutableListOf(), mutableListOf(),
|
||||||
|
|
||||||
userSetDisplayName, mapToInt(displayIndex),
|
userSetDisplayName, mapToInt(displayIndex),
|
||||||
hideAccount, includeInAutomaticAccountsUpdate
|
hideAccount, includeInAutomaticAccountsUpdate
|
||||||
|
@ -102,7 +94,7 @@ open class SqliteBankingRepository(
|
||||||
account.isAccountTypeSupportedByApplication, mapEnumCollectionToString(account.features),
|
account.isAccountTypeSupportedByApplication, mapEnumCollectionToString(account.features),
|
||||||
|
|
||||||
mapInt(account.serverTransactionsRetentionDays),
|
mapInt(account.serverTransactionsRetentionDays),
|
||||||
mapInstant(account.lastAccountUpdateTime), mapDate(account.retrievedTransactionsFrom),
|
mapInstant(account.lastTransactionsRetrievalTime), mapDate(account.retrievedTransactionsFrom),
|
||||||
|
|
||||||
account.userSetDisplayName, mapInt(account.displayIndex),
|
account.userSetDisplayName, mapInt(account.displayIndex),
|
||||||
account.hideAccount, account.includeInAutomaticAccountsUpdate
|
account.hideAccount, account.includeInAutomaticAccountsUpdate
|
||||||
|
@ -114,9 +106,7 @@ open class SqliteBankingRepository(
|
||||||
persistTransaction(userId, accountId, transaction)
|
persistTransaction(userId, accountId, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
val holdings = account.holdings.map { holding -> persistHolding(userId, accountId, holding) }
|
return BankAccountEntity(accountId, userId, account, accountTransactionEntities)
|
||||||
|
|
||||||
return BankAccountEntity(accountId, userId, account, accountTransactionEntities, holdings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -206,65 +196,6 @@ open class SqliteBankingRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected open fun getAllHoldings(): List<HoldingEntity> =
|
|
||||||
accountTransactionQueries.selectAllHoldings { id, userId, bankAccountId, name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate ->
|
|
||||||
HoldingEntity(id, userId, bankAccountId, name, isin, wkn, mapToInt(quantity), currency, mapToAmount(totalBalance), mapToAmount(marketValue), performancePercentage?.toFloat(), mapToAmount(totalCostPrice), mapToAmount(averageCostPrice), mapToInstant(pricingTime), mapToDate(buyingDate))
|
|
||||||
}.executeAsList()
|
|
||||||
|
|
||||||
override suspend fun persistHoldings(bankAccount: BankAccountEntity, holdings: List<Holding>): List<HoldingEntity> =
|
|
||||||
accountTransactionQueries.transactionWithResult {
|
|
||||||
holdings.map { persistHolding(bankAccount.userId, bankAccount.id, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Has to be executed in a transaction in order that getting persisted Holding's id works~
|
|
||||||
*/
|
|
||||||
protected open suspend fun persistHolding(userId: Long, bankAccountId: Long, holding: Holding): HoldingEntity {
|
|
||||||
accountTransactionQueries.insertHolding(
|
|
||||||
userId, bankAccountId,
|
|
||||||
|
|
||||||
holding.name, holding.isin, holding.wkn,
|
|
||||||
|
|
||||||
mapInt(holding.quantity), holding.currency,
|
|
||||||
|
|
||||||
mapAmount(holding.totalBalance), mapAmount(holding.marketValue),
|
|
||||||
holding.performancePercentage?.toDouble(),
|
|
||||||
mapAmount(holding.totalCostPrice), mapAmount(holding.averageCostPrice),
|
|
||||||
|
|
||||||
mapInstant(holding.pricingTime), mapDate(holding.buyingDate)
|
|
||||||
)
|
|
||||||
|
|
||||||
return HoldingEntity(getLastInsertedId(), userId, bankAccountId, holding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateHoldings(holdings: List<HoldingEntity>) {
|
|
||||||
accountTransactionQueries.transaction {
|
|
||||||
holdings.onEach { holding ->
|
|
||||||
accountTransactionQueries.updateHolding(
|
|
||||||
holding.name, holding.isin, holding.wkn,
|
|
||||||
mapInt(holding.quantity), holding.currency,
|
|
||||||
|
|
||||||
mapAmount(holding.totalBalance), mapAmount(holding.marketValue),
|
|
||||||
holding.performancePercentage?.toDouble(),
|
|
||||||
mapAmount(holding.totalCostPrice), mapAmount(holding.averageCostPrice),
|
|
||||||
|
|
||||||
mapInstant(holding.pricingTime), mapDate(holding.buyingDate),
|
|
||||||
|
|
||||||
holding.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteHoldings(holdings: List<HoldingEntity>) {
|
|
||||||
accountTransactionQueries.transaction {
|
|
||||||
holdings.forEach { holding ->
|
|
||||||
accountTransactionQueries.deleteHolding(holding.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
|
override fun getAllAccountTransactionsAsViewModel(): List<AccountTransactionViewModel> =
|
||||||
accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category ->
|
accountTransactionQueries.selectAllTransactionsAsViewModel { id, userId, bankAccountId, amount, currency, reference, valueDate, otherPartyName, postingText, userSetDisplayName, category ->
|
||||||
AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category)
|
AccountTransactionViewModel(id, userId, bankAccountId, mapToAmount(amount), currency, reference, mapToDate(valueDate), otherPartyName, postingText, userSetDisplayName, category)
|
||||||
|
@ -397,15 +328,13 @@ open class SqliteBankingRepository(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@JvmName("mapAmountNullable")
|
@JvmName("mapAmount")
|
||||||
@JsName("mapAmountNullable")
|
@JsName("mapAmount")
|
||||||
private fun mapAmount(amount: Amount?): String? =
|
private fun mapAmount(amount: Amount?): String? =
|
||||||
amount?.let { mapAmount(it) }
|
amount?.let { mapAmount(it) }
|
||||||
|
|
||||||
private fun mapAmount(amount: Amount): String = amount.toString()
|
private fun mapAmount(amount: Amount): String = amount.amount
|
||||||
|
|
||||||
@JvmName("mapToAmountNullable")
|
|
||||||
@JsName("mapToAmountNullable")
|
|
||||||
private fun mapToAmount(serializedAmount: String?): Amount? =
|
private fun mapToAmount(serializedAmount: String?): Amount? =
|
||||||
serializedAmount?.let { mapToAmount(it) }
|
serializedAmount?.let { mapToAmount(it) }
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package net.codinux.banking.dataaccess.entities
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import net.codinux.banking.client.model.*
|
import net.codinux.banking.client.model.*
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
|
|
||||||
class BankAccountEntity(
|
class BankAccountEntity(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
@ -25,12 +24,11 @@ class BankAccountEntity(
|
||||||
balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
|
balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
|
||||||
|
|
||||||
serverTransactionsRetentionDays: Int? = null,
|
serverTransactionsRetentionDays: Int? = null,
|
||||||
lastAccountUpdateTime: Instant? = null,
|
lastTransactionsRetrievalTime: Instant? = null,
|
||||||
retrievedTransactionsFrom: LocalDate? = null,
|
retrievedTransactionsFrom: LocalDate? = null,
|
||||||
|
|
||||||
bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
|
bookedTransactions: MutableList<AccountTransactionEntity> = mutableListOf(),
|
||||||
prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
|
prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
|
||||||
override var holdings: List<HoldingEntity> = emptyList(),
|
|
||||||
|
|
||||||
userSetDisplayName: String? = null,
|
userSetDisplayName: String? = null,
|
||||||
displayIndex: Int = 0,
|
displayIndex: Int = 0,
|
||||||
|
@ -46,15 +44,14 @@ class BankAccountEntity(
|
||||||
|
|
||||||
balance,
|
balance,
|
||||||
|
|
||||||
serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom,
|
serverTransactionsRetentionDays, lastTransactionsRetrievalTime, retrievedTransactionsFrom,
|
||||||
|
|
||||||
bookedTransactions as MutableList<AccountTransaction>, prebookedTransactions,
|
bookedTransactions as MutableList<AccountTransaction>, prebookedTransactions,
|
||||||
holdings,
|
|
||||||
|
|
||||||
userSetDisplayName, displayIndex,
|
userSetDisplayName, displayIndex,
|
||||||
hideAccount, includeInAutomaticAccountsUpdate
|
hideAccount, includeInAutomaticAccountsUpdate
|
||||||
) {
|
) {
|
||||||
constructor(id: Long, userId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList(), holdings: List<HoldingEntity> = emptyList()) : this(
|
constructor(id: Long, userId: Long, account: BankAccount, transactions: List<AccountTransactionEntity> = emptyList()) : this(
|
||||||
id, userId,
|
id, userId,
|
||||||
account.identifier, account.subAccountNumber, account.iban, account.productName,
|
account.identifier, account.subAccountNumber, account.iban, account.productName,
|
||||||
|
|
||||||
|
@ -66,9 +63,9 @@ class BankAccountEntity(
|
||||||
account.balance,
|
account.balance,
|
||||||
|
|
||||||
account.serverTransactionsRetentionDays,
|
account.serverTransactionsRetentionDays,
|
||||||
account.lastAccountUpdateTime, account.retrievedTransactionsFrom,
|
account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
|
||||||
|
|
||||||
transactions.toMutableList(), mutableListOf(), holdings,
|
transactions.toMutableList(), mutableListOf(),
|
||||||
|
|
||||||
account.userSetDisplayName, account.displayIndex,
|
account.userSetDisplayName, account.displayIndex,
|
||||||
account.hideAccount, account.includeInAutomaticAccountsUpdate
|
account.hideAccount, account.includeInAutomaticAccountsUpdate
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
package net.codinux.banking.dataaccess.entities
|
|
||||||
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import net.codinux.banking.client.model.Amount
|
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
|
|
||||||
class HoldingEntity(
|
|
||||||
val id: Long,
|
|
||||||
val userId: Long,
|
|
||||||
val bankAccountId: Long,
|
|
||||||
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
isin: String? = null,
|
|
||||||
wkn: String? = null,
|
|
||||||
|
|
||||||
quantity: Int? = null,
|
|
||||||
currency: String? = null,
|
|
||||||
|
|
||||||
totalBalance: Amount? = null,
|
|
||||||
marketValue: Amount? = null,
|
|
||||||
|
|
||||||
performancePercentage: Float? = null,
|
|
||||||
|
|
||||||
totalCostPrice: Amount? = null,
|
|
||||||
averageCostPrice: Amount? = null,
|
|
||||||
|
|
||||||
pricingTime: Instant? = null,
|
|
||||||
buyingDate: LocalDate? = null
|
|
||||||
) : Holding(name, isin, wkn, quantity, currency, totalBalance, marketValue, performancePercentage, totalCostPrice, averageCostPrice, pricingTime, buyingDate) {
|
|
||||||
|
|
||||||
constructor(id: Long, userId: Long, bankAccountId: Long, holding: Holding) : this(
|
|
||||||
id, userId, bankAccountId,
|
|
||||||
|
|
||||||
holding.name, holding.isin, holding.wkn,
|
|
||||||
|
|
||||||
holding.quantity, holding.currency,
|
|
||||||
|
|
||||||
holding.totalBalance, holding.marketValue,
|
|
||||||
holding.performancePercentage,
|
|
||||||
holding.totalCostPrice, holding.averageCostPrice,
|
|
||||||
|
|
||||||
holding.pricingTime, holding.buyingDate
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -23,7 +23,5 @@ fun ContentPane(
|
||||||
TransactionsList(uiState, uiSettings, isMobile)
|
TransactionsList(uiState, uiSettings, isMobile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
StateHandler(uiState, snackbarHostState)
|
StateHandler(uiState, snackbarHostState)
|
||||||
}
|
}
|
||||||
}
|
|
|
@ -2,11 +2,10 @@ package net.codinux.banking.ui.appskeleton
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.SnackbarHostState
|
import androidx.compose.material.SnackbarHostState
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.codinux.banking.ui.composables.StateHandler
|
|
||||||
import net.codinux.banking.ui.settings.UiSettings
|
import net.codinux.banking.ui.settings.UiSettings
|
||||||
import net.codinux.banking.ui.state.UiState
|
import net.codinux.banking.ui.state.UiState
|
||||||
|
|
||||||
|
@ -24,8 +23,6 @@ fun DesktopLayout(
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize().weight(1f).padding(start = 6.dp)) {
|
Column(Modifier.fillMaxSize().weight(1f).padding(start = 6.dp)) {
|
||||||
StateHandler(uiState, snackbarHostState)
|
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth().weight(1f)) {
|
Row(Modifier.fillMaxWidth().weight(1f)) {
|
||||||
ContentPane(scaffoldPadding, uiState, uiSettings, snackbarHostState, false)
|
ContentPane(scaffoldPadding, uiState, uiSettings, snackbarHostState, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.launch
|
||||||
import net.codinux.banking.ui.composables.BanksList
|
import net.codinux.banking.ui.composables.BanksList
|
||||||
import net.codinux.banking.ui.composables.NavigationMenuItem
|
import net.codinux.banking.ui.composables.NavigationMenuItem
|
||||||
import net.codinux.banking.ui.composables.settings.UiSettings
|
import net.codinux.banking.ui.composables.settings.UiSettings
|
||||||
import net.codinux.banking.ui.composables.text.ItemDivider
|
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
|
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
|
||||||
|
@ -70,7 +69,7 @@ fun SideMenuContent() {
|
||||||
Text("Version 1.0.0 Alpha 12", color = Color.LightGray)
|
Text("Version 1.0.0 Alpha 12", color = Color.LightGray)
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemDivider(color = Colors.DrawerDivider)
|
Divider(color = Colors.DrawerDivider)
|
||||||
|
|
||||||
Column(Modifier.padding(horizontal = 16.dp, vertical = 24.dp)) {
|
Column(Modifier.padding(horizontal = 16.dp, vertical = 24.dp)) {
|
||||||
Column(Modifier.height(ItemHeight), verticalArrangement = Arrangement.Center) {
|
Column(Modifier.height(ItemHeight), verticalArrangement = Arrangement.Center) {
|
||||||
|
@ -111,7 +110,7 @@ fun SideMenuContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accounts.isNotEmpty()) {
|
if (accounts.isNotEmpty()) {
|
||||||
ItemDivider(color = Colors.DrawerDivider)
|
Divider(color = Colors.DrawerDivider)
|
||||||
|
|
||||||
Column(Modifier.padding(16.dp)) {
|
Column(Modifier.padding(16.dp)) {
|
||||||
UiSettings(Modifier.fillMaxWidth().padding(bottom = VerticalSpacing), textColor)
|
UiSettings(Modifier.fillMaxWidth().padding(bottom = VerticalSpacing), textColor)
|
||||||
|
|
|
@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.LocalContentAlpha
|
import androidx.compose.material.LocalContentAlpha
|
||||||
import androidx.compose.material.LocalContentColor
|
import androidx.compose.material.LocalContentColor
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
@ -31,14 +34,14 @@ private val bankingGroupMapper = BankingGroupMapper()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
|
fun BankIcon(bank: BankInfo, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
|
||||||
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bank.bic, bankingGroupMapper.getBankingGroup(bank.name, bank.bic))) }
|
val iconUrl by remember(bank.bic) { mutableStateOf(bankIconService.findIconForBank(bank.name, bankingGroupMapper.getBankingGroup(bank.name, bank.bic))) }
|
||||||
|
|
||||||
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
|
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BankIcon(user: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
|
fun BankIcon(user: BankViewInfo?, modifier: Modifier = Modifier, iconModifier: Modifier = DefaultIconModifier, fallbackIcon: ImageVector? = null) {
|
||||||
val iconUrl = user?.let { bankIconService.findIconForBank(it.bankName, null, it.bankingGroup) }
|
val iconUrl = user?.let { bankIconService.findIconForBank(it.bankName, it.bankingGroup) }
|
||||||
|
|
||||||
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
|
BankIcon(iconUrl, modifier, iconModifier, fallbackIcon = fallbackIcon)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ fun IconForUrl(iconUrl: String, contentDescription: String, modifier: Modifier =
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (imageBitmap == null && iconUrl.endsWith(".svg") == false) { // SVG is not supported on Android
|
if (iconUrl.endsWith(".svg") == false) { // SVG is not supported on Android
|
||||||
coroutineScope.launch(Dispatchers.IOorDefault) {
|
coroutineScope.launch(Dispatchers.IOorDefault) {
|
||||||
val received = imageCache.getImageBitmap(iconUrl)
|
val received = imageCache.getImageBitmap(iconUrl)
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ fun NavigationMenuItem(
|
||||||
if (balance != null) {
|
if (balance != null) {
|
||||||
Text(
|
Text(
|
||||||
formatUtil.formatAmount(balance, calculator.getTransactionsCurrency(emptyList())),
|
formatUtil.formatAmount(balance, calculator.getTransactionsCurrency(emptyList())),
|
||||||
color = formatUtil.getColorForAmount(balance, showColoredAmounts),
|
color = if (showColoredAmounts) formatUtil.getColorForAmount(balance) else textColor,
|
||||||
modifier = Modifier.padding(start = 4.dp)
|
modifier = Modifier.padding(start = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,24 +59,17 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
uiState.transactionsRetrievedEvents.collect { event ->
|
uiState.transactionsRetrievedEvents.collect { event ->
|
||||||
var actionLabel = "Coolio"
|
val messagePrefix = if (event.newTransactions.isEmpty()) {
|
||||||
|
|
||||||
val messagePrefix = if (event.newTransactions.size == 1) {
|
|
||||||
"1 neuer Umsatz"
|
|
||||||
} else if (event.newTransactions.size > 1) {
|
|
||||||
"${event.newTransactions.size} neue Umsätze"
|
|
||||||
} else if (event.updatedHoldings.size == 1) {
|
|
||||||
"1 Kurse aktualisiert"
|
|
||||||
} else if (event.updatedHoldings.size > 1) {
|
|
||||||
"${event.updatedHoldings.size} Kurse aktualisiert"
|
|
||||||
} else {
|
|
||||||
actionLabel = "Na super"
|
|
||||||
"Keine neuen Umsätze"
|
"Keine neuen Umsätze"
|
||||||
|
} else if (event.newTransactions.size == 1) {
|
||||||
|
"1 Umsatz"
|
||||||
|
} else {
|
||||||
|
"${event.newTransactions.size} Umsätze"
|
||||||
}
|
}
|
||||||
|
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = "$messagePrefix für ${event.user.displayName} ${event.account.displayName}",
|
message = "$messagePrefix für ${event.user.displayName} ${event.account.displayName}",
|
||||||
actionLabel = actionLabel,
|
actionLabel = "Coolio",
|
||||||
duration = SnackbarDuration.Long
|
duration = SnackbarDuration.Long
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
package net.codinux.banking.ui.composables.text
|
|
||||||
|
|
||||||
import androidx.compose.material.Divider
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import net.codinux.banking.ui.config.Colors
|
|
||||||
import net.codinux.banking.ui.config.Style
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ItemDivider(color: Color = Colors.ItemDividerColor, thickness: Dp = Style.DividerThickness, modifier: Modifier = Modifier) {
|
|
||||||
Divider(color = color, thickness = thickness, modifier = modifier)
|
|
||||||
}
|
|
|
@ -13,9 +13,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import net.codinux.banking.client.model.Amount
|
import net.codinux.banking.client.model.Amount
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.dataaccess.entities.UserEntity
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
import net.codinux.banking.ui.config.Colors
|
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
import net.codinux.banking.ui.forms.RoundedCornersCard
|
import net.codinux.banking.ui.forms.RoundedCornersCard
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
|
@ -30,7 +28,6 @@ private val formatUtil = DI.formatUtil
|
||||||
fun GroupedTransactionsListItems(
|
fun GroupedTransactionsListItems(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
transactionsToDisplay: List<AccountTransactionViewModel>,
|
transactionsToDisplay: List<AccountTransactionViewModel>,
|
||||||
holdingsToDisplay: List<Holding>,
|
|
||||||
usersById: Map<Long, UserEntity>,
|
usersById: Map<Long, UserEntity>,
|
||||||
transactionsGrouping: TransactionsGrouping
|
transactionsGrouping: TransactionsGrouping
|
||||||
) {
|
) {
|
||||||
|
@ -44,35 +41,6 @@ fun GroupedTransactionsListItems(
|
||||||
|
|
||||||
|
|
||||||
LazyColumn(modifier, contentPadding = PaddingValues(bottom = 12.dp)) { // padding bottom = add the space the FAB sticks into the content area (= 26 - the 16 we add at the bottom of the expenses line)
|
LazyColumn(modifier, contentPadding = PaddingValues(bottom = 12.dp)) { // padding bottom = add the space the FAB sticks into the content area (= 26 - the 16 we add at the bottom of the expenses line)
|
||||||
if (holdingsToDisplay.isNotEmpty()) {
|
|
||||||
items(1) {
|
|
||||||
Column(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp)) {
|
|
||||||
Text(
|
|
||||||
text = "Depotwerte",
|
|
||||||
fontSize = 16.sp,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.padding(bottom = 2.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
Text("Bitte beachten: Der Abruf der Depotwerte ist sehr experimentell. Wir haben nur seitenweise Spezifikation und am Ende ein kleines Beispiel, " +
|
|
||||||
"welches sich selbst nicht an die Spezifikation hält, und keine realen Bankantworten, wir mussten es also 'blind' implementieren.",
|
|
||||||
color = Colors.Red600,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp).padding(top = 2.dp, bottom = 4.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
RoundedCornersCard {
|
|
||||||
Column(Modifier.background(Color.White)) {
|
|
||||||
holdingsToDisplay.forEachIndexed { index, holding ->
|
|
||||||
// key(statementOfHoldings.id) {
|
|
||||||
HoldingListItem(holding, index % 2 == 1, index < holdingsToDisplay.size - 1)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items(groupedByDate.keys.sortedDescending()) { groupingDate ->
|
items(groupedByDate.keys.sortedDescending()) { groupingDate ->
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
package net.codinux.banking.ui.composables.transactions
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import net.codinux.banking.client.model.Amount
|
|
||||||
import net.codinux.banking.client.model.DefaultValues
|
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.ui.composables.text.ItemDivider
|
|
||||||
import net.codinux.banking.ui.config.Colors
|
|
||||||
import net.codinux.banking.ui.config.DI
|
|
||||||
import net.codinux.banking.ui.config.Style
|
|
||||||
|
|
||||||
private val uiSettings = DI.uiSettings
|
|
||||||
|
|
||||||
private val formatUtil = DI.formatUtil
|
|
||||||
|
|
||||||
private val verticalSpace = 6.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HoldingListItem(holding: Holding, isOddItem: Boolean = false, isNotLastItem: Boolean = true, fallbackCurrency: String = DefaultValues.DefaultCurrency) {
|
|
||||||
|
|
||||||
// TODO: also regard showBalance?
|
|
||||||
val showColoredAmounts by uiSettings.showColoredAmounts.collectAsState()
|
|
||||||
|
|
||||||
val zebraStripes by uiSettings.zebraStripes.collectAsState()
|
|
||||||
|
|
||||||
val showBankIcons by uiSettings.showBankIcons.collectAsState()
|
|
||||||
|
|
||||||
val backgroundColor = if (zebraStripes && isOddItem) Colors.ZebraStripesColor else Color.White
|
|
||||||
|
|
||||||
val currency = holding.currency ?: fallbackCurrency
|
|
||||||
|
|
||||||
|
|
||||||
Column(Modifier.fillMaxWidth().background(backgroundColor).padding(horizontal = 6.dp, vertical = 6.dp)) {
|
|
||||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
if (showBankIcons) {
|
|
||||||
// BankIcon(user, Modifier.padding(end = 6.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
holding.name,
|
|
||||||
fontWeight = Style.ListItemHeaderWeight,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
modifier = Modifier.weight(1f).padding(end = 4.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(Modifier.width(70.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
val performance = holding.performancePercentage
|
|
||||||
if (performance != null) {
|
|
||||||
Text(
|
|
||||||
text = formatUtil.formatPercentage(performance),
|
|
||||||
color = formatUtil.getColorForAmount(Amount(performance.toString()), showColoredAmounts)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth().padding(top = verticalSpace), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Row(Modifier.weight(1f).padding(end = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
// TODO: set maxLines = 1 and TextOverflow.Ellipsis
|
|
||||||
if (holding.quantity != null) {
|
|
||||||
Text(holding.quantity.toString() + " Stück, ")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (holding.averageCostPrice != null) {
|
|
||||||
Text(formatUtil.formatAmount(holding.averageCostPrice!!, currency) + " ➞ ")
|
|
||||||
}
|
|
||||||
|
|
||||||
val marketValue = holding.marketValue
|
|
||||||
if (marketValue != null) {
|
|
||||||
Text(formatUtil.formatAmount(marketValue, currency), color = formatUtil.getColorForAmount(marketValue, showColoredAmounts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(Modifier.widthIn(90.dp, 210.dp), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
if (holding.totalCostPrice != null) {
|
|
||||||
Text(formatUtil.formatAmount(holding.totalCostPrice!!, currency) + " ➞ ")
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalBalance = holding.totalBalance
|
|
||||||
if (totalBalance != null) {
|
|
||||||
Text(formatUtil.formatAmount(totalBalance, currency), color = formatUtil.getColorForAmount(totalBalance, showColoredAmounts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (isNotLastItem) {
|
|
||||||
ItemDivider()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -9,17 +9,14 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.codinux.banking.client.model.User
|
import net.codinux.banking.client.model.User
|
||||||
import net.codinux.banking.ui.composables.BankIcon
|
import net.codinux.banking.ui.composables.BankIcon
|
||||||
import net.codinux.banking.ui.composables.text.ItemDivider
|
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
import net.codinux.banking.ui.config.DI
|
import net.codinux.banking.ui.config.DI
|
||||||
import net.codinux.banking.ui.config.Style
|
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
|
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
|
||||||
|
|
||||||
|
@ -35,7 +32,7 @@ fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, i
|
||||||
|
|
||||||
val showColoredAmounts by uiSettings.showColoredAmounts.collectAsState()
|
val showColoredAmounts by uiSettings.showColoredAmounts.collectAsState()
|
||||||
|
|
||||||
val backgroundColor = if (zebraStripes && itemIndex % 2 == 1) Colors.ZebraStripesColor else Color.White
|
val backgroundColor = if (zebraStripes && itemIndex % 2 == 1) Colors.Zinc100_50 else Color.White
|
||||||
|
|
||||||
val bottomPadding = 56.dp
|
val bottomPadding = 56.dp
|
||||||
|
|
||||||
|
@ -83,9 +80,8 @@ fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, i
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = transaction.otherPartyName ?: transaction.postingText ?: "",
|
text = transaction.otherPartyName ?: "",
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
fontWeight = Style.ListItemHeaderWeight,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
@ -130,6 +126,6 @@ fun TransactionListItem(user: User?, transaction: AccountTransactionViewModel, i
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemIndex < countItems - 1) {
|
if (itemIndex < countItems - 1) {
|
||||||
ItemDivider()
|
Divider(color = Colors.Zinc200, thickness = 1.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -38,12 +38,6 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
|
||||||
derivedStateOf { filterService.filterAccounts(transactions, transactionsFilter) }
|
derivedStateOf { filterService.filterAccounts(transactions, transactionsFilter) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val holdings by uiState.holdings.collectAsState()
|
|
||||||
|
|
||||||
val holdingsToDisplay by remember(transactionsFilter, holdings) {
|
|
||||||
derivedStateOf { filterService.filterHoldings(holdings, transactionsFilter) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val showBalance by uiSettings.showBalance.collectAsState()
|
val showBalance by uiSettings.showBalance.collectAsState()
|
||||||
|
|
||||||
val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
|
val transactionsGrouping by uiSettings.transactionsGrouping.collectAsState()
|
||||||
|
@ -65,15 +59,9 @@ fun TransactionsList(uiState: UiState, uiSettings: UiSettings, isMobile: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionsGrouping != TransactionsGrouping.None) {
|
if (transactionsGrouping != TransactionsGrouping.None) {
|
||||||
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, holdingsToDisplay, usersById, transactionsGrouping)
|
GroupedTransactionsListItems(transactionsListModifier, transactionsToDisplay, usersById, transactionsGrouping)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
|
LazyColumn(transactionsListModifier, contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp)) {
|
||||||
itemsIndexed(holdingsToDisplay) { index, holding ->
|
|
||||||
// key(holding.isin) {
|
|
||||||
HoldingListItem(holding, index % 2 == 1, index < holdingsToDisplay.size - 1)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(transactionsToDisplay) { index, transaction ->
|
itemsIndexed(transactionsToDisplay) { index, transaction ->
|
||||||
key(transaction.id) {
|
key(transaction.id) {
|
||||||
TransactionListItem(usersById[transaction.userId], transaction, index, transactionsToDisplay.size)
|
TransactionListItem(usersById[transaction.userId], transaction, index, transactionsToDisplay.size)
|
||||||
|
|
|
@ -52,9 +52,4 @@ object Colors {
|
||||||
|
|
||||||
val Emerald700 = Color(4, 120, 87)
|
val Emerald700 = Color(4, 120, 87)
|
||||||
|
|
||||||
|
|
||||||
val ZebraStripesColor = Zinc100_50
|
|
||||||
|
|
||||||
val ItemDividerColor = Colors.Zinc200
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ package net.codinux.banking.ui.config
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
object Style {
|
object Style {
|
||||||
|
@ -13,9 +12,4 @@ object Style {
|
||||||
|
|
||||||
val HeaderFontWeight: FontWeight = FontWeight.Bold
|
val HeaderFontWeight: FontWeight = FontWeight.Bold
|
||||||
|
|
||||||
val ListItemHeaderWeight = FontWeight.Medium // couldn't believe it, the FontWeights look different on Desktop and Android
|
|
||||||
|
|
||||||
|
|
||||||
val DividerThickness = 1.dp
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -56,7 +56,7 @@ fun TransferMoneyDialog(
|
||||||
|
|
||||||
var recipientName by remember { mutableStateOf(data.recipientName ?: "") }
|
var recipientName by remember { mutableStateOf(data.recipientName ?: "") }
|
||||||
var recipientAccountIdentifier by remember { mutableStateOf(data.recipientAccountIdentifier ?: "") }
|
var recipientAccountIdentifier by remember { mutableStateOf(data.recipientAccountIdentifier ?: "") }
|
||||||
var amount by remember { mutableStateOf(data.amount.toString()) }
|
var amount by remember { mutableStateOf(data.amount?.amount ?: "") }
|
||||||
var paymentReference by remember { mutableStateOf(data.reference ?: "") }
|
var paymentReference by remember { mutableStateOf(data.reference ?: "") }
|
||||||
val accountSupportsInstantTransfer by remember(senderAccount) { derivedStateOf { senderAccount.supportsInstantTransfer } }
|
val accountSupportsInstantTransfer by remember(senderAccount) { derivedStateOf { senderAccount.supportsInstantTransfer } }
|
||||||
var instantTransfer by remember { mutableStateOf(false) }
|
var instantTransfer by remember { mutableStateOf(false) }
|
||||||
|
@ -202,10 +202,10 @@ fun TransferMoneyDialog(
|
||||||
minTextLengthForSearch = 0,
|
minTextLengthForSearch = 0,
|
||||||
modifier = Modifier.weight(1f).focusRequester(amountFocus),
|
modifier = Modifier.weight(1f).focusRequester(amountFocus),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Next),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Next),
|
||||||
getItemTitle = { suggestion -> suggestion.amount.toString() },
|
getItemTitle = { suggestion -> suggestion.amount.amount },
|
||||||
onEnteredTextChanged = { amount = it },
|
onEnteredTextChanged = { amount = it },
|
||||||
onSelectedItemChanged = {
|
onSelectedItemChanged = {
|
||||||
amount = it?.amount.toString()
|
amount = it?.amount?.amount ?: ""
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
paymentReference = it.reference
|
paymentReference = it.reference
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package net.codinux.banking.ui.extensions
|
||||||
|
|
||||||
|
import net.codinux.banking.client.model.Amount
|
||||||
|
|
||||||
|
fun Amount.toBigDecimal(): Double = this.amount.toDouble()
|
|
@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.codinux.banking.ui.composables.text.ItemDivider
|
import net.codinux.banking.ui.config.Colors
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -125,7 +125,7 @@ fun <T> AutocompleteTextField(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDividersBetweenItems && index < suggestions.size - 1) {
|
if (showDividersBetweenItems && index < suggestions.size - 1) {
|
||||||
ItemDivider(modifier = Modifier.padding(horizontal = 8.dp))
|
Divider(color = Colors.Zinc200, thickness = 1.dp, modifier = Modifier.padding(horizontal = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ data class AccountTransactionViewModel(
|
||||||
val valueDate: LocalDate,
|
val valueDate: LocalDate,
|
||||||
val otherPartyName: String? = null,
|
val otherPartyName: String? = null,
|
||||||
|
|
||||||
val postingText: String? = null,
|
val bookingText: String? = null,
|
||||||
val userSetDisplayName: String? = null,
|
val userSetDisplayName: String? = null,
|
||||||
val category: String? = null
|
val category: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -2,12 +2,10 @@ package net.codinux.banking.ui.model.events
|
||||||
|
|
||||||
import net.codinux.banking.client.model.BankAccount
|
import net.codinux.banking.client.model.BankAccount
|
||||||
import net.codinux.banking.client.model.User
|
import net.codinux.banking.client.model.User
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
|
|
||||||
data class AccountTransactionsRetrievedEvent(
|
data class AccountTransactionsRetrievedEvent(
|
||||||
val user: User,
|
val user: User,
|
||||||
val account: BankAccount,
|
val account: BankAccount,
|
||||||
val newTransactions: List<AccountTransactionViewModel>,
|
val newTransactions: List<AccountTransactionViewModel>
|
||||||
val updatedHoldings: List<Holding> = emptyList()
|
|
||||||
)
|
)
|
|
@ -10,8 +10,6 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import net.codinux.banking.ui.composables.text.HeaderText
|
import net.codinux.banking.ui.composables.text.HeaderText
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
|
@ -24,10 +22,6 @@ fun FullscreenViewBase(
|
||||||
confirmButtonEnabled: Boolean = true,
|
confirmButtonEnabled: Boolean = true,
|
||||||
onClosed: () -> Unit,
|
onClosed: () -> Unit,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
|
||||||
Dialog(
|
|
||||||
onClosed,
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize().zIndex(1000f).background(Color.White).padding(8.dp)) {
|
Column(Modifier.fillMaxSize().zIndex(1000f).background(Color.White).padding(8.dp)) {
|
||||||
|
|
||||||
|
@ -62,4 +56,3 @@ fun FullscreenViewBase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
|
@ -1,15 +1,11 @@
|
||||||
package net.codinux.banking.ui.service
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
||||||
import net.codinux.banking.dataaccess.entities.HoldingEntity
|
|
||||||
import net.codinux.banking.dataaccess.entities.UserEntity
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
import net.codinux.banking.ui.model.AccountTransactionsFilter
|
import net.codinux.banking.ui.model.AccountTransactionsFilter
|
||||||
import net.codinux.banking.ui.model.BankAccountFilter
|
import net.codinux.banking.ui.model.BankAccountFilter
|
||||||
|
|
||||||
|
|
||||||
private const val SearchTermOrSeparatorSymbol = ','
|
|
||||||
|
|
||||||
class AccountTransactionsFilterService {
|
class AccountTransactionsFilterService {
|
||||||
|
|
||||||
fun filterAccounts(transactions: List<AccountTransactionViewModel>, filter: AccountTransactionsFilter): List<AccountTransactionViewModel> {
|
fun filterAccounts(transactions: List<AccountTransactionViewModel>, filter: AccountTransactionsFilter): List<AccountTransactionViewModel> {
|
||||||
|
@ -25,13 +21,11 @@ class AccountTransactionsFilterService {
|
||||||
appliedAccountFilter = appliedAccountFilter.filter { it.valueDate.year == year && (month == null || it.valueDate.monthNumber == month) }
|
appliedAccountFilter = appliedAccountFilter.filter { it.valueDate.year == year && (month == null || it.valueDate.monthNumber == month) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchTerms = filter.searchTerm.split(SearchTermOrSeparatorSymbol).filter { it.isNotBlank() }
|
val searchTerm = filter.searchTerm
|
||||||
return if (searchTerms.isEmpty()) {
|
return if (searchTerm.isBlank()) {
|
||||||
appliedAccountFilter
|
appliedAccountFilter
|
||||||
} else {
|
} else {
|
||||||
appliedAccountFilter.filter { transaction ->
|
appliedAccountFilter.filter { matchesSearchTerm(it, searchTerm) }
|
||||||
searchTerms.any { matchesSearchTerm(transaction, it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,40 +43,6 @@ class AccountTransactionsFilterService {
|
||||||
|| (transaction.otherPartyName != null && transaction.otherPartyName.contains(searchTerm, true))
|
|| (transaction.otherPartyName != null && transaction.otherPartyName.contains(searchTerm, true))
|
||||||
|
|
||||||
|
|
||||||
fun filterHoldings(holdings: List<HoldingEntity>, filter: AccountTransactionsFilter): List<HoldingEntity> {
|
|
||||||
if (filter.year != null) { // TODO: check if it's current year (and month)
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
var appliedHoldingFilter = if (filter.showAllAccounts) {
|
|
||||||
holdings
|
|
||||||
} else {
|
|
||||||
holdings.filter { matchesFilter(it, filter.selectedAccounts.value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val searchTerm = filter.searchTerm
|
|
||||||
return if (searchTerm.isBlank()) {
|
|
||||||
appliedHoldingFilter
|
|
||||||
} else {
|
|
||||||
appliedHoldingFilter.filter { matchesSearchTerm(it, searchTerm) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchesFilter(holding: HoldingEntity, filter: List<BankAccountFilter>): Boolean =
|
|
||||||
filter.any { (user, bankAccount) ->
|
|
||||||
if (bankAccount != null) {
|
|
||||||
holding.bankAccountId == bankAccount.id
|
|
||||||
} else {
|
|
||||||
holding.userId == user.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun matchesSearchTerm(holding: HoldingEntity, searchTerm: String): Boolean =
|
|
||||||
holding.name.contains(searchTerm, true)
|
|
||||||
|| holding.isin?.contains(searchTerm, true) == true
|
|
||||||
|| holding.wkn?.contains(searchTerm, true) == true
|
|
||||||
|
|
||||||
|
|
||||||
fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
|
fun isSelected(user: UserEntity, transactionsFilter: AccountTransactionsFilter): Boolean {
|
||||||
if (transactionsFilter.showAllAccounts) {
|
if (transactionsFilter.showAllAccounts) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -32,9 +32,9 @@ class BankDataImporterAndExporter {
|
||||||
|
|
||||||
private fun formatAmount(amount: Amount, decimalSeparator: Char): String =
|
private fun formatAmount(amount: Amount, decimalSeparator: Char): String =
|
||||||
if (decimalSeparator == '.') {
|
if (decimalSeparator == '.') {
|
||||||
amount.toString()
|
amount.amount
|
||||||
} else {
|
} else {
|
||||||
amount.toString().replace('.', decimalSeparator)
|
amount.amount.replace('.', decimalSeparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,9 +5,9 @@ import net.codinux.banking.client.model.User
|
||||||
|
|
||||||
class BankIconService { // TODO: extract to a common library
|
class BankIconService { // TODO: extract to a common library
|
||||||
|
|
||||||
fun findIconForBank(user: User) = findIconForBank(user.bankName, user.bic, user.bankingGroup)
|
fun findIconForBank(user: User) = findIconForBank(user.bankName, user.bankingGroup)
|
||||||
|
|
||||||
fun findIconForBank(bankName: String, bic: String? = null, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) {
|
fun findIconForBank(bankName: String, bankingGroup: BankingGroup? = null): String? = when (bankingGroup) {
|
||||||
BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png"
|
BankingGroup.Sparkasse -> "https://sparkasse.de/favicon-32x32.png"
|
||||||
BankingGroup.DKB -> "https://www.ib.dkb.de/favicon.ico"
|
BankingGroup.DKB -> "https://www.ib.dkb.de/favicon.ico"
|
||||||
BankingGroup.OldenburgischeLandesbank -> "https://olb.de/assets/img/icon/olb/favicon-32x32.png"
|
BankingGroup.OldenburgischeLandesbank -> "https://olb.de/assets/img/icon/olb/favicon-32x32.png"
|
||||||
|
@ -33,11 +33,6 @@ class BankIconService { // TODO: extract to a common library
|
||||||
|
|
||||||
BankingGroup.N26 -> "https://n26.de/favicon.ico"
|
BankingGroup.N26 -> "https://n26.de/favicon.ico"
|
||||||
|
|
||||||
else -> findByBicOrName(bankName, bic)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findByBicOrName(bankName: String, bic: String?): String? = when {
|
|
||||||
bic?.startsWith("BDWBDEMM") == true || bankName.equals("Baader Bank", true) -> "https://www.baaderbank.de/favicon.ico"
|
|
||||||
else -> null // TODO: call Favicon web service
|
else -> null // TODO: call Favicon web service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,19 @@ import kotlinx.coroutines.*
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||||
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
|
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
|
||||||
import net.codinux.banking.client.fints4k.FinTsClientOptions
|
|
||||||
import net.codinux.banking.client.model.*
|
import net.codinux.banking.client.model.*
|
||||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||||
import net.codinux.banking.client.model.options.RetrieveTransactions
|
import net.codinux.banking.client.model.options.RetrieveTransactions
|
||||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||||
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
|
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
|
||||||
import net.codinux.banking.client.model.response.*
|
import net.codinux.banking.client.model.response.*
|
||||||
import net.codinux.banking.client.model.securitiesaccount.Holding
|
|
||||||
import net.codinux.banking.client.service.BankingModelService
|
import net.codinux.banking.client.service.BankingModelService
|
||||||
import net.codinux.banking.dataaccess.BankingRepository
|
import net.codinux.banking.dataaccess.BankingRepository
|
||||||
import net.codinux.banking.dataaccess.entities.*
|
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
|
||||||
|
import net.codinux.banking.dataaccess.entities.BankAccountEntity
|
||||||
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
|
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||||
|
import net.codinux.banking.fints.config.FinTsClientOptions
|
||||||
import net.codinux.banking.ui.IOorDefault
|
import net.codinux.banking.ui.IOorDefault
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
import net.codinux.banking.ui.model.BankInfo
|
import net.codinux.banking.ui.model.BankInfo
|
||||||
|
@ -34,7 +36,7 @@ class BankingService(
|
||||||
private val bankFinder: BankFinder
|
private val bankFinder: BankFinder
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val client = FinTs4kBankingClient(FinTsClientOptions(true, closeDialogs = false), SimpleBankingClientCallback { tanChallenge, callback ->
|
private val client = FinTs4kBankingClient(FinTsClientConfiguration(FinTsClientOptions(true)), SimpleBankingClientCallback { tanChallenge, callback ->
|
||||||
uiState.receivedTanChallenge(tanChallenge, callback)
|
uiState.receivedTanChallenge(tanChallenge, callback)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -48,7 +50,6 @@ class BankingService(
|
||||||
uiState.users.value = getAllUsers()
|
uiState.users.value = getAllUsers()
|
||||||
|
|
||||||
uiState.transactions.value = getAllAccountTransactionsAsViewModel()
|
uiState.transactions.value = getAllAccountTransactionsAsViewModel()
|
||||||
uiState.holdings.value = uiState.users.value.flatMap { it.accounts }.flatMap { it.holdings }
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
log.error(e) { "Could not read all user accounts and account transactions from repository" }
|
log.error(e) { "Could not read all user accounts and account transactions from repository" }
|
||||||
}
|
}
|
||||||
|
@ -113,7 +114,6 @@ class BankingService(
|
||||||
uiState.users.value = users
|
uiState.users.value = users
|
||||||
|
|
||||||
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
|
updateTransactionsInUi(newUserEntity.accounts.flatMap { it.bookedTransactionsEntities })
|
||||||
updateHoldingsInUi(newUserEntity.accounts.flatMap { it.holdings }, emptyList())
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
log.error(e) { "Could not save user account ${response.user}" }
|
log.error(e) { "Could not save user account ${response.user}" }
|
||||||
}
|
}
|
||||||
|
@ -171,52 +171,14 @@ class BankingService(
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val existingHoldingsByIsin = account.holdings.associateBy { it.identifier }
|
|
||||||
val retrievedHoldingsByIsin = response.holdings.associateBy { it.identifier }
|
|
||||||
|
|
||||||
val (newHoldings, updatedRetrievedHoldings) = response.holdings.partition { existingHoldingsByIsin.keys.contains(it.identifier) == false }
|
|
||||||
val (updatedExistingHoldings, deletedHoldings) = account.holdings.partition { retrievedHoldingsByIsin.keys.contains(it.identifier) }
|
|
||||||
|
|
||||||
val persistedNewHoldings = bankingRepository.persistHoldings(account, newHoldings)
|
|
||||||
bankingRepository.updateHoldings(updateHoldings(updatedExistingHoldings, updatedRetrievedHoldings))
|
|
||||||
bankingRepository.deleteHoldings(deletedHoldings)
|
|
||||||
|
|
||||||
account.holdings = account.holdings.toMutableList().apply {
|
|
||||||
addAll(persistedNewHoldings)
|
|
||||||
removeAll(deletedHoldings)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHoldingsInUi(persistedNewHoldings, deletedHoldings)
|
|
||||||
|
|
||||||
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
|
val transactionsViewModel = updateTransactionsInUi(newTransactionsEntities)
|
||||||
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(user, account, transactionsViewModel, response.holdings))
|
uiState.dispatchNewTransactionsRetrieved(AccountTransactionsRetrievedEvent(user, account, transactionsViewModel))
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
log.error(e) { "Could not save updated account transactions for user $user" }
|
log.error(e) { "Could not save updated account transactions for user $user" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateHoldings(updatedExistingHoldings: List<HoldingEntity>, updatedRetrievedHoldings: List<Holding>): List<HoldingEntity> =
|
|
||||||
updatedExistingHoldings.onEach { holding ->
|
|
||||||
updatedRetrievedHoldings.firstOrNull { it.identifier == holding.identifier }?.let { retrievedHolding ->
|
|
||||||
holding.name = retrievedHolding.name
|
|
||||||
holding.isin = retrievedHolding.isin
|
|
||||||
holding.wkn = retrievedHolding.wkn
|
|
||||||
|
|
||||||
holding.quantity = retrievedHolding.quantity
|
|
||||||
holding.currency = retrievedHolding.currency
|
|
||||||
|
|
||||||
holding.totalBalance = retrievedHolding.totalBalance
|
|
||||||
holding.marketValue = retrievedHolding.marketValue
|
|
||||||
holding.performancePercentage = retrievedHolding.performancePercentage
|
|
||||||
holding.totalCostPrice = retrievedHolding.totalCostPrice
|
|
||||||
holding.averageCostPrice = retrievedHolding.averageCostPrice
|
|
||||||
|
|
||||||
holding.pricingTime = retrievedHolding.pricingTime
|
|
||||||
holding.buyingDate = retrievedHolding.buyingDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTransactionsInUi(addedTransactions: List<AccountTransactionEntity>): List<AccountTransactionViewModel> {
|
private fun updateTransactionsInUi(addedTransactions: List<AccountTransactionEntity>): List<AccountTransactionViewModel> {
|
||||||
val transactionsViewModel = addedTransactions.map { AccountTransactionViewModel(it) }
|
val transactionsViewModel = addedTransactions.map { AccountTransactionViewModel(it) }
|
||||||
|
|
||||||
|
@ -227,13 +189,6 @@ class BankingService(
|
||||||
return transactionsViewModel
|
return transactionsViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateHoldingsInUi(persistedNewHoldings: List<HoldingEntity>, deletedHoldings: List<HoldingEntity>) {
|
|
||||||
val allHoldings = uiState.holdings.value.toMutableList()
|
|
||||||
allHoldings.removeAll(deletedHoldings)
|
|
||||||
allHoldings.addAll(persistedNewHoldings)
|
|
||||||
uiState.holdings.value = allHoldings.sortedByDescending { it.pricingTime }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
suspend fun transferMoney(user: UserEntity, account: BankAccountEntity,
|
suspend fun transferMoney(user: UserEntity, account: BankAccountEntity,
|
||||||
recipientName: String, recipientAccountIdentifier: String, amount: Amount, currency: String,
|
recipientName: String, recipientAccountIdentifier: String, amount: Amount, currency: String,
|
||||||
|
|
|
@ -1,26 +1,32 @@
|
||||||
package net.codinux.banking.ui.service
|
package net.codinux.banking.ui.service
|
||||||
|
|
||||||
import net.codinux.banking.client.model.*
|
import net.codinux.banking.client.model.Amount
|
||||||
|
import net.codinux.banking.client.model.User
|
||||||
import net.codinux.banking.dataaccess.entities.UserEntity
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
|
import net.codinux.banking.ui.extensions.toBigDecimal
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
import net.codinux.banking.ui.model.AccountTransactionsFilter
|
import net.codinux.banking.ui.model.AccountTransactionsFilter
|
||||||
|
|
||||||
class CalculatorService {
|
class CalculatorService {
|
||||||
|
|
||||||
fun sumTransactions(transactions: Collection<AccountTransactionViewModel>): Amount =
|
fun sumTransactions(transactions: Collection<AccountTransactionViewModel>): Amount =
|
||||||
transactions.map { it.amount }.sum()
|
// TODO: find a better solution
|
||||||
|
Amount(transactions.sumOf { it.amount.toBigDecimal() }.toString())
|
||||||
|
|
||||||
fun calculateBalanceOfUser(user: User): Amount =
|
fun calculateBalanceOfUser(user: User): Amount =
|
||||||
sumAmounts(user.accounts.map { it.balance })
|
sumAmounts(user.accounts.map { it.balance })
|
||||||
|
|
||||||
fun sumAmounts(amounts: Collection<Amount>): Amount =
|
fun sumAmounts(amounts: Collection<Amount>): Amount =
|
||||||
amounts.sum()
|
// TODO: find a better solution
|
||||||
|
Amount(amounts.sumOf { it.toBigDecimal() }.toString())
|
||||||
|
|
||||||
fun sumIncome(transactions: Collection<AccountTransactionViewModel>): Amount =
|
fun sumIncome(transactions: Collection<AccountTransactionViewModel>): Amount =
|
||||||
sumAmounts(transactions.map { it.amount }.filterNot { it.isNegative })
|
// TODO: find a better solution
|
||||||
|
Amount(transactions.map { it.amount.toBigDecimal() }.filter { it > 0 }.sum().toString())
|
||||||
|
|
||||||
fun sumExpenses(transactions: Collection<AccountTransactionViewModel>): Amount =
|
fun sumExpenses(transactions: Collection<AccountTransactionViewModel>): Amount =
|
||||||
sumAmounts(transactions.map { it.amount }.filter { it.isNegative })
|
// TODO: find a better solution
|
||||||
|
Amount(transactions.map { it.amount.toBigDecimal() }.filter { it < 0 }.sum().toString())
|
||||||
|
|
||||||
fun calculateBalanceOfDisplayedTransactions(transactions: Collection<AccountTransactionViewModel>, users: Collection<UserEntity>, filter: AccountTransactionsFilter): Amount {
|
fun calculateBalanceOfDisplayedTransactions(transactions: Collection<AccountTransactionViewModel>, users: Collection<UserEntity>, filter: AccountTransactionsFilter): Amount {
|
||||||
if (filter.noFiltersApplied) {
|
if (filter.noFiltersApplied) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package net.codinux.banking.ui.service
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import net.codinux.banking.client.model.Amount
|
import net.codinux.banking.client.model.Amount
|
||||||
import net.codinux.banking.client.model.isNegative
|
|
||||||
import net.codinux.banking.ui.config.Colors
|
import net.codinux.banking.ui.config.Colors
|
||||||
import net.codinux.banking.ui.model.TransactionsGrouping
|
import net.codinux.banking.ui.model.TransactionsGrouping
|
||||||
|
|
||||||
|
@ -56,26 +55,8 @@ class FormatUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun formatAmount(amount: Amount, currency: String): String {
|
fun formatAmount(amount: Amount, currency: String): String { // TODO: find a better way
|
||||||
return "${formatToTwoDecimalPlaces(amount.toString())} ${formatCurrency(currency)}"
|
val parts = amount.amount.split('.')
|
||||||
}
|
|
||||||
|
|
||||||
fun formatCurrency(currency: String): String = when (currency) {
|
|
||||||
"EUR" -> "€"
|
|
||||||
else -> currency
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getColorForAmount(amount: Amount, showColoredAmounts: Boolean = true): Color = when {
|
|
||||||
showColoredAmounts == false -> Color.Unspecified
|
|
||||||
amount.isNegative -> Colors.Red600
|
|
||||||
else -> Colors.Green600
|
|
||||||
}
|
|
||||||
|
|
||||||
fun formatPercentage(performance: Float): String =
|
|
||||||
(if (performance > 0) "+" else "") + formatToTwoDecimalPlaces(performance.toString()) + " %"
|
|
||||||
|
|
||||||
private fun formatToTwoDecimalPlaces(amount: String): String { // TODO: find a better way
|
|
||||||
val parts = amount.split('.')
|
|
||||||
|
|
||||||
var integerPart = parts[0]
|
var integerPart = parts[0]
|
||||||
val isNegative = integerPart.startsWith("-")
|
val isNegative = integerPart.startsWith("-")
|
||||||
|
@ -89,7 +70,19 @@ class FormatUtil {
|
||||||
|
|
||||||
val decimalPart = if (parts.size == 2) parts[1] else "00"
|
val decimalPart = if (parts.size == 2) parts[1] else "00"
|
||||||
|
|
||||||
return "$integerPart,${decimalPart.padEnd(2, '0').substring(0, 2)}"
|
// TODO: add thousands separator
|
||||||
|
return "$integerPart,${decimalPart.padEnd(2, '0').substring(0, 2)} ${formatCurrency(currency)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun formatCurrency(currency: String): String = when (currency) {
|
||||||
|
"EUR" -> "€"
|
||||||
|
else -> currency
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getColorForAmount(amount: Amount, showColoredAmounts: Boolean = true): Color = when {
|
||||||
|
showColoredAmounts == false -> Color.Unspecified
|
||||||
|
amount.amount.startsWith("-") -> Colors.Red600
|
||||||
|
else -> Colors.Green600
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ class RecipientFinder(private val bankFinder: BankFinder) {
|
||||||
|
|
||||||
transactionsByIban = transactions.filter { it.otherPartyAccountId != null }.groupBy { it.otherPartyAccountId!! }
|
transactionsByIban = transactions.filter { it.otherPartyAccountId != null }.groupBy { it.otherPartyAccountId!! }
|
||||||
.mapValues { it.value.map {
|
.mapValues { it.value.map {
|
||||||
PaymentDataSuggestion(it.reference ?: "", Amount(it.amount.toString().replace("-", "")), it.currency, it.valueDate)
|
PaymentDataSuggestion(it.reference ?: "", Amount(it.amount.amount.replace("-", "")), it.currency, it.valueDate)
|
||||||
}.toSet().sortedByDescending { it.valueDate } }
|
}.toSet().sortedByDescending { it.valueDate } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package net.codinux.banking.ui.service
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.Month
|
import kotlinx.datetime.Month
|
||||||
import net.codinux.banking.client.model.extensions.minusDays
|
import net.codinux.banking.fints.extensions.minusDays
|
||||||
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
import net.codinux.banking.ui.model.AccountTransactionViewModel
|
||||||
import net.codinux.banking.ui.model.TransactionsGrouping
|
import net.codinux.banking.ui.model.TransactionsGrouping
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import net.codinux.banking.client.model.tan.EnterTanResult
|
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||||
import net.codinux.banking.client.model.tan.TanChallenge
|
import net.codinux.banking.client.model.tan.TanChallenge
|
||||||
import net.codinux.banking.dataaccess.entities.HoldingEntity
|
|
||||||
import net.codinux.banking.dataaccess.entities.UserEntity
|
import net.codinux.banking.dataaccess.entities.UserEntity
|
||||||
import net.codinux.banking.ui.model.*
|
import net.codinux.banking.ui.model.*
|
||||||
import net.codinux.banking.ui.model.error.ApplicationError
|
import net.codinux.banking.ui.model.error.ApplicationError
|
||||||
|
@ -22,8 +21,6 @@ class UiState : ViewModel() {
|
||||||
|
|
||||||
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
|
val transactions = MutableStateFlow<List<AccountTransactionViewModel>>(emptyList())
|
||||||
|
|
||||||
val holdings = MutableStateFlow<List<HoldingEntity>>(emptyList())
|
|
||||||
|
|
||||||
val transactionsRetrievedEvents = MutableSharedFlow<AccountTransactionsRetrievedEvent>()
|
val transactionsRetrievedEvents = MutableSharedFlow<AccountTransactionsRetrievedEvent>()
|
||||||
|
|
||||||
suspend fun dispatchNewTransactionsRetrieved(event: AccountTransactionsRetrievedEvent) {
|
suspend fun dispatchNewTransactionsRetrieved(event: AccountTransactionsRetrievedEvent) {
|
||||||
|
|
|
@ -134,86 +134,3 @@ FROM AccountTransaction WHERE userId = ?;
|
||||||
getTransactionWithId:
|
getTransactionWithId:
|
||||||
SELECT AccountTransaction.*
|
SELECT AccountTransaction.*
|
||||||
FROM AccountTransaction WHERE id = ?;
|
FROM AccountTransaction WHERE id = ?;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS Holding (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
|
|
||||||
userId INTEGER NOT NULL,
|
|
||||||
bankAccountId INTEGER NOT NULL,
|
|
||||||
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
isin TEXT,
|
|
||||||
wkn TEXT,
|
|
||||||
|
|
||||||
quantity INTEGER ,
|
|
||||||
currency TEXT,
|
|
||||||
|
|
||||||
totalBalance TEXT,
|
|
||||||
marketValue TEXT,
|
|
||||||
|
|
||||||
performancePercentage REAL,
|
|
||||||
|
|
||||||
totalCostPrice TEXT,
|
|
||||||
averageCostPrice TEXT,
|
|
||||||
|
|
||||||
pricingTime TEXT,
|
|
||||||
buyingDate TEXT
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
insertHolding:
|
|
||||||
INSERT INTO Holding(
|
|
||||||
userId, bankAccountId,
|
|
||||||
|
|
||||||
name, isin, wkn,
|
|
||||||
|
|
||||||
quantity, currency,
|
|
||||||
|
|
||||||
totalBalance, marketValue,
|
|
||||||
performancePercentage,
|
|
||||||
|
|
||||||
totalCostPrice, averageCostPrice,
|
|
||||||
|
|
||||||
pricingTime, buyingDate
|
|
||||||
)
|
|
||||||
VALUES(
|
|
||||||
?, ?,
|
|
||||||
|
|
||||||
?, ?, ?,
|
|
||||||
|
|
||||||
?, ?,
|
|
||||||
|
|
||||||
?, ?,
|
|
||||||
?,
|
|
||||||
|
|
||||||
?,?,
|
|
||||||
|
|
||||||
?, ?
|
|
||||||
);
|
|
||||||
|
|
||||||
updateHolding:
|
|
||||||
UPDATE Holding
|
|
||||||
SET
|
|
||||||
name = ?, isin = ?, wkn = ?,
|
|
||||||
|
|
||||||
quantity = ?, currency = ?,
|
|
||||||
|
|
||||||
totalBalance = ?, marketValue = ?,
|
|
||||||
performancePercentage = ?,
|
|
||||||
|
|
||||||
totalCostPrice = ?, averageCostPrice = ?,
|
|
||||||
|
|
||||||
pricingTime = ?, buyingDate = ?
|
|
||||||
|
|
||||||
WHERE id = ?;
|
|
||||||
|
|
||||||
|
|
||||||
selectAllHoldings:
|
|
||||||
SELECT Holding.*
|
|
||||||
FROM Holding;
|
|
||||||
|
|
||||||
deleteHolding:
|
|
||||||
DELETE FROM Holding WHERE id = ?;
|
|
|
@ -99,7 +99,7 @@ CREATE TABLE IF NOT EXISTS BankAccount (
|
||||||
balance TEXT NOT NULL,
|
balance TEXT NOT NULL,
|
||||||
|
|
||||||
serverTransactionsRetentionDays INTEGER,
|
serverTransactionsRetentionDays INTEGER,
|
||||||
lastAccountUpdateTime TEXT,
|
lastTransactionsRetrievalTime TEXT,
|
||||||
retrievedTransactionsFrom TEXT,
|
retrievedTransactionsFrom TEXT,
|
||||||
|
|
||||||
userSetDisplayName TEXT,
|
userSetDisplayName TEXT,
|
||||||
|
@ -121,7 +121,7 @@ INSERT INTO BankAccount(
|
||||||
|
|
||||||
isAccountTypeSupportedByApplication, features,
|
isAccountTypeSupportedByApplication, features,
|
||||||
|
|
||||||
serverTransactionsRetentionDays, lastAccountUpdateTime, retrievedTransactionsFrom,
|
serverTransactionsRetentionDays, lastTransactionsRetrievalTime, retrievedTransactionsFrom,
|
||||||
|
|
||||||
userSetDisplayName, displayIndex,
|
userSetDisplayName, displayIndex,
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import app.cash.sqldelight.async.coroutines.synchronous
|
import app.cash.sqldelight.async.coroutines.synchronous
|
||||||
import app.cash.sqldelight.db.SqlDriver
|
|
||||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||||
import bankmeister.composeapp.generated.resources.AppIcon_svg
|
import bankmeister.composeapp.generated.resources.AppIcon_svg
|
||||||
import bankmeister.composeapp.generated.resources.Res
|
import bankmeister.composeapp.generated.resources.Res
|
||||||
|
@ -27,27 +26,22 @@ fun main() = application {
|
||||||
icon = painterResource(Res.drawable.AppIcon_svg),
|
icon = painterResource(Res.drawable.AppIcon_svg),
|
||||||
state = WindowState(position = WindowPosition(Alignment.Center), size = DpSize(1000.dp, 800.dp)),
|
state = WindowState(position = WindowPosition(Alignment.Center), size = DpSize(1000.dp, 800.dp)),
|
||||||
) {
|
) {
|
||||||
DI.setRepository(createSqlDriverDriver())
|
File("data/db").mkdirs()
|
||||||
|
DI.setRepository(JdbcSqliteDriver("jdbc:sqlite:data/db/Bankmeister.db").apply {
|
||||||
|
val schema = BankmeisterDb.Schema
|
||||||
|
schema.synchronous().also {
|
||||||
|
if (File("data/db/Bankmeister.db").exists() == false) {
|
||||||
|
it.create(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.migrate(this, schema.version, 4)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
App()
|
App()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSqlDriverDriver(): SqlDriver {
|
|
||||||
File("data/db").mkdirs()
|
|
||||||
|
|
||||||
return JdbcSqliteDriver("jdbc:sqlite:data/db/Bankmeister.db").also { driver ->
|
|
||||||
BankmeisterDb.Schema.synchronous().also { schema ->
|
|
||||||
if (File("data/db/Bankmeister.db").exists() == false) {
|
|
||||||
schema.create(driver)
|
|
||||||
}
|
|
||||||
|
|
||||||
schema.migrate(driver, schema.version, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun AppPreview() {
|
fun AppPreview() {
|
||||||
|
|
|
@ -2,37 +2,11 @@ package net.codinux.banking.ui.service
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||||
import net.codinux.log.Log
|
|
||||||
import org.jetbrains.skia.Image
|
import org.jetbrains.skia.Image
|
||||||
import java.io.File
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
|
|
||||||
private val cacheDir = File("data/imageCache").also { it.mkdirs() }
|
|
||||||
|
|
||||||
private val messageDigest = MessageDigest.getInstance("SHA-256")
|
|
||||||
|
|
||||||
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
actual fun createImageBitmap(imageBytes: ByteArray): ImageBitmap =
|
||||||
Image.makeFromEncoded(imageBytes).toComposeImageBitmap()
|
Image.makeFromEncoded(imageBytes).toComposeImageBitmap()
|
||||||
|
|
||||||
actual suspend fun fetchBytesFromUrl(url: String): ByteArray {
|
actual suspend fun fetchBytesFromUrl(url: String): ByteArray =
|
||||||
var imageFile: File? = null
|
URL(url).openStream().buffered().use { it.readAllBytes() }
|
||||||
try {
|
|
||||||
val urlHash = messageDigest.digest(url.toByteArray())
|
|
||||||
.joinToString("") { "%02x".format(it) }
|
|
||||||
imageFile = File(cacheDir, urlHash)
|
|
||||||
|
|
||||||
if (imageFile.exists()) {
|
|
||||||
return imageFile.readBytes()
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.error(e) { "Could not create SHA-256 or read image bytes from file for url '$url'" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val imageBytes = URL(url).openStream().buffered().use { it.readAllBytes() }
|
|
||||||
|
|
||||||
imageFile?.writeBytes(imageBytes)
|
|
||||||
|
|
||||||
return imageBytes
|
|
||||||
}
|
|
|
@ -63,7 +63,7 @@ class SqliteBankingRepositoryTest {
|
||||||
|
|
||||||
assertEquals(bankAccounts.first().balance, persistedBankAccount.balance)
|
assertEquals(bankAccounts.first().balance, persistedBankAccount.balance)
|
||||||
assertEquals(bankAccounts.first().retrievedTransactionsFrom, persistedBankAccount.retrievedTransactionsFrom)
|
assertEquals(bankAccounts.first().retrievedTransactionsFrom, persistedBankAccount.retrievedTransactionsFrom)
|
||||||
assertEquals(bankAccounts.first().lastAccountUpdateTime, persistedBankAccount.lastAccountUpdateTime)
|
assertEquals(bankAccounts.first().lastTransactionsRetrievalTime, persistedBankAccount.lastTransactionsRetrievalTime)
|
||||||
|
|
||||||
assertEquals(bankAccounts.first().features, persistedBankAccount.features)
|
assertEquals(bankAccounts.first().features, persistedBankAccount.features)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue