Implemented basic export data as CSV functionality (only CSV and only a few columns get exported)

This commit is contained in:
dankito 2024-08-31 17:56:31 +02:00
parent e4c5e5ccfc
commit 84a147d4c9
10 changed files with 208 additions and 17 deletions

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -97,6 +98,18 @@ fun SideMenu(appContent: @Composable () -> Unit) {
} }
} }
} }
Divider(color = Colors.DrawerDivider)
Spacer(Modifier.height(12.dp))
NavigationMenuItem(itemModifier, "Daten exportieren", textColor, horizontalPadding = ItemHorizontalPadding, icon = { Icon(Icons.AutoMirrored.Filled.Send, "Konto hinzufügen", Modifier.size(iconSize)) }) {
coroutineScope.launch {
drawerState.close()
}
uiState.showExportScreen.value = true
}
} }
} }
) )

View File

@ -5,11 +5,14 @@ import net.codinux.banking.ui.dialogs.AddAccountDialog
import net.codinux.banking.ui.dialogs.ApplicationErrorDialog import net.codinux.banking.ui.dialogs.ApplicationErrorDialog
import net.codinux.banking.ui.dialogs.BankingClientErrorDialog import net.codinux.banking.ui.dialogs.BankingClientErrorDialog
import net.codinux.banking.ui.dialogs.EnterTanDialog import net.codinux.banking.ui.dialogs.EnterTanDialog
import net.codinux.banking.ui.screens.ExportScreen
import net.codinux.banking.ui.state.UiState import net.codinux.banking.ui.state.UiState
@Composable @Composable
fun StateHandler(uiState: UiState) { fun StateHandler(uiState: UiState) {
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState() val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
val showExportScreen by uiState.showExportScreen.collectAsState()
val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState() val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState()
val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState() val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState()
val applicationError by uiState.applicationErrorOccurred.collectAsState() val applicationError by uiState.applicationErrorOccurred.collectAsState()
@ -19,6 +22,11 @@ fun StateHandler(uiState: UiState) {
AddAccountDialog { uiState.showAddAccountDialog.value = false } AddAccountDialog { uiState.showAddAccountDialog.value = false }
} }
if (showExportScreen) {
ExportScreen { uiState.showExportScreen.value = false }
}
tanChallengeReceived?.let { tanChallengeReceived -> tanChallengeReceived?.let { tanChallengeReceived ->
EnterTanDialog(tanChallengeReceived) { EnterTanDialog(tanChallengeReceived) {
uiState.tanChallengeReceived.value = null uiState.tanChallengeReceived.value = null

View File

@ -0,0 +1,19 @@
package net.codinux.banking.ui.composables.text
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import net.codinux.banking.ui.config.Style
@Composable
fun HeaderText(title: String, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Start) {
Text(
title,
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight,
modifier = modifier,
textAlign = textAlign
)
}

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.Style import net.codinux.banking.ui.config.Style
import net.codinux.banking.ui.forms.* import net.codinux.banking.ui.forms.*
@ -34,13 +35,7 @@ fun BaseDialog(
Column(Modifier.background(Color.White).padding(8.dp)) { Column(Modifier.background(Color.White).padding(8.dp)) {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Text( HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f))
title,
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight,
modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)
)
TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) { TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp)) Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))

View File

@ -8,6 +8,7 @@ import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.Style import net.codinux.banking.ui.config.Style
@ -22,14 +23,7 @@ fun ErrorDialog(
AlertDialog( AlertDialog(
text = { Text(text) }, text = { Text(text) },
title = { title?.let { title = { title?.let {
Text( HeaderText(title, Modifier.fillMaxWidth(), TextAlign.Center)
title,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
color = Style.HeaderTextColor,
fontSize = Style.HeaderFontSize,
fontWeight = Style.HeaderFontWeight
)
} }, } },
onDismissRequest = { onDismiss?.invoke() }, onDismissRequest = { onDismiss?.invoke() },
confirmButton = { confirmButton = {

View File

@ -0,0 +1,58 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.codinux.banking.dataaccess.entities.AccountTransactionEntity
import net.codinux.banking.ui.IOorDefault
import net.codinux.banking.ui.config.Colors
import net.codinux.banking.ui.config.DI
import net.codinux.banking.ui.service.BankDataImporterAndExporter
@Composable
fun ExportScreen(onClosed: () -> Unit) {
var transactions: Collection<AccountTransactionEntity>
var exportedDataText by remember { mutableStateOf("") }
val importerExporter = BankDataImporterAndExporter()
val clipboardManager = LocalClipboardManager.current
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch(Dispatchers.IOorDefault) {
transactions = DI.bankingService.getAllAccountTransactions() // a only very bit problematic: if in the meantime new transactions are retrieved, then this transactions property doesn't contain the newly retrieved transactions
exportedDataText = importerExporter.exportTransactionsAsCsv(transactions, ',')
}
FullscreenViewBase("Umsätze exportieren", onClosed = onClosed) {
Column {
Text("Es gibt leider noch keinen \"Datei auswählen Dialog\", ist sehr schwierig plattformübergreifend umzusetzen, deshalb bitte folgenden Text kopieren und in eine Textdatei einfügen:")
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton({ clipboardManager.setText(AnnotatedString(exportedDataText))}) {
Text("Kopieren", color = Colors.CodinuxSecondaryColor)
}
}
Column(Modifier.verticalScroll(ScrollState(0), enabled = true).horizontalScroll(ScrollState(0), enabled = true)) {
SelectionContainer {
Text(exportedDataText, fontFamily = FontFamily.Monospace)
}
}
}
}
}

View File

@ -0,0 +1,54 @@
package net.codinux.banking.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import net.codinux.banking.ui.composables.text.HeaderText
import net.codinux.banking.ui.config.Colors
@Composable
fun FullscreenViewBase(
title: String,
confirmButtonTitle: String = "OK",
confirmButtonEnabled: Boolean = true,
onClosed: () -> Unit,
content: @Composable () -> Unit
) {
Column(Modifier.fillMaxSize().background(Color.White).padding(8.dp)) {
Row(Modifier.fillMaxWidth()) {
HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f))
TextButton(onClosed, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) {
Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp))
}
}
Column(Modifier.fillMaxWidth().weight(1f)) {
content()
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
// TextButton(onClick = onClosed, Modifier.width(Style.DialogButtonWidth)) {
// Text("Abbrechen", color = Colors.CodinuxSecondaryColor)
// }
//
// Spacer(Modifier.width(8.dp))
TextButton(
modifier = Modifier.fillMaxWidth(),
enabled = confirmButtonEnabled,
onClick = { /* onConfirm?.invoke() ?: */ onClosed() }
) {
Text(confirmButtonTitle, color = Colors.CodinuxSecondaryColor, textAlign = TextAlign.Center)
}
}
}
}

View File

@ -0,0 +1,40 @@
package net.codinux.banking.ui.service
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.csv.writer.CsvWriter
// TODO: extract to a common utility project
class BankDataImporterAndExporter {
fun exportTransactionsAsCsv(transactions: Collection<AccountTransaction>, decimalSeparator: Char = '.'): String {
val builder = StringBuilder()
val writer = CsvWriter.builder(if (decimalSeparator == ',') ';' else ',').writer(builder)
writer.writeRow(
"Betrug", "Währung", "Wertstellungstag", "Buchungstag",
"Verwendungszweck", "Der Andere Name", "Der Andere Bank", "Der Andere Konto"
)
transactions.forEach { transaction ->
writer.writeRow(
// TODO: add bank and bank account
formatAmount(transaction.amount, decimalSeparator), transaction.currency,
transaction.valueDate.toString(), transaction.bookingDate.toString(),
transaction.sepaReference ?: transaction.reference,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId
// TODO: export all columns / transaction data
)
}
return builder.toString()
}
private fun formatAmount(amount: Amount, decimalSeparator: Char): String =
if (decimalSeparator == '.') {
amount.amount
} else {
amount.amount.replace('.', decimalSeparator)
}
}

View File

@ -42,14 +42,22 @@ class BankingService(
suspend fun init() { suspend fun init() {
try { try {
uiState.userAccounts.value = bankingRepository.getAllUserAccounts() uiState.userAccounts.value = getAllUserAccounts()
uiState.transactions.value = bankingRepository.getAllAccountTransactionsAsViewModel() uiState.transactions.value = getAllAccountTransactionsAsViewModel()
} 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" }
} }
} }
fun getAllUserAccounts() = bankingRepository.getAllUserAccounts()
fun getAllAccountTransactions() = bankingRepository.getAllAccountTransactions()
fun getAllAccountTransactionsAsViewModel() = bankingRepository.getAllAccountTransactionsAsViewModel()
suspend fun findBanks(query: String): List<BankInfo> = suspend fun findBanks(query: String): List<BankInfo> =
bankFinder.findBankByNameBankCodeOrCity(query, 25) bankFinder.findBankByNameBankCodeOrCity(query, 25)

View File

@ -28,6 +28,8 @@ class UiState : ViewModel() {
val showAddAccountDialog = MutableStateFlow(false) val showAddAccountDialog = MutableStateFlow(false)
val showExportScreen = MutableStateFlow(false)
val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null) val tanChallengeReceived = MutableStateFlow<TanChallengeReceived?>(null)
val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null) val bankingClientErrorOccurred = MutableStateFlow<BankingClientError?>(null)