diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 8639d15..bba510f 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -52,21 +52,28 @@ kotlin { sourceSets { val desktopMain by getting - - androidMain.dependencies { - implementation(compose.preview) - implementation(libs.androidx.activity.compose) - } + commonMain.dependencies { + implementation(libs.kcsv) + implementation(libs.klf) + + // UI implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.runtime.compose) } + + androidMain.dependencies { + implementation(compose.preview) + implementation(libs.androidx.activity.compose) + } + desktopMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutines.swing) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt index e8d6b2a..e9579c3 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/App.kt @@ -1,37 +1,43 @@ package net.codinux.banking.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Button import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import org.jetbrains.compose.resources.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch +import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.ui.composables.TransactionsList +import net.codinux.banking.ui.service.BankingService import org.jetbrains.compose.ui.tooling.preview.Preview -import bankmeister.composeapp.generated.resources.Res -import bankmeister.composeapp.generated.resources.compose_multiplatform +private val typography = Typography( + body1 = TextStyle(fontSize = 14.sp) +) + +private val bankService = BankingService() @Composable @Preview fun App() { - MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Compose: $greeting") - } - } + val coroutineScope = rememberCoroutineScope() + val (transactions, setTransaction) = remember { mutableStateOf>(emptyList()) } + + coroutineScope.launch { + setTransaction(bankService.getTransactions()) + } + + MaterialTheme(typography = typography) { + Column(Modifier.fillMaxWidth().fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally) { + TransactionsList(transactions) } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt new file mode 100644 index 0000000..90310dc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionListItem.kt @@ -0,0 +1,60 @@ +package net.codinux.banking.ui.composables + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.ui.service.FormatUtil + +private val formatUtil = FormatUtil() + +@Composable +fun TransactionListItem(transaction: AccountTransaction, backgroundColor: Color) { + Row( + modifier = Modifier.fillMaxWidth() + .background(color = backgroundColor) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = transaction.otherPartyName ?: "", + Modifier.fillMaxWidth(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = transaction.reference, + Modifier.fillMaxWidth(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + Column(Modifier.width(90.dp), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Center) { + Text( + text = formatUtil.formatAmount(transaction.amount, transaction.currency), + color = formatUtil.getColorForAmount(transaction.amount), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = formatUtil.formatDate(transaction.valueDate) + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt new file mode 100644 index 0000000..07a91a6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/TransactionsList.kt @@ -0,0 +1,37 @@ +package net.codinux.banking.ui.composables + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +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.client.model.AccountTransaction +import net.codinux.banking.ui.service.Colors +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun TransactionsList(transactions: List) { + LazyColumn( + modifier = Modifier.padding(4.dp) + ) { + itemsIndexed(transactions) { index, transaction -> + TransactionListItem(transaction, if (index % 2 == 0) Colors.Zinc100_50 else Color.White) + + if (index < transactions.size) { + Divider(color = Colors.Zinc200, thickness = 1.dp) + } + } + } +} + +@Preview +@Composable +fun TransactionsListPreview() { + MaterialTheme { + TransactionsList(emptyList()) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt new file mode 100644 index 0000000..4b53901 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt @@ -0,0 +1,50 @@ +package net.codinux.banking.ui.service + +import bankmeister.composeapp.generated.resources.Res +import kotlinx.datetime.LocalDate +import net.codinux.banking.client.model.AccountTransaction +import net.codinux.banking.client.model.Amount +import net.codinux.csv.reader.CsvReader +import net.codinux.log.logger +import org.jetbrains.compose.resources.ExperimentalResourceApi + +@OptIn(ExperimentalResourceApi::class) +class BankingService { + + private var cachedTransactions: List? = null + + private val log by logger() + + + suspend fun getTransactions(): List { + cachedTransactions?.let { + return it + } + + val transactions = readTransactionsFromCsv() + cachedTransactions = readTransactionsFromCsv() + + return transactions + } + + private suspend fun readTransactionsFromCsv(): List { + val csv = Res.readBytes("files/transactions.csv").decodeToString() + val csvReader = CsvReader(hasHeaderRow = true, reuseRowInstance = true, skipEmptyRows = true).read(csv) + + return csvReader.mapNotNull { row -> + try { + AccountTransaction( + Amount(row.getString("Amount")), row.getString("Currency"), row.getString("Reference"), + LocalDate.parse(row.getString("BookingDate")), LocalDate.parse(row.getString("ValueDate")), + row.getStringOrNull("OtherPartyName"), row.getStringOrNull("OtherPartyBankCode"), row.getStringOrNull("OtherPartyAccountId"), + row.getString("BookingText") + ) + } catch (e: Throwable) { + log.error(e) { "Could not map row: ${row.fields}" } + null + } + } + .sortedByDescending { it.valueDate } + } + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/Colors.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/Colors.kt new file mode 100644 index 0000000..a89c085 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/Colors.kt @@ -0,0 +1,12 @@ +package net.codinux.banking.ui.service + +import androidx.compose.ui.graphics.Color + +object Colors { + + val Zinc100 = Color(244, 244, 245) + val Zinc100_50 = Zinc100.copy(alpha = 0.5f) + + val Zinc200 = Color(228, 228, 231) + +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/FormatUtil.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/FormatUtil.kt new file mode 100644 index 0000000..4647079 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/FormatUtil.kt @@ -0,0 +1,30 @@ +package net.codinux.banking.ui.service + +import androidx.compose.ui.graphics.Color +import kotlinx.datetime.LocalDate +import net.codinux.banking.client.model.Amount +import net.codinux.banking.fints.extensions.toStringWithMinDigits + +class FormatUtil { + + fun formatDate(date: LocalDate): String = // TODO: find a better way + "${date.dayOfMonth.toStringWithMinDigits(2)}.${date.monthNumber.toStringWithMinDigits(2)}.${date.year.toString().substring(2)}" + + fun formatAmount(amount: Amount, currency: String): String { // TODO: find a better way + val parts = amount.amount.split('.') + val decimalPart = if (parts.size == 2) parts[1] else "00" + + return "${parts[0]},${decimalPart.padEnd(2, '0')} ${formatCurrency(currency)}" + } + + fun formatCurrency(currency: String): String = when (currency) { + "EUR" -> "€" + else -> currency + } + + fun getColorForAmount(amount: Amount): Color = when { + amount.amount.startsWith("-") -> Color.Red + else -> Color.Green + } + +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba9acdc..08e469b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,9 @@ kotlin = "2.0.10" kotlinx-coroutines = "1.8.1" +kcsv = "2.1.1" +klf = "1.5.1" + agp = "8.2.2" android-compileSdk = "34" android-minSdk = "24" @@ -19,6 +22,9 @@ compose-plugin = "1.6.11" junit = "4.13.2" [libraries] +kcsv = { group = "net.codinux.csv", name = "kcsv", version.ref = "kcsv" } +klf = { group = "net.codinux.log", name = "kmp-log", version.ref = "klf" } + androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }