Skip to content

Commit b72d6a1

Browse files
committed
feat(feature:send-money): implement transaction history screen with search
1 parent 2ad303b commit b72d6a1

File tree

6 files changed

+648
-3
lines changed

6 files changed

+648
-3
lines changed

cmp-shared/src/commonMain/kotlin/org/mifospay/shared/navigation/MifosNavHost.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import org.mifospay.feature.send.money.navigation.navigateToPaymentSuccessScreen
8484
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyOptionsScreen
8585
import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen
8686
import org.mifospay.feature.send.money.navigation.navigateToUpiPinScreen
87+
import org.mifospay.feature.send.money.navigation.navigateToUpiTransactionHistoryScreen
8788
import org.mifospay.feature.send.money.navigation.payeeDetailsScreen
8889
import org.mifospay.feature.send.money.navigation.paymentChatHistoryScreen
8990
import org.mifospay.feature.send.money.navigation.paymentDetailsScreen
@@ -92,6 +93,7 @@ import org.mifospay.feature.send.money.navigation.paymentSuccessScreen
9293
import org.mifospay.feature.send.money.navigation.sendMoneyOptionsScreen
9394
import org.mifospay.feature.send.money.navigation.sendMoneyScreen
9495
import org.mifospay.feature.send.money.navigation.upiPinScreen
96+
import org.mifospay.feature.send.money.navigation.upiTransactionHistoryScreen
9597
import org.mifospay.feature.settings.navigation.settingsScreen
9698
import org.mifospay.feature.standing.instruction.StandingInstructionsScreen
9799
import org.mifospay.feature.standing.instruction.createOrUpdate.addEditSIScreen
@@ -331,6 +333,9 @@ internal fun MifosNavHost(
331333
onPaymentHistoryClick = {
332334
navController.navigateToPaymentChatHistoryScreen()
333335
},
336+
onUpiTransactionHistoryClick = {
337+
navController.navigateToUpiTransactionHistoryScreen()
338+
},
334339
)
335340

336341
sendMoneyScreen(
@@ -350,6 +355,10 @@ internal fun MifosNavHost(
350355
},
351356
)
352357

