This commit is contained in:
moon 2026-05-22 17:32:58 +06:30
parent af917520b6
commit 030eb6f836
8 changed files with 444 additions and 253 deletions

View File

@ -1,24 +1,12 @@
package com.mob.utsmyanmar.ui.amount
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material.icons.rounded.KeyboardArrowLeft
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.mob.utsmyanmar.ui.components.NumericEntryScreen
import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun AmountScreen(
@ -28,224 +16,29 @@ fun AmountScreen(
) {
var amount by remember { mutableStateOf("") }
val displayAmount = amount.ifEmpty { "0" }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.IvoryBeige)
.navigationBarsPadding()
.statusBarsPadding()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
contentAlignment = Alignment.Center
) {
IconButton(
onClick = onBackClick,
modifier = Modifier.align(Alignment.CenterStart)
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowLeft,
contentDescription = "Back",
tint = Color.LegacyRed
)
}
Text(
text = title,
color = Color.LegacyRed,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(36.dp))
Text(
text = "Enter $title",
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.Bottom
) {
Text(
text = "MMK",
color = Color.LegacyRed,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
)
Text(
text = formatAmount(displayAmount),
color = Color.LegacyRed,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Enter the amount to continue",
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(28.dp))
AmountKeypad(
onNumberClick = { value ->
amount = appendAmountValue(amount, value)
}
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = { },
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = {
NumericEntryScreen(
title = title,
prompt = "Enter $title",
displayValue = formatAmount(amount.ifEmpty { "0" }),
supportingText = "Enter the amount to continue",
confirmText = "Next",
prefixLabel = "MMK",
onBackClick = onBackClick,
onCancelClick = onBackClick,
onConfirmClick = {
if (amount.isNotEmpty()) {
onChargeClick(amount)
}
},
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = amount.isNotEmpty(),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text(
text = "Next",
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(18.dp))
}
Card(
modifier = Modifier
.align(Alignment.TopEnd)//?
.padding(top = 110.dp, end = 20.dp)
.clickable(enabled = amount.isNotEmpty()) {
onKeyClick = { value ->
amount = appendAmountValue(amount, value)
},
onDeleteClick = {
amount = amount.dropLast(1)
},
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Icon(
imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete",
tint = if (amount.isNotEmpty()) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
confirmEnabled = amount.isNotEmpty(),
canDelete = amount.isNotEmpty()
)
}
}
}
@Composable
private fun AmountKeypad(
onNumberClick: (String) -> Unit
) {
val keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
KeypadButton(
text = key,
modifier = Modifier.weight(1f),
onClick = { onNumberClick(key) }
)
}
}
}
}
}
@Composable
private fun KeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(
modifier = modifier
.height(66.dp)
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(8.dp),
clip = false
)
.background(
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.clickable { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = Color.LegacyRed,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}
private fun formatAmount(value: String): String {

View File

@ -0,0 +1,270 @@
package com.mob.utsmyanmar.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material.icons.rounded.KeyboardArrowLeft
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.Color
@Composable
fun NumericEntryScreen(
title: String,
prompt: String,
displayValue: String,
supportingText: String,
confirmText: String,
onBackClick: () -> Unit,
onCancelClick: () -> Unit,
onConfirmClick: () -> Unit,
onKeyClick: (String) -> Unit,
onDeleteClick: () -> Unit,
modifier: Modifier = Modifier,
prefixLabel: String? = null,
errorMessage: String? = null,
confirmEnabled: Boolean = true,
canDelete: Boolean = true,
keys: List<List<String>> = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.IvoryBeige)
.navigationBarsPadding()
.statusBarsPadding()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
contentAlignment = Alignment.Center
) {
IconButton(
onClick = onBackClick,
modifier = Modifier.align(Alignment.CenterStart)
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowLeft,
contentDescription = "Back",
tint = Color.LegacyRed
)
}
Text(
text = title,
color = Color.LegacyRed,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(36.dp))
Text(
text = prompt,
color = Color.Gray,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
verticalAlignment = Alignment.Bottom
) {
if (!prefixLabel.isNullOrBlank()) {
Text(
text = prefixLabel,
color = Color.LegacyRed,
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(end = 10.dp, bottom = 6.dp)
)
}
Text(
text = displayValue,
color = Color.LegacyRed,
fontSize = 32.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(12.dp))
Text(
text = supportingText,
color = if (errorMessage == null) Color.Gray else Color.Error,
fontSize = 11.sp,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(28.dp))
NumericKeypad(
keys = keys,
onKeyClick = onKeyClick
)
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
onClick = onCancelClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.White,
contentColor = Color.LegacyRed
)
) {
Text("Cancel")
}
Button(
onClick = onConfirmClick,
modifier = Modifier
.weight(1f)
.height(56.dp),
enabled = confirmEnabled,
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.LegacyRed,
contentColor = Color.White,
disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f),
disabledContentColor = Color.White
)
) {
Text(
text = confirmText,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(18.dp))
}
Card(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 110.dp, end = 20.dp)
.clickable(enabled = canDelete) {
onDeleteClick()
},
shape = RoundedCornerShape(18.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
) {
Icon(
imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete",
tint = if (canDelete) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
}
}
@Composable
private fun NumericKeypad(
keys: List<List<String>>,
onKeyClick: (String) -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
KeypadButton(
text = key,
modifier = Modifier.weight(1f),
onClick = { onKeyClick(key) }
)
}
}
}
}
}
@Composable
private fun KeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val enabled = text.isNotBlank()
Box(
modifier = modifier
.height(66.dp)
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(8.dp),
clip = false
)
.background(
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.clickable(enabled = enabled) { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = if (enabled) Color.LegacyRed else Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}

View File

@ -20,6 +20,7 @@ import com.mob.utsmyanmar.ui.pinpad.PinPadRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel
import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen
import com.mob.utsmyanmar.ui.refund_rrn.InputRrnRoute
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
import com.mob.utsmyanmar.ui.sending_to_host.SendingToHostRoute
@ -306,6 +307,14 @@ fun AppNavGraph(
pinPadViewModel = pinPadViewModel,
sharedViewModel = sharedViewModel,
transProcessViewModel = transProcessViewModel,
onNavigateInputRrn = {
navController.navigate(Routes.InputRrn.route) {
popUpTo(Routes.PinPad.route) {
inclusive = true
}
launchSingleTop = true
}
},
onNavigateSendingToHost = {
navController.navigate(Routes.SendingToHost.route) {
popUpTo(Routes.PinPad.route) {
@ -318,6 +327,23 @@ fun AppNavGraph(
)
}
composable(Routes.InputRrn.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
InputRrnRoute(
sharedViewModel = sharedViewModel,
onBack = { navController.popBackStack() },
onNavigateSendingToHost = {
navController.navigate(Routes.SendingToHost.route) {
popUpTo(Routes.InputRrn.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
composable(Routes.SendingToHost.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)

View File

@ -22,6 +22,7 @@ sealed class Routes(val route: String) {
data object CardWaiting : Routes("card_waiting")
data object ProcessingCard : Routes("processing_card")
data object PinPad : Routes("pin_pad")
data object InputRrn : Routes("input_rrn")
data object SendingToHost : Routes("sending_to_host")
data object TransactionResult : Routes("transaction_result")
data object PrintReceipt : Routes("print_receipt")

View File

@ -17,6 +17,7 @@ fun PinPadRoute(
pinPadViewModel: PinPadViewModel,
sharedViewModel: SharedViewModel,
transProcessViewModel: TransProcessViewModel,
onNavigateInputRrn: () -> Unit,
onNavigateSendingToHost: () -> Unit,
onBack: () -> Unit
) {
@ -37,8 +38,12 @@ fun PinPadRoute(
payDetail?.tradeAnswerCode = Constant.ANSWER_CODE_APPROVED
sharedViewModel.payDetail.value = payDetail
transProcessViewModel.resetTransactionStatus()
if (sharedViewModel.transactionsType.value == TransactionsType.REFUND) {
onNavigateInputRrn()
} else {
onNavigateSendingToHost()
}
}
PinPadStatus.ON_CANCEL,
PinPadStatus.ON_TIMEOUT,

View File

@ -127,15 +127,6 @@ class ProcessingCardViewModel(
)
return
}
sharedViewModel.transactionsType.value == TransactionsType.REFUND -> {
sendUiEvent(
ProcessingCardUiEvent.NavigateToError(
"Card not supported"
)
)
return
}
}
sharedViewModel.setCardDataExist(true)
@ -207,15 +198,6 @@ class ProcessingCardViewModel(
)
return
}
sharedViewModel.transactionsType.value == TransactionsType.REFUND -> {
sendUiEvent(
ProcessingCardUiEvent.NavigateToError(
"Card not supported"
)
)
return
}
}
sharedViewModel.setCardDataExist(true)

View File

@ -0,0 +1,77 @@
package com.mob.utsmyanmar.ui.refund_rrn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.mob.utsmyanmar.ui.components.NumericEntryScreen
import com.mob.utsmyanmar.viewmodel.SharedViewModel
import com.utsmyanmar.paylibs.utils.POSUtil
private const val RRN_MAX_LENGTH = 12
@Composable
fun InputRrnRoute(
sharedViewModel: SharedViewModel,
onBack: () -> Unit,
onNavigateSendingToHost: () -> Unit
) {
var rrn by rememberSaveable {
mutableStateOf(sharedViewModel.rrNNo.value.orEmpty())
}
var errorMessage by rememberSaveable { mutableStateOf<String?>(null) }
NumericEntryScreen(
title = "Input RRN",
prompt = "Enter RRN",
displayValue = rrn.ifEmpty { "0" },
supportingText = errorMessage ?: "Enter the retrieval reference number to continue",
confirmText = "Next",
onBackClick = onBack,
onCancelClick = onBack,
onConfirmClick = {
val trimmedRrn = rrn.trim()
if (POSUtil.getInstance().checkNumberField(trimmedRrn)) {
errorMessage = "Invalid RRN"
return@NumericEntryScreen
}
sharedViewModel.rrNNo.value = trimmedRrn
sharedViewModel.payDetail.value?.let { payDetail ->
payDetail.referNo = trimmedRrn
sharedViewModel.payDetail.value = payDetail
}
onNavigateSendingToHost()
},
onKeyClick = { value ->
rrn = appendRrnValue(rrn, value)
errorMessage = null
},
onDeleteClick = {
rrn = rrn.dropLast(1)
errorMessage = null
},
confirmEnabled = rrn.isNotEmpty(),
canDelete = rrn.isNotEmpty(),
keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "00")
)
)
}
private fun appendRrnValue(current: String, value: String): String {
if (value.isBlank()) {
return current
}
val remainingDigits = RRN_MAX_LENGTH - current.length
if (remainingDigits <= 0) {
return current
}
return current + value.take(remainingDigits)
}

View File

@ -451,6 +451,7 @@ class SharedViewModel @Inject constructor(
when (transactionsType.value) {
TransactionsType.SETTLEMENT -> saveMockSettlementForTesting()
TransactionsType.VOID -> saveMockApprovedVoidForTesting()
TransactionsType.REFUND -> saveMockApprovedRefundForTesting()
else -> saveMockApprovedSaleForVoidTesting()
}
}
@ -558,6 +559,42 @@ class SharedViewModel @Inject constructor(
approvalCode.value = mockApprovalCode
}
fun saveMockApprovedRefundForTesting() {
val refundDetail = payDetail.value ?: return
val systemParams = SystemParamsOperation.getInstance()
val mockTraceNo = systemParams.incrementSerialNum
val mockInvoiceNo = systemParams.incrementInvoiceNum
val mockApprovalCode = mockTraceNo.takeLast(6).padStart(6, '0')
val refundRrn = rrNNo.value?.trim().orEmpty()
refundDetail.voucherNo = mockTraceNo
refundDetail.invoiceNo = mockInvoiceNo
refundDetail.referNo = refundRrn
refundDetail.approvalCode = mockApprovalCode
refundDetail.authNo = mockApprovalCode
refundDetail.tradeAnswerCode = "00"
refundDetail.tradeResultDes = "MOCK REFUND APPROVED"
refundDetail.transactionType = TransactionType.REFUND
refundDetail.transType = TransactionsType.REFUND.name
refundDetail.isCanceled = false
refundDetail.isSettle = false
refundDetail.isNeedReversal = false
refundDetail.isReturnGood = false
refundDetail.TradeDate = SystemDateTime.getMMDD()
refundDetail.TradeTime = SystemDateTime.getHHmmss()
refundDetail.tradeDateAndTime = SystemDateTime.getMMDDhhmmss()
refundDetail.tradeDateTime = SystemDateTime.getYYMMDDhhmmss()
refundDetail.transDate = SystemDateTime.getTodayDateFormat()
refundDetail.transTime = SystemDateTime.getTodayTimeFormat()
repository.insertPayDetail(refundDetail)
payDetail.value = refundDetail
traceNo.value = mockTraceNo
rrNNo.value = refundRrn
approvalCode.value = mockApprovalCode
}
fun saveMockSettlementForTesting() {
val systemParams = SystemParamsOperation.getInstance()
val mockTraceNo = systemParams.incrementSerialNum