diff --git a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt index d6a94277..03a443a9 100644 --- a/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt +++ b/cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt @@ -31,6 +31,7 @@ import org.mifospay.feature.finance.navigation.FINANCE_ROUTE import org.mifospay.feature.finance.navigation.financeScreen import org.mifospay.feature.history.HistoryScreen import org.mifospay.feature.history.navigation.historyNavigation +import org.mifospay.feature.history.navigation.navigateToHistory import org.mifospay.feature.history.navigation.navigateToSpecificTransaction import org.mifospay.feature.history.navigation.navigateToTransactionDetail import org.mifospay.feature.history.navigation.specificTransactionsScreen @@ -108,6 +109,7 @@ internal fun MifosNavHost( TabContent(PaymentsScreenContents.HISTORY.name) { HistoryScreen( viewTransferDetail = navController::navigateToTransactionDetail, + showTopBar = false, ) }, TabContent(PaymentsScreenContents.SI.name) { @@ -163,6 +165,7 @@ internal fun MifosNavHost( onPay = navController::navigateToSendMoneyScreen, navigateToTransactionDetail = navController::navigateToSpecificTransaction, navigateToAccountDetail = navController::navigateToSavingAccountDetails, + navigateToHistory = navController::navigateToHistory, ) settingsScreen( @@ -193,6 +196,7 @@ internal fun MifosNavHost( historyNavigation( viewTransactionDetail = navController::navigateToTransactionDetail, + onBackClick = navController::navigateUp, ) paymentsScreen(tabContents = paymentsTabContents) diff --git a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt index be4efc7c..7c563e43 100644 --- a/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt +++ b/core/common/src/commonMain/kotlin/org/mifospay/core/common/DateHelper.kt @@ -15,12 +15,11 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.Month import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.format -import kotlinx.datetime.format.FormatStringsInDatetimeFormats import kotlinx.datetime.format.byUnicodePattern import kotlinx.datetime.toLocalDateTime -@OptIn(FormatStringsInDatetimeFormats::class) object DateHelper { /* * This is the full month format for the date picker. @@ -370,4 +369,41 @@ object DateHelper { * "dd-MM-yyyy" is the format of the date picker. */ val formattedShortDate = currentDate.format(shortMonthFormat) + + /** + * Parses a date string in "dd MMM yyyy" format and converts it to milliseconds. + * This is useful for sorting transactions by date. + * + * @param dateString The date string in format like "14 Apr 2016" + * @return The date in milliseconds since epoch, or null if parsing fails + */ + fun parseDateToMillis(dateString: String): Long? { + val parts = dateString.split(" ") + if (parts.size != 3) return null + + val (dayStr, monthName, yearStr) = parts + + val month = when (monthName) { + "Jan" -> 1 + "Feb" -> 2 + "Mar" -> 3 + "Apr" -> 4 + "May" -> 5 + "Jun" -> 6 + "Jul" -> 7 + "Aug" -> 8 + "Sep" -> 9 + "Oct" -> 10 + "Nov" -> 11 + "Dec" -> 12 + else -> throw IllegalArgumentException("Invalid month: $monthName") + } + + return runCatching { + val day = dayStr.toInt() + val year = yearStr.toInt() + val localDate = LocalDate(year, Month(month), day) + localDate.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() + }.getOrNull() + } } diff --git a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/SelfServiceRepositoryImpl.kt b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/SelfServiceRepositoryImpl.kt index 9f7cb458..2a65898c 100644 --- a/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/SelfServiceRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImpl/SelfServiceRepositoryImpl.kt @@ -15,8 +15,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -24,6 +26,7 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.zip import kotlinx.coroutines.withContext import org.mifospay.core.common.DataState +import org.mifospay.core.common.DateHelper import org.mifospay.core.common.asDataStateFlow import org.mifospay.core.common.combineResultsWith import org.mifospay.core.data.mapper.toAccount @@ -156,7 +159,9 @@ class SelfServiceRepositoryImpl( return accountId.asFlow().flatMapMerge { clientId -> getSelfAccountTransactions(clientId) }.scan(emptyList()) { acc, transactions -> - acc + transactions.sortedByDescending { it.date }.let { sortedList -> + (acc + transactions).sortedByDescending { transaction -> + DateHelper.parseDateToMillis(transaction.date) ?: Long.MIN_VALUE + }.let { sortedList -> limit?.let { sortedList.take(it) } ?: sortedList } } @@ -167,17 +172,18 @@ class SelfServiceRepositoryImpl( ): Flow>> { return apiManager.clientsApi .getAccounts(clientId, Constants.SAVINGS) - .onStart { DataState.Loading } - .catch { DataState.Error(it, null) } .map { it.toAccount() } .map { list -> list.filter { it.status.active } } .map { list -> list.map { it.id } } - .flatMapLatest { - getTransactions(accountId = it, null) - }.map { - DataState.Success(it) + .flatMapLatest { accountIds -> + if (accountIds.isEmpty()) { + flowOf(emptyList()) + } else { + getTransactions(accountId = accountIds, null) + .filter { transactions -> transactions.isNotEmpty() } + } } - .flowOn(dispatcher) + .asDataStateFlow() } override fun getBeneficiaryList(): Flow>> { diff --git a/feature/history/src/commonMain/composeResources/values/strings.xml b/feature/history/src/commonMain/composeResources/values/strings.xml index 9af67f4a..f103c667 100644 --- a/feature/history/src/commonMain/composeResources/values/strings.xml +++ b/feature/history/src/commonMain/composeResources/values/strings.xml @@ -9,6 +9,7 @@ See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md --> + History Transaction ID : Transaction Date : Other diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryScreen.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryScreen.kt index ce883fec..fc99a127 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryScreen.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/HistoryScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -25,9 +26,12 @@ import mobile_wallet.feature.history.generated.resources.feature_history_empty import mobile_wallet.feature.history.generated.resources.feature_history_error import mobile_wallet.feature.history.generated.resources.feature_history_error_oops import mobile_wallet.feature.history.generated.resources.feature_history_loading +import mobile_wallet.feature.history.generated.resources.feature_history_title import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTopBar import org.mifospay.core.model.savingsaccount.TransactionType import org.mifospay.core.ui.EmptyContentScreen import org.mifospay.core.ui.utils.EventsEffect @@ -40,6 +44,8 @@ fun HistoryScreen( viewTransferDetail: (Long) -> Unit, modifier: Modifier = Modifier, viewModel: HistoryViewModel = koinViewModel(), + showTopBar: Boolean = true, + onBackClick: (() -> Unit)? = null, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -57,6 +63,8 @@ fun HistoryScreen( onAction = remember(viewModel) { { action -> viewModel.trySendAction(action) } }, + showTopBar = showTopBar, + onBackClick = onBackClick, ) } @@ -65,24 +73,41 @@ internal fun HistoryScreenContent( state: HistoryState, modifier: Modifier = Modifier, onAction: (HistoryAction) -> Unit, + showTopBar: Boolean = true, + onBackClick: (() -> Unit)? = null, ) { - Box( + MifosScaffold( modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { + topBar = { + if (showTopBar && onBackClick != null) { + MifosTopBar( + topBarTitle = stringResource(Res.string.feature_history_title), + backPress = onBackClick, + ) + } + }, + ) { paddingValues -> when (state.viewState) { is HistoryState.ViewState.Loading -> { - MifosLoadingWheel( - modifier = Modifier.align(Alignment.Center), - contentDesc = stringResource(Res.string.feature_history_loading), - ) + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center, + ) { + MifosLoadingWheel( + contentDesc = stringResource(Res.string.feature_history_loading), + ) + } } is HistoryState.ViewState.Error -> { EmptyContentScreen( title = stringResource(Res.string.feature_history_error_oops), subTitle = stringResource(Res.string.feature_history_error), - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), iconTint = KptTheme.colorScheme.error, ) } @@ -91,7 +116,9 @@ internal fun HistoryScreenContent( EmptyContentScreen( title = stringResource(Res.string.feature_history_error_oops), subTitle = stringResource(Res.string.feature_history_empty), - modifier = Modifier.fillMaxSize().align(Alignment.Center), + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), ) } @@ -100,6 +127,7 @@ internal fun HistoryScreenContent( state = state.viewState, selectedTransactionType = state.transactionType, onAction = onAction, + modifier = Modifier.padding(paddingValues), ) } } @@ -113,6 +141,16 @@ private fun HistoryScreenContent( modifier: Modifier = Modifier, onAction: (HistoryAction) -> Unit, ) { + val allTransactionsListState = rememberLazyListState() + val debitTransactionsListState = rememberLazyListState() + val creditTransactionsListState = rememberLazyListState() + + val currentListState = when (selectedTransactionType) { + TransactionType.OTHER -> allTransactionsListState + TransactionType.DEBIT -> debitTransactionsListState + TransactionType.CREDIT -> creditTransactionsListState + } + Column( modifier = modifier .fillMaxSize(), @@ -127,6 +165,7 @@ private fun HistoryScreenContent( TransactionList( transactions = state.list, onAction = onAction, + lazyListState = currentListState, ) } } diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt index a6e79165..e73b2347 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt @@ -19,10 +19,13 @@ const val HISTORY_ROUTE = "history_route" fun NavGraphBuilder.historyNavigation( viewTransactionDetail: (Long) -> Unit, + onBackClick: () -> Unit, ) { composable(HISTORY_ROUTE) { HistoryScreen( viewTransferDetail = viewTransactionDetail, + showTopBar = true, + onBackClick = onBackClick, ) } } diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt index a2b31672..600da571 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/HomeScreen.kt @@ -126,6 +126,7 @@ internal fun HomeScreen( onPay: () -> Unit, navigateToTransactionDetail: (Long, Long) -> Unit, navigateToAccountDetail: (Long) -> Unit, + navigateToHistory: () -> Unit, modifier: Modifier = Modifier, viewModel: HomeViewModel = koinViewModel(), ) { @@ -138,15 +139,15 @@ internal fun HomeScreen( EventsEffect(viewModel) { event -> when (event) { - is HomeEvent.NavigateBack -> onNavigateBack.invoke() + is HomeEvent.NavigateBack -> onNavigateBack() is HomeEvent.NavigateToRequestScreen -> onRequest(event.vpa) - is HomeEvent.NavigateToSendScreen -> onPay.invoke() + is HomeEvent.NavigateToSendScreen -> onPay() is HomeEvent.NavigateToClientDetailScreen -> {} is HomeEvent.NavigateToTransactionDetail -> { navigateToTransactionDetail(event.accountId, event.transactionId) } - is HomeEvent.NavigateToTransactionScreen -> {} + is HomeEvent.NavigateToTransactionScreen -> navigateToHistory() is HomeEvent.ShowToast -> { scope.launch { snackbarState.showSnackbar(getString(event.message)) diff --git a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt index 5ea8e977..1f797325 100644 --- a/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/commonMain/kotlin/org/mifospay/feature/home/navigation/HomeNavigation.kt @@ -25,6 +25,7 @@ fun NavGraphBuilder.homeScreen( onPay: () -> Unit, navigateToTransactionDetail: (Long, Long) -> Unit, navigateToAccountDetail: (Long) -> Unit, + navigateToHistory: () -> Unit, ) { composable(route = HOME_ROUTE) { HomeScreen( @@ -33,6 +34,7 @@ fun NavGraphBuilder.homeScreen( onNavigateBack = onNavigateBack, navigateToTransactionDetail = navigateToTransactionDetail, navigateToAccountDetail = navigateToAccountDetail, + navigateToHistory = navigateToHistory, ) } } diff --git a/feature/send-money/src/commonMain/kotlin/org/mifospay/feature/send/money/navigation/SendNavigation.kt b/feature/send-money/src/commonMain/kotlin/org/mifospay/feature/send/money/navigation/SendNavigation.kt index 04af30a0..99d1fc01 100644 --- a/feature/send-money/src/commonMain/kotlin/org/mifospay/feature/send/money/navigation/SendNavigation.kt +++ b/feature/send-money/src/commonMain/kotlin/org/mifospay/feature/send/money/navigation/SendNavigation.kt @@ -14,6 +14,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.navArgument +import androidx.navigation.navOptions import org.mifospay.core.ui.composableWithSlideTransitions import org.mifospay.feature.send.money.SendMoneyScreen @@ -54,9 +55,11 @@ fun NavController.navigateToSendMoneyScreen( navOptions: NavOptions? = null, ) { val route = "$SEND_MONEY_ROUTE?$SEND_MONEY_ARG=$requestData" - val options = navOptions ?: NavOptions.Builder() - .setPopUpTo(SEND_MONEY_ROUTE, inclusive = true) - .build() + val options = navOptions ?: navOptions { + popUpTo(SEND_MONEY_ROUTE) { + inclusive = true + } + } navigate(route, options) }