fix(home, history): see all; sorting; empty state; per-tab scroll; top bar (#1912)

This commit is contained in:
Biplab Dutta 2025-09-05 01:41:46 +05:30 committed by GitHub
parent 9d55b23faf
commit 71a3b82529
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 120 additions and 25 deletions

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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<DataState<List<Transaction>>> {
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<DataState<List<Beneficiary>>> {

View File

@ -9,6 +9,7 @@
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources>
<string name="feature_history_title">History</string>
<string name="feature_history_transaction_id">Transaction ID : </string>
<string name="feature_history_transaction_date">Transaction Date : </string>
<string name="feature_history_other">Other</string>

View File

@ -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,
)
}
}

View File

@ -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,
)
}
}

View File

@ -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))

View File

@ -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,
)
}
}

View File

@ -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)
}