diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt index 27600d9..b09c100 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/SideMenu.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.Composable 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 + } } } ) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt index a735f71..e9b7fdf 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt @@ -5,11 +5,14 @@ import net.codinux.banking.ui.dialogs.AddAccountDialog import net.codinux.banking.ui.dialogs.ApplicationErrorDialog import net.codinux.banking.ui.dialogs.BankingClientErrorDialog import net.codinux.banking.ui.dialogs.EnterTanDialog +import net.codinux.banking.ui.screens.ExportScreen import net.codinux.banking.ui.state.UiState @Composable fun StateHandler(uiState: UiState) { val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState() + val showExportScreen by uiState.showExportScreen.collectAsState() + val tanChallengeReceived by uiState.tanChallengeReceived.collectAsState() val bankingClientError by uiState.bankingClientErrorOccurred.collectAsState() val applicationError by uiState.applicationErrorOccurred.collectAsState() @@ -19,6 +22,11 @@ fun StateHandler(uiState: UiState) { AddAccountDialog { uiState.showAddAccountDialog.value = false } } + if (showExportScreen) { + ExportScreen { uiState.showExportScreen.value = false } + } + + tanChallengeReceived?.let { tanChallengeReceived -> EnterTanDialog(tanChallengeReceived) { uiState.tanChallengeReceived.value = null diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/text/HeaderText.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/text/HeaderText.kt new file mode 100644 index 0000000..c55308a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/text/HeaderText.kt @@ -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 + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt index d15b0bc..e071aee 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/BaseDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog 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.Style import net.codinux.banking.ui.forms.* @@ -34,13 +35,7 @@ fun BaseDialog( Column(Modifier.background(Color.White).padding(8.dp)) { Row(Modifier.fillMaxWidth()) { - Text( - title, - color = Style.HeaderTextColor, - fontSize = Style.HeaderFontSize, - fontWeight = Style.HeaderFontWeight, - modifier = Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f) - ) + HeaderText(title, Modifier.padding(top = 8.dp, bottom = 16.dp).weight(1f)) TextButton(onDismiss, colors = ButtonDefaults.buttonColors(contentColor = Colors.Zinc700, backgroundColor = Color.Transparent)) { Icon(Icons.Filled.Close, contentDescription = "Close dialog", Modifier.size(32.dp)) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ErrorDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ErrorDialog.kt index 01058d9..3cbf857 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ErrorDialog.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ErrorDialog.kt @@ -8,6 +8,7 @@ import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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.Style @@ -22,14 +23,7 @@ fun ErrorDialog( AlertDialog( text = { Text(text) }, title = { title?.let { - Text( - title, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - color = Style.HeaderTextColor, - fontSize = Style.HeaderFontSize, - fontWeight = Style.HeaderFontWeight - ) + HeaderText(title, Modifier.fillMaxWidth(), TextAlign.Center) } }, onDismissRequest = { onDismiss?.invoke() }, confirmButton = { diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/ExportScreen.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/ExportScreen.kt new file mode 100644 index 0000000..18f7d4d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/ExportScreen.kt @@ -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 + + 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) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/FullscreenViewBase.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/FullscreenViewBase.kt new file mode 100644 index 0000000..c9e2fbb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/FullscreenViewBase.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt new file mode 100644 index 0000000..898c488 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankDataImporterAndExporter.kt @@ -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, 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) + } + +} \ 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 index b3db12c..4f7f60e 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/BankingService.kt @@ -42,14 +42,22 @@ class BankingService( suspend fun init() { try { - uiState.userAccounts.value = bankingRepository.getAllUserAccounts() + uiState.userAccounts.value = getAllUserAccounts() - uiState.transactions.value = bankingRepository.getAllAccountTransactionsAsViewModel() + uiState.transactions.value = getAllAccountTransactionsAsViewModel() } catch (e: Throwable) { 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 = bankFinder.findBankByNameBankCodeOrCity(query, 25) diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt index 432f3e9..a02fb99 100644 --- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt +++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt @@ -28,6 +28,8 @@ class UiState : ViewModel() { val showAddAccountDialog = MutableStateFlow(false) + val showExportScreen = MutableStateFlow(false) + val tanChallengeReceived = MutableStateFlow(null) val bankingClientErrorOccurred = MutableStateFlow(null)