mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 11:07:02 +00:00
fix(home, history): see all; sorting; empty state; per-tab scroll; top bar (#1912)
This commit is contained in:
parent
9d55b23faf
commit
71a3b82529
@ -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)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>>> {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user