358+
upiTransactionHistoryScreen(
359+
onBackClick = navController::popBackStack,
360+
)
361+
353362
paymentDetailsScreen(
354363
onBackClick = navController::popBackStack,
355364
onPayAgainClick = {

feature/send-money/src/commonMain/kotlin/org/mifospay/feature/send/money/SendMoneyOptionsScreen.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ fun SendMoneyOptionsScreen(
7070
onQrCodeScanned: (String) -> Unit,
7171
onNavigateToPayeeDetails: (String) -> Unit,
7272
onPaymentHistoryClick: () -> Unit,
73+
onUpiTransactionHistoryClick: () -> Unit,
7374
modifier: Modifier = Modifier,
7475
viewModel: SendMoneyOptionsViewModel = koinViewModel(),
7576
) {
@@ -149,9 +150,7 @@ fun SendMoneyOptionsScreen(
149150
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
150151

151152
TransactionHistorySection(
152-
onSeeAllClick = {
153-
// TODO: Navigate to full transaction history
154-
},
153+
onSeeAllClick = onUpiTransactionHistoryClick,
155154
)
156155

157156
Spacer(modifier = Modifier.height(KptTheme.spacing.md))
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.feature.send.money
11+
12+
import androidx.compose.foundation.background
13+
import androidx.compose.foundation.layout.Arrangement
14+
import androidx.compose.foundation.layout.Box
15+
import androidx.compose.foundation.layout.Column
16+
import androidx.compose.foundation.layout.Row
17+
import androidx.compose.foundation.layout.fillMaxSize
18+
import androidx.compose.foundation.layout.fillMaxWidth
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.foundation.layout.size
21+
import androidx.compose.foundation.lazy.LazyColumn
22+
import androidx.compose.foundation.lazy.items
23+
import androidx.compose.foundation.shape.CircleShape
24+
import androidx.compose.foundation.shape.RoundedCornerShape
25+
import androidx.compose.material3.Card
26+
import androidx.compose.material3.CardDefaults
27+
import androidx.compose.material3.ExperimentalMaterial3Api
28+
import androidx.compose.material3.Icon
29+
import androidx.compose.material3.IconButton
30+
import androidx.compose.material3.OutlinedTextField
31+
import androidx.compose.material3.OutlinedTextFieldDefaults
32+
import androidx.compose.material3.Surface
33+
import androidx.compose.material3.Text
34+
import androidx.compose.runtime.Composable
35+
import androidx.compose.runtime.getValue
36+
import androidx.compose.ui.Alignment
37+
import androidx.compose.ui.Modifier
38+
import androidx.compose.ui.input.key.Key
39+
import androidx.compose.ui.input.key.key
40+
import androidx.compose.ui.input.key.onKeyEvent
41+
import androidx.compose.ui.text.font.FontWeight
42+
import androidx.compose.ui.text.style.TextAlign
43+
import androidx.compose.ui.unit.dp
44+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
45+
import kotlinx.datetime.LocalDate
46+
import org.koin.compose.viewmodel.koinViewModel
47+
import org.mifospay.core.common.CurrencyFormatter
48+
import org.mifospay.core.designsystem.component.MifosScaffold
49+
import org.mifospay.core.designsystem.icon.MifosIcons
50+
import org.mifospay.core.ui.utils.EventsEffect
51+
import template.core.base.designsystem.theme.KptTheme
52+
53+
@Composable
54+
fun UpiTransactionHistoryScreen(
55+
onBackClick: () -> Unit,
56+
modifier: Modifier = Modifier,
57+
viewModel: UpiTransactionHistoryViewModel = koinViewModel(),
58+
) {
59+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
60+
61+
EventsEffect(viewModel) { event ->
62+
when (event) {
63+
UpiTransactionHistoryEvent.NavigateBack -> {
64+
onBackClick.invoke()
65+
}
66+
}
67+
}
68+
69+
MifosScaffold(
70+
modifier = modifier,
71+
containerColor = KptTheme.colorScheme.background,
72+
topBar = {
73+
UpiTransactionHistoryTopBar(
74+
searchQuery = state.searchQuery,
75+
onSearchQueryChange = { query ->
76+
viewModel.trySendAction(UpiTransactionHistoryAction.SearchQueryChanged(query))
77+
},
78+
onSearch = {
79+
viewModel.trySendAction(UpiTransactionHistoryAction.SearchPerformed)
80+
},
81+
onBackClick = {
82+
viewModel.trySendAction(UpiTransactionHistoryAction.BackPressed)
83+
},
84+
onClearSearch = {
85+
viewModel.trySendAction(UpiTransactionHistoryAction.ClearSearch)
86+
},
87+
)
88+
},
89+
) { paddingValues ->
90+
if (state.isLoading) {
91+
Box(
92+
modifier = Modifier
93+
.fillMaxSize()
94+
.padding(paddingValues),
95+
contentAlignment = Alignment.Center,
96+
) {
97+
Text(
98+
text = "Loading transactions...",
99+
style = KptTheme.typography.bodyLarge,
100+
color = KptTheme.colorScheme.onSurface,
101+
)
102+
}
103+
} else {
104+
LazyColumn(
105+
modifier = Modifier
106+
.fillMaxSize()
107+
.padding(paddingValues)
108+
.padding(horizontal = KptTheme.spacing.lg)
109+
.padding(top = KptTheme.spacing.lg),
110+
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
111+
) {
112+
items(state.groupedTransactions) { monthGroup ->
113+
MonthTransactionGroup(
114+
monthGroup = monthGroup,
115+
)
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
@OptIn(ExperimentalMaterial3Api::class)
123+
@Composable
124+
private fun UpiTransactionHistoryTopBar(
125+
searchQuery: String,
126+
onSearchQueryChange: (String) -> Unit,
127+
onSearch: () -> Unit,
128+
onBackClick: () -> Unit,
129+
onClearSearch: () -> Unit,
130+
modifier: Modifier = Modifier,
131+
) {
132+
Surface(
133+
modifier = modifier.fillMaxWidth(),
134+
color = KptTheme.colorScheme.surface,
135+
tonalElevation = 4.dp,
136+
) {
137+
Box(
138+
modifier = Modifier
139+
.fillMaxWidth()
140+
.padding(horizontal = KptTheme.spacing.md, vertical = KptTheme.spacing.sm),
141+
) {
142+
IconButton(
143+
onClick = onBackClick,
144+
modifier = Modifier.align(Alignment.CenterStart),
145+
) {
146+
Icon(
147+
imageVector = MifosIcons.ArrowBack,
148+
contentDescription = "Back",
149+
tint = KptTheme.colorScheme.onSurface,
150+
)
151+
}
152+
153+
OutlinedTextField(
154+
value = searchQuery,
155+
onValueChange = onSearchQueryChange,
156+
modifier = Modifier
157+
.fillMaxWidth(0.7f)
158+
.align(Alignment.Center)
159+
.onKeyEvent { keyEvent ->
160+
if (keyEvent.key == Key.Enter) {
161+
onSearch()
162+
true
163+
} else {
164+
false
165+
}
166+
},
167+
placeholder = {
168+
Text(
169+
text = "Search transactions...",
170+
color = KptTheme.colorScheme.onSurfaceVariant,
171+
)
172+
},
173+
singleLine = true,
174+
shape = RoundedCornerShape(32.dp),
175+
colors = OutlinedTextFieldDefaults.colors(
176+
focusedBorderColor = KptTheme.colorScheme.primary,
177+
unfocusedBorderColor = KptTheme.colorScheme.outline,
178+
),
179+
trailingIcon = {
180+
if (searchQuery.isNotEmpty()) {
181+
IconButton(
182+
onClick = onClearSearch,
183+
) {
184+
Icon(
185+
imageVector = MifosIcons.Close,
186+
contentDescription = "Clear search",
187+
tint = KptTheme.colorScheme.onSurfaceVariant,
188+
)
189+
}
190+
}
191+
},
192+
)
193+
}
194+
}
195+
}
196+
197+
@Composable
198+
private fun MonthTransactionGroup(
199+
monthGroup: MonthTransactionGroup,
200+
modifier: Modifier = Modifier,
201+
) {
202+
Column(
203+
modifier = modifier.fillMaxWidth(),
204+
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
205+
) {
206+
Row(
207+
modifier = Modifier.fillMaxWidth(),
208+
horizontalArrangement = Arrangement.SpaceBetween,
209+
verticalAlignment = Alignment.CenterVertically,
210+
) {
211+
Text(
212+
text = monthGroup.monthYear,
213+
style = KptTheme.typography.titleMedium,
214+
fontWeight = FontWeight.Medium,
215+
color = KptTheme.colorScheme.onSurface,
216+
)
217+
218+
Text(
219+
text = CurrencyFormatter.format(
220+
balance = monthGroup.totalAmount,
221+
currencyCode = "INR",
222+
maximumFractionDigits = 2,
223+
),
224+
style = KptTheme.typography.titleMedium,
225+
fontWeight = FontWeight.Medium,
226+
color = KptTheme.colorScheme.onSurface,
227+
)
228+
}
229+
230+
Column(
231+
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.sm),
232+
) {
233+
monthGroup.transactions.forEach { transaction ->
234+
TransactionHistoryCard(
235+
transaction = transaction,
236+
)
237+
}
238+
}
239+
}
240+
}
241+
242+
@Composable
243+
private fun TransactionHistoryCard(
244+
transaction: UpiTransaction,
245+
modifier: Modifier = Modifier,
246+
) {
247+
Card(
248+
modifier = modifier.fillMaxWidth(),
249+
colors = CardDefaults.cardColors(
250+
containerColor = KptTheme.colorScheme.surface,
251+
),
252+
elevation = CardDefaults.cardElevation(
253+
defaultElevation = 2.dp,
254+
),
255+
) {
256+
Row(
257+
modifier = Modifier
258+
.fillMaxWidth()
259+
.padding(KptTheme.spacing.md),
260+
verticalAlignment = Alignment.CenterVertically,
261+
horizontalArrangement = Arrangement.spacedBy(KptTheme.spacing.md),
262+
) {
263+
Box(
264+
modifier = Modifier
265+
.size(48.dp)
266+
.background(
267+
color = KptTheme.colorScheme.primaryContainer,
268+
shape = CircleShape,
269+
),
270+
contentAlignment = Alignment.Center,
271+
) {
272+
if (transaction.profileImageUrl != null) {
273+
Text(
274+
text = transaction.payeeName.take(1).uppercase(),
275+
style = KptTheme.typography.bodyLarge,
276+
fontWeight = FontWeight.Bold,
277+
color = KptTheme.colorScheme.onPrimaryContainer,
278+
)
279+
} else {
280+
Text(
281+
text = transaction.payeeName.take(1).uppercase(),
282+
style = KptTheme.typography.bodyLarge,
283+
fontWeight = FontWeight.Bold,
284+
color = KptTheme.colorScheme.onPrimaryContainer,
285+
)
286+
}
287+
}
288+
289+
Column(
290+
modifier = Modifier.weight(1f),
291+
verticalArrangement = Arrangement.spacedBy(KptTheme.spacing.xs),
292+
) {
293+
Text(
294+
text = transaction.payeeName.uppercase(),
295+
style = KptTheme.typography.bodyLarge,
296+
fontWeight = FontWeight.Medium,
297+
color = KptTheme.colorScheme.onSurface,
298+
)
299+
Text(
300+
text = transaction.formattedDate,
301+
style = KptTheme.typography.bodyMedium,
302+
fontWeight = FontWeight.Normal,
303+
color = KptTheme.colorScheme.onSurfaceVariant,
304+
)
305+
}
306+
307+
Text(
308+
text = CurrencyFormatter.format(
309+
balance = transaction.amount,
310+
currencyCode = "INR",
311+
maximumFractionDigits = 2,
312+
),
313+
style = KptTheme.typography.bodyLarge,
314+
fontWeight = FontWeight.Medium,
315+
color = KptTheme.colorScheme.onSurface,
316+
textAlign = TextAlign.End,
317+
)
318+
}
319+
}
320+
}
321+
322+
data class UpiTransaction(
323+
val id: String,
324+
val payeeName: String,
325+
val amount: Double,
326+
val date: LocalDate,
327+
val profileImageUrl: String?,
328+
) {
329+
val formattedDate: String
330+
get() = "${date.dayOfMonth} ${date.month.name.lowercase().replaceFirstChar { it.uppercase() }}"
331+
}
332+
333+
data class MonthTransactionGroup(
334+
val monthYear: String,
335+
val totalAmount: Double,
336+
val transactions: List<UpiTransaction>,
337+
)

0 commit comments

Comments
 (0)