From 211e633517de09fa63e80a01974a50f03adcd2f4 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:17:33 +0630 Subject: [PATCH 1/7] side bar modified --- .../ui/dashboard/DashboardScreen2.kt | 221 ++++++++++++++++-- .../ui/functions/FunctionsScreen.kt | 13 +- .../main/res/drawable/ic_cancel_circle.xml | 9 + app/src/main/res/drawable/ic_four_boxes.xml | 9 + app/src/main/res/drawable/ic_lock.xml | 28 +-- .../main/res/drawable/ic_up_down_arrow.xml | 9 + app/src/main/res/drawable/ic_version.xml | 30 +-- 7 files changed, 245 insertions(+), 74 deletions(-) create mode 100644 app/src/main/res/drawable/ic_cancel_circle.xml create mode 100644 app/src/main/res/drawable/ic_four_boxes.xml create mode 100644 app/src/main/res/drawable/ic_up_down_arrow.xml diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt index 05a2a6d..3b5ee78 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt @@ -6,15 +6,53 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.Image 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.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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.filled.BarChart +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,6 +61,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel @@ -54,7 +93,7 @@ fun DashboardScreen2( deviceInfoViewModel.loadDeviceInfo() } - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val drawerState = rememberDrawerState(initialValue = DrawerValue.Open) val scope = rememberCoroutineScope() val mainHandler = remember { Handler(Looper.getMainLooper()) } var showHostActionDialog by remember { mutableStateOf(false) } @@ -215,32 +254,96 @@ fun DashboardScreen2( } Spacer(modifier = Modifier.height(10.dp)) + Text( + text = "Connection Settings", + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Item( + title = "Echo Test", subTitle = "Test Connection Status", + onClick = { + scope.launch { drawerState.close() } + openHostActionDialog("Echo Test") + }, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_up_down_arrow), + contentDescription = "icon", + tint = Color.LegacyRed + ) + }, + trailingIcon = {}, + ) + Item( + title = "Log-On", subTitle = "Log on to System", + onClick = { + scope.launch { drawerState.close() } + openHostActionDialog("Log-On") + }, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_lock), + contentDescription = "icon", + tint = Color.LegacyRed + ) + }, + trailingIcon = {}, + ) + Item( + title = "Log-Off", subTitle = "Log off from System", + onClick = { + scope.launch { drawerState.close() } + openHostActionDialog("Log-Off") + }, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_cancel_circle), + contentDescription = "icon", + tint = Color.LegacyRed + ) + }, + trailingIcon = {}, + ) - DrawerItem("Log-On", Icons.Default.Dashboard) { - scope.launch { drawerState.close() } - openHostActionDialog("Log-On") - } - DrawerItem("Echo Test", Icons.Default.Sync) { - scope.launch { drawerState.close() } - openHostActionDialog("Echo Test") - } - DrawerItem("Log-Off", Icons.Default.Dashboard) { - scope.launch { drawerState.close() } - openHostActionDialog("Log-Off") - } Text( text = "System Management", fontWeight = FontWeight.Medium, modifier = Modifier.padding(horizontal = 16.dp) ) - DrawerItem("Function", Icons.Default.Dashboard) { - scope.launch { drawerState.close() } - onNavigateFunctions() - } - DrawerItem("Version", Icons.Default.Dashboard) { - scope.launch { drawerState.close() } - onNavigateVersion() - } + + Item( + title = "Functions", subTitle = "System Function Settings", + onClick = { + scope.launch { drawerState.close() } + onNavigateFunctions() + }, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_four_boxes), + contentDescription = "icon", + tint = Color.LegacyRed, + modifier = Modifier.size(16.dp) + ) + }, + trailingIcon = {}, + ) + Item( + title = "Version", subTitle = "View App Version Info", + onClick = { + scope.launch { drawerState.close() } + onNavigateVersion() + }, + leadingIcon = { + Icon( + painterResource(R.drawable.ic_version), + contentDescription = "icon", + tint = Color.LegacyRed + ) + }, + trailingIcon = {}, + ) + } }) { Scaffold( @@ -597,3 +700,71 @@ fun PreviewDashboardScreen2() { fun PreviewDashboardScreen3() { DashboardScreen2() } + + +@Preview +@Composable +fun PreviewItem() { + Item( + onClick = {}, + title = "title", + subTitle = "sub-title" + ) +} + + +@Composable +fun Item( + onClick: () -> Unit, + title: String, + subTitle: String, + leadingIcon: (@Composable () -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, +) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.Black + ), + modifier = Modifier.fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + ) { + // Square icon background + leadingIcon?.let { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(16.dp)) // change to RectangleShape for sharp corners + .background(Color.LegacyRed.copy(alpha = 0.1f)) + .padding(8.dp) + ) { + it() + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text(text = title) + Text( + text = subTitle, + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + + trailingIcon?.invoke() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/functions/FunctionsScreen.kt b/app/src/main/java/com/mob/utsmyanmar/ui/functions/FunctionsScreen.kt index 3211b17..095401a 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/functions/FunctionsScreen.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/functions/FunctionsScreen.kt @@ -1,13 +1,11 @@ package com.mob.utsmyanmar.ui.functions -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement 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.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -17,7 +15,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.filled.OnDeviceTraining import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ElevatedButton import androidx.compose.material3.Icon @@ -30,15 +27,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.mob.utsmyanmar.R import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.preview.P2Preview import com.mob.utsmyanmar.ui.preview.P3Preview import com.mob.utsmyanmar.ui.theme.Color -import com.mob.utsmyanmar.R +import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation + @Composable fun FunctionsScreen( onBack: () -> Unit = {} ) { + val tmsAddress = SystemParamsOperation.getInstance().tmsAddress + Scaffold( containerColor = Color.IvoryBeige, topBar = { @@ -139,7 +140,7 @@ fun FunctionsScreen( FunctionButton( onClick = {}, title = "TMS Server Url", - subTitle = "Detail for bound hosts", + subTitle = tmsAddress, leadingIcon = { Icon( modifier = Modifier.size(24.dp), @@ -232,4 +233,4 @@ fun PreviewFunctionButton() { title = "title", subTitle = "sub-title" ) -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/ic_cancel_circle.xml b/app/src/main/res/drawable/ic_cancel_circle.xml new file mode 100644 index 0000000..1d7afd1 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_four_boxes.xml b/app/src/main/res/drawable/ic_four_boxes.xml new file mode 100644 index 0000000..a8f1aa6 --- /dev/null +++ b/app/src/main/res/drawable/ic_four_boxes.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml index bb9afa0..6ef732a 100644 --- a/app/src/main/res/drawable/ic_lock.xml +++ b/app/src/main/res/drawable/ic_lock.xml @@ -1,25 +1,9 @@ - + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + android:pathData="M240,880q-33,0 -56.5,-23.5T160,800v-400q0,-33 23.5,-56.5T240,320h40v-80q0,-83 58.5,-141.5T480,40q83,0 141.5,58.5T680,240v80h40q33,0 56.5,23.5T800,400v400q0,33 -23.5,56.5T720,880L240,880ZM240,800h480v-400L240,400v400ZM536.5,656.5Q560,633 560,600t-23.5,-56.5Q513,520 480,520t-56.5,23.5Q400,567 400,600t23.5,56.5Q447,680 480,680t56.5,-23.5ZM360,320h240v-80q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85v80ZM240,800v-400,400Z" + android:fillColor="#e3e3e3"/> diff --git a/app/src/main/res/drawable/ic_up_down_arrow.xml b/app/src/main/res/drawable/ic_up_down_arrow.xml new file mode 100644 index 0000000..87e3175 --- /dev/null +++ b/app/src/main/res/drawable/ic_up_down_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_version.xml b/app/src/main/res/drawable/ic_version.xml index 77b3fea..916b16d 100644 --- a/app/src/main/res/drawable/ic_version.xml +++ b/app/src/main/res/drawable/ic_version.xml @@ -1,24 +1,12 @@ - + android:width="800dp" + android:height="800dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M20.245,14.75C21.18,15.364 21.137,16.787 20.117,17.326L12.935,21.122C12.35,21.432 11.65,21.432 11.066,21.122L3.884,17.326C2.863,16.787 2.82,15.364 3.755,14.75L3.818,14.789L3.818,14.789L11.065,18.622C11.65,18.931 12.35,18.931 12.935,18.622L20.116,14.826C20.161,14.802 20.204,14.777 20.245,14.75ZM20.245,10.75C21.139,11.337 21.139,12.665 20.244,13.251L20.117,13.326L12.935,17.122C12.403,17.403 11.777,17.429 11.228,17.199L11.066,17.122L3.884,13.326C2.863,12.787 2.82,11.364 3.755,10.75L3.818,10.789L3.818,10.789L11.065,14.622C11.597,14.903 12.224,14.929 12.773,14.699L12.935,14.622L20.116,10.826C20.161,10.802 20.204,10.777 20.245,10.75ZM12.935,2.878L20.116,6.674C21.182,7.237 21.182,8.763 20.116,9.326L12.935,13.123C12.35,13.432 11.65,13.432 11.065,13.123L3.884,9.326C2.818,8.763 2.818,7.237 3.884,6.674L11.065,2.878C11.65,2.569 12.35,2.569 12.935,2.878Z" + android:strokeWidth="1" + android:fillColor="#09244B" + android:fillType="nonZero" + android:strokeColor="#00000000"/> From d745815f743ef96d06a9de232a97281229ec2068 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:52:51 +0630 Subject: [PATCH 2/7] password screen added --- .../main/java/com/mob/utsmyanmar/AGENTS.md | 6 +- .../ui/dashboard/DashboardScreen2.kt | 2 +- .../utsmyanmar/ui/navigation/AppNavGraph.kt | 57 +++- .../mob/utsmyanmar/ui/navigation/Routes.kt | 4 + .../ui/password_input/PasswordInput.kt | 303 ++++++++++++++++++ 5 files changed, 353 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/AGENTS.md b/app/src/main/java/com/mob/utsmyanmar/AGENTS.md index c0ef129..898970e 100644 --- a/app/src/main/java/com/mob/utsmyanmar/AGENTS.md +++ b/app/src/main/java/com/mob/utsmyanmar/AGENTS.md @@ -1,3 +1,7 @@ ## Working agreements -- use Scaffold and AppBar() in every main Screen \ No newline at end of file +- use Scaffold and AppBar() in every main Screen + +### Re-usable Screens +- PasswordInput.kt +- InputAmountScreen.kt \ No newline at end of file diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt index 3b5ee78..e071274 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt @@ -93,7 +93,7 @@ fun DashboardScreen2( deviceInfoViewModel.loadDeviceInfo() } - val drawerState = rememberDrawerState(initialValue = DrawerValue.Open) + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() val mainHandler = remember { Handler(Looper.getMainLooper()) } var showHostActionDialog by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt index e1b3e35..a88953d 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt @@ -1,49 +1,51 @@ package com.mob.utsmyanmar.ui.navigation import android.annotation.SuppressLint +import android.net.Uri import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.mob.utsmyanmar.model.ProcessCode -import com.mob.utsmyanmar.ui.input_amount.AmountRoute import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2 import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel +import com.mob.utsmyanmar.ui.functions.FunctionsScreen +import com.mob.utsmyanmar.ui.input_amount.AmountRoute +import com.mob.utsmyanmar.ui.password_input.InputPassword +import com.mob.utsmyanmar.ui.password_input.PasswordType import com.mob.utsmyanmar.ui.pinpad.PinPadRoute +import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel +import com.mob.utsmyanmar.ui.print_receipt.PrintReceiptScreen 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.sale_void.TranDetailPage +import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen +import com.mob.utsmyanmar.ui.sale_void.VoidViewModel import com.mob.utsmyanmar.ui.sending_to_host.ProcessingRoute import com.mob.utsmyanmar.ui.settlement.SettlementScreen -import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute -import com.mob.utsmyanmar.ui.sale_void.TranDetailPage -import com.mob.utsmyanmar.ui.sale_void.VoidViewModel -import com.mob.utsmyanmar.ui.sale_void.VoidTraceScreen -import com.mob.utsmyanmar.viewmodel.CardReaderViewModel -import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel -import com.mob.utsmyanmar.ui.pinpad.PinPadViewModel import com.mob.utsmyanmar.ui.settlement.SettlementViewModel -import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent -import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel -import com.mob.utsmyanmar.ui.functions.FunctionsScreen +import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen +import com.mob.utsmyanmar.ui.sign_on.SignOnRoute import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel +import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent +import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute +import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel import com.mob.utsmyanmar.ui.version.VersionScreen +import com.mob.utsmyanmar.viewmodel.CardReaderViewModel +import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel import com.mob.utsmyanmar.viewmodel.SharedViewModel import com.mob.utsmyanmar.viewmodel.TransProcessViewModel -import com.utsmyanmar.ecr.data.TransType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType @SuppressLint("ContextCastToActivity") @@ -104,7 +106,7 @@ fun AppNavGraph( navController.navigate(Routes.Version.route) }, onNavigateFunctions = { - navController.navigate(Routes.Functions.route) { + navController.navigate(Routes.Password.createRoute(Routes.Functions.route, PasswordType.SETTING)) { launchSingleTop = true } } @@ -136,6 +138,27 @@ fun AppNavGraph( ) } + composable( + route = Routes.Password.route, + arguments = listOf( + navArgument("destination") { type = NavType.StringType }, + navArgument("passwordType") { type = NavType.StringType } + ) + ) { backStackEntry -> + val destination = Uri.decode(backStackEntry.arguments?.getString("destination").orEmpty()) + val passwordType = backStackEntry.arguments?.getString("passwordType").orEmpty() + InputPassword( + passwordType = passwordType, + onBack = { navController.popBackStack() }, + onPasswordCorrect = { + navController.navigate(destination) { + popUpTo(Routes.Password.route) { inclusive = true } + launchSingleTop = true + } + } + ) + } + composable(Routes.Functions.route) { FunctionsScreen( onBack = { navController.popBackStack() } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt index c798df9..9da6e55 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt @@ -29,4 +29,8 @@ sealed class Routes(val route: String) { data object PrintReceipt : Routes("print_receipt") data object Version : Routes("version") data object Functions : Routes("functions") + data object Password : Routes("password/{destination}/{passwordType}") { + fun createRoute(destination: String, passwordType: String): String = + "password/${Uri.encode(destination)}/$passwordType" + } } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt new file mode 100644 index 0000000..acf48ed --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt @@ -0,0 +1,303 @@ +package com.mob.utsmyanmar.ui.password_input + +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.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Backspace +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.Scaffold +import androidx.compose.material3.Text +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 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.components.appbar.AppBar +import com.mob.utsmyanmar.ui.preview.P2Preview +import com.mob.utsmyanmar.ui.preview.P3Preview +import com.mob.utsmyanmar.ui.theme.Color + +object PasswordType { + const val SYSTEM = "system_password" + const val SETTLEMENT = "settlement_password" + const val SETTING = "setting_password" +} + +private val passwords = mapOf( + PasswordType.SYSTEM to "111111", + PasswordType.SETTING to "222222", + PasswordType.SETTLEMENT to "123456", +) + +private const val PASSWORD_LENGTH = 6 + +@Composable +fun InputPassword( + passwordType: String, + onBack: () -> Unit = {}, + onPasswordCorrect: () -> Unit = {} +) { + val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!! + var input by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf(null) } + + Scaffold( + topBar = { + AppBar(title = "Password", icon = Icons.Default.ArrowBackIosNew, onIconClick = onBack) + }, + containerColor = Color.IvoryBeige + ) { paddingValues -> + PasswordEntryScreen( + modifier = Modifier.padding(paddingValues), + input = input, + errorMessage = errorMessage, + onBack = onBack, + onConfirm = { + if (input == expectedPassword) { + onPasswordCorrect() + } else { + errorMessage = "Incorrect password" + input = "" + } + }, + onKey = { digit -> + if (input.length < PASSWORD_LENGTH) { + input += digit + errorMessage = null + } + }, + onDelete = { + input = input.dropLast(1) + errorMessage = null + } + ) + } +} + +@Composable +private fun PasswordEntryScreen( + input: String, + errorMessage: String?, + onBack: () -> Unit, + onConfirm: () -> Unit, + onKey: (String) -> Unit, + onDelete: () -> Unit, + modifier: Modifier = Modifier +) { + val keys = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf("", "0", "") + ) + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .navigationBarsPadding() + .statusBarsPadding() + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp) + ) { + Text( + text = "Enter Password", + color = Color.Gray, + fontSize = 11.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PasswordDots( + filledCount = input.length, + totalCount = PASSWORD_LENGTH, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = errorMessage ?: "", + color = Color.LegacyRed, + fontSize = 11.sp, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + NumericKeypad(keys = keys, onKeyClick = onKey) + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = onBack, + modifier = Modifier.weight(1f).height(56.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.LegacyRed + ) + ) { + Text("Cancel") + } + + Button( + onClick = onConfirm, + modifier = Modifier.weight(1f).height(56.dp), + enabled = input.length == PASSWORD_LENGTH, + 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 = 32.dp, end = 20.dp) + .clickable(enabled = input.isNotEmpty()) { onDelete() }, + 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 (input.isNotEmpty()) Color.LegacyRed else Color.Gray, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp) + ) + } + } +} + +@Composable +private fun PasswordDots( + filledCount: Int, + totalCount: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(totalCount) { index -> + Box( + modifier = Modifier + .size(14.dp) + .background( + color = if (index < filledCount) Color.LegacyRed else Color.Gray.copy(alpha = 0.3f), + shape = CircleShape + ) + ) + } + } +} + +@Composable +private fun NumericKeypad( + keys: List>, + 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) + .then( + if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false) + else Modifier + ) + .background( + color = if (enabled) Color.White else androidx.compose.ui.graphics.Color.Transparent, + shape = RoundedCornerShape(8.dp) + ) + .clickable(enabled = enabled) { onClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + color = if (enabled) Color.LegacyRed else androidx.compose.ui.graphics.Color.Transparent, + fontSize = 24.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center + ) + } +} + +@P3Preview +@P2Preview +@Composable +fun PreviewInputPassword() { + InputPassword(passwordType = PasswordType.SETTING) +} From 582b08ceadf204d823bc8c3ccdd4a99dd6ea9026 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:46:17 +0630 Subject: [PATCH 3/7] responsive keypad --- .../ui/components/NumericEntryScreen.kt | 62 ---- .../utsmyanmar/ui/components/NumericKeypad.kt | 104 ++++++ .../ui/input_amount/InputAmountScreen.kt | 78 +---- .../ui/password_input/PasswordInput.kt | 313 +++++++----------- 4 files changed, 232 insertions(+), 325 deletions(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/components/NumericKeypad.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericEntryScreen.kt b/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericEntryScreen.kt index 93e310d..5fc4ee0 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericEntryScreen.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericEntryScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Backspace -import androidx.compose.material.icons.rounded.Backspace import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -26,9 +25,7 @@ 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 @@ -181,62 +178,3 @@ fun NumericEntryScreen( } } } - -@Composable -private fun NumericKeypad( - keys: List>, - 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 - ) - } -} diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericKeypad.kt b/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericKeypad.kt new file mode 100644 index 0000000..bc1e990 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/components/NumericKeypad.kt @@ -0,0 +1,104 @@ +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphics.Color as ComposeColor +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mob.utsmyanmar.ui.theme.Color + +@Composable +fun NumericKeypad( + modifier: Modifier = Modifier, + onKeyClick: (String) -> Unit, + keys : List>, +) { +// val keys : List> = listOf( +// listOf("1", "2", "3"), +// listOf("4", "5", "6"), +// listOf("7", "8", "9"), +// listOf(".", "0", "00") +// ) + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + keys.forEach { row -> + Row( + modifier = Modifier.fillMaxWidth().weight(1f), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + row.forEach { key -> + KeypadButton( + text = key, + modifier = Modifier.weight(1f).fillMaxHeight(), + onClick = { onKeyClick(key) } + ) + } + } + } + } +} + +@Composable +fun KeypadButton( + text: String, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + val enabled = text.isNotBlank() + + Box( + modifier = modifier + .then( + if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false) + else Modifier + ) + .background( + color = if (enabled) Color.White else ComposeColor.Transparent, + shape = RoundedCornerShape(8.dp) + ) + .clickable(enabled = enabled) { onClick() }, + contentAlignment = Alignment.Center + ) { + if (enabled) { + Text( + text = text, + color = Color.LegacyRed, + fontSize = 24.sp, + fontWeight = FontWeight.Normal, + textAlign = TextAlign.Center + ) + } + } +} + +@Preview +@Composable +fun PreviewNumericKeypad(){ + val keys : List> = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf(".", "0", "00") + ) + NumericKeypad( + keys = keys, + onKeyClick = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/input_amount/InputAmountScreen.kt b/app/src/main/java/com/mob/utsmyanmar/ui/input_amount/InputAmountScreen.kt index 84d7b87..66b6b93 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/input_amount/InputAmountScreen.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/input_amount/InputAmountScreen.kt @@ -1,20 +1,17 @@ package com.mob.utsmyanmar.ui.input_amount -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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Backspace import androidx.compose.material.icons.rounded.Backspace import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -30,16 +27,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.components.NumericKeypad import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.preview.P2Preview import com.mob.utsmyanmar.ui.preview.P3Preview import com.mob.utsmyanmar.ui.theme.Color -import kotlin.collections.List + +private val amountKeys = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf(".", "0", "00") +) @Composable fun InputAmount( @@ -140,6 +142,7 @@ fun InputAmount( verticalArrangement = Arrangement.Bottom ){ NumericKeypad( + keys = amountKeys, modifier = Modifier.fillMaxSize(), onKeyClick = { value -> amount = appendAmountValue(amount, value) @@ -202,69 +205,6 @@ fun InputAmount( } } -@Composable -private fun NumericKeypad( - modifier: Modifier = Modifier, - onKeyClick: (String) -> Unit -) { - val keys : List> = listOf( - listOf("1", "2", "3"), - listOf("4", "5", "6"), - listOf("7", "8", "9"), - listOf(".", "0", "00") - ) - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(6.dp) - ) { - keys.forEach { row -> - Row( - modifier = Modifier.fillMaxWidth().weight(1f), - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - row.forEach { key -> - KeypadButton( - text = key, - modifier = Modifier.weight(1f).fillMaxHeight(), - onClick = { onKeyClick(key) } - ) - } - } - } - } -} - -@Composable -private fun KeypadButton( - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - val enabled = text.isNotBlank() - - Box( - modifier = modifier - .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 - ) - } -} private fun appendAmountValue(current: String, value: String): String { if (value == ".") { diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt index acf48ed..5cc027e 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt @@ -10,10 +10,8 @@ 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.size -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -33,30 +31,36 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.components.NumericKeypad import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.preview.P2Preview import com.mob.utsmyanmar.ui.preview.P3Preview import com.mob.utsmyanmar.ui.theme.Color object PasswordType { - const val SYSTEM = "system_password" + const val SYSTEM = "system_password" const val SETTLEMENT = "settlement_password" - const val SETTING = "setting_password" + const val SETTING = "setting_password" } private val passwords = mapOf( PasswordType.SYSTEM to "111111", PasswordType.SETTING to "222222", - PasswordType.SETTLEMENT to "123456", + PasswordType.SETTLEMENT to "123456" ) private const val PASSWORD_LENGTH = 6 +private val passwordKeys = listOf( + listOf("1", "2", "3"), + listOf("4", "5", "6"), + listOf("7", "8", "9"), + listOf("", "0", "") +) + @Composable fun InputPassword( passwordType: String, @@ -73,154 +77,132 @@ fun InputPassword( }, containerColor = Color.IvoryBeige ) { paddingValues -> - PasswordEntryScreen( - modifier = Modifier.padding(paddingValues), - input = input, - errorMessage = errorMessage, - onBack = onBack, - onConfirm = { - if (input == expectedPassword) { - onPasswordCorrect() - } else { - errorMessage = "Incorrect password" - input = "" - } - }, - onKey = { digit -> - if (input.length < PASSWORD_LENGTH) { - input += digit - errorMessage = null - } - }, - onDelete = { - input = input.dropLast(1) - errorMessage = null - } - ) - } -} - -@Composable -private fun PasswordEntryScreen( - input: String, - errorMessage: String?, - onBack: () -> Unit, - onConfirm: () -> Unit, - onKey: (String) -> Unit, - onDelete: () -> Unit, - modifier: Modifier = Modifier -) { - val keys = listOf( - listOf("1", "2", "3"), - listOf("4", "5", "6"), - listOf("7", "8", "9"), - listOf("", "0", "") - ) - - Box( - modifier = modifier - .fillMaxSize() - .background(Color.White) - .navigationBarsPadding() - .statusBarsPadding() - ) { Column( modifier = Modifier + .padding(paddingValues) .fillMaxSize() - .padding(horizontal = 20.dp) + .padding(16.dp) ) { - Text( - text = "Enter Password", - color = Color.Gray, - fontSize = 11.sp, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) + Spacer(Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(16.dp)) - - PasswordDots( - filledCount = input.length, - totalCount = PASSWORD_LENGTH, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = errorMessage ?: "", - color = Color.LegacyRed, - fontSize = 11.sp, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - - Spacer(modifier = Modifier.height(24.dp)) - - NumericKeypad(keys = keys, onKeyClick = onKey) - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) + Box( + modifier = Modifier + .fillMaxSize() + .weight(2f) ) { - Button( - onClick = onBack, - modifier = Modifier.weight(1f).height(56.dp), - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.White, - contentColor = Color.LegacyRed - ) + Card( + modifier = Modifier + .align(Alignment.CenterEnd) + .clickable(enabled = input.isNotEmpty()) { + input = input.dropLast(1) + errorMessage = null + }, + shape = RoundedCornerShape(18.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) ) { - Text("Cancel") + Icon( + imageVector = Icons.Rounded.Backspace, + contentDescription = "Delete", + tint = if (input.isNotEmpty()) Color.LegacyRed else Color.Gray, + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp) + ) } - Button( - onClick = onConfirm, - modifier = Modifier.weight(1f).height(56.dp), - enabled = input.length == PASSWORD_LENGTH, - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors( - containerColor = Color.LegacyRed, - contentColor = Color.White, - disabledContainerColor = Color.LegacyRed.copy(alpha = 0.5f), - disabledContentColor = Color.White - ) + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Text(text = "Next", fontSize = 14.sp, fontWeight = FontWeight.Medium) + Text( + text = "Enter Password", + color = Color.Gray, + fontSize = 18.sp + ) + Spacer(Modifier.height(16.dp)) + PasswordDots(filledCount = input.length, totalCount = PASSWORD_LENGTH) + Spacer(Modifier.height(8.dp)) + Text( + text = errorMessage ?: "", + color = Color.LegacyRed, + fontSize = 14.sp + ) } } - Spacer(modifier = Modifier.height(18.dp)) - } + // Keypad — mirrors InputAmountScreen weight(3f) section + Column( + modifier = Modifier + .fillMaxWidth() + .weight(3f), + verticalArrangement = Arrangement.Bottom + ) { + NumericKeypad( + keys = passwordKeys, + modifier = Modifier.fillMaxSize(), + onKeyClick = { digit -> + if (input.length < PASSWORD_LENGTH) { + input += digit + errorMessage = null + } + } + ) + } - Card( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 32.dp, end = 20.dp) - .clickable(enabled = input.isNotEmpty()) { onDelete() }, - 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 (input.isNotEmpty()) Color.LegacyRed else Color.Gray, - modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp) - ) + Spacer(Modifier.height(16.dp)) + + // Action buttons — mirrors InputAmountScreen weight(0.5f) section + Box( + modifier = Modifier + .fillMaxWidth() + .weight(0.5f) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = onBack, + modifier = Modifier.weight(1f).height(56.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = Color.LegacyRed + ) + ) { + Text("Cancel") + } + + Button( + onClick = { + if (input == expectedPassword) { + onPasswordCorrect() + } else { + errorMessage = "Incorrect password" + input = "" + } + }, + modifier = Modifier.weight(1f).height(56.dp), + enabled = input.length == PASSWORD_LENGTH, + 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) + } + } + } } } } @Composable -private fun PasswordDots( - filledCount: Int, - totalCount: Int, - modifier: Modifier = Modifier -) { +private fun PasswordDots(filledCount: Int, totalCount: Int) { Row( - modifier = modifier, horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -229,7 +211,8 @@ private fun PasswordDots( modifier = Modifier .size(14.dp) .background( - color = if (index < filledCount) Color.LegacyRed else Color.Gray.copy(alpha = 0.3f), + color = if (index < filledCount) Color.LegacyRed + else Color.Gray.copy(alpha = 0.3f), shape = CircleShape ) ) @@ -237,64 +220,6 @@ private fun PasswordDots( } } -@Composable -private fun NumericKeypad( - keys: List>, - 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) - .then( - if (enabled) Modifier.shadow(elevation = 2.dp, shape = RoundedCornerShape(8.dp), clip = false) - else Modifier - ) - .background( - color = if (enabled) Color.White else androidx.compose.ui.graphics.Color.Transparent, - shape = RoundedCornerShape(8.dp) - ) - .clickable(enabled = enabled) { onClick() }, - contentAlignment = Alignment.Center - ) { - Text( - text = text, - color = if (enabled) Color.LegacyRed else androidx.compose.ui.graphics.Color.Transparent, - fontSize = 24.sp, - fontWeight = FontWeight.Normal, - textAlign = TextAlign.Center - ) - } -} - @P3Preview @P2Preview @Composable From 3a63c65e8f526ae3823ad65ba9e205459d543ba3 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:57:14 +0630 Subject: [PATCH 4/7] update to MVVM --- .../ui/password_input/PasswordInput.kt | 58 +++++++------------ .../ui/password_input/PasswordInputUiState.kt | 11 ++++ .../password_input/PasswordInputViewModel.kt | 40 +++++++++++++ 3 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputUiState.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputViewModel.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt index 5cc027e..a4ba592 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInput.kt @@ -25,15 +25,15 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import com.mob.utsmyanmar.ui.components.NumericKeypad import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.preview.P2Preview @@ -52,8 +52,6 @@ private val passwords = mapOf( PasswordType.SETTLEMENT to "123456" ) -private const val PASSWORD_LENGTH = 6 - private val passwordKeys = listOf( listOf("1", "2", "3"), listOf("4", "5", "6"), @@ -65,11 +63,13 @@ private val passwordKeys = listOf( fun InputPassword( passwordType: String, onBack: () -> Unit = {}, - onPasswordCorrect: () -> Unit = {} + onPasswordCorrect: () -> Unit = {}, + viewModel: PasswordInputViewModel = viewModel() ) { val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!! - var input by remember { mutableStateOf("") } - var errorMessage by remember { mutableStateOf(null) } + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { viewModel.reset() } Scaffold( topBar = { @@ -85,6 +85,7 @@ fun InputPassword( ) { Spacer(Modifier.height(8.dp)) + // Display area Box( modifier = Modifier .fillMaxSize() @@ -92,11 +93,8 @@ fun InputPassword( ) { Card( modifier = Modifier - .align(Alignment.CenterEnd) - .clickable(enabled = input.isNotEmpty()) { - input = input.dropLast(1) - errorMessage = null - }, + .align(Alignment.TopEnd) + .clickable(enabled = uiState.canDelete, onClick = viewModel::onDelete), shape = RoundedCornerShape(18.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp) @@ -104,7 +102,7 @@ fun InputPassword( Icon( imageVector = Icons.Rounded.Backspace, contentDescription = "Delete", - tint = if (input.isNotEmpty()) Color.LegacyRed else Color.Gray, + tint = if (uiState.canDelete) Color.LegacyRed else Color.Gray, modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp) ) } @@ -114,23 +112,19 @@ fun InputPassword( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = "Enter Password", - color = Color.Gray, - fontSize = 18.sp - ) + Text(text = "Enter Password", color = Color.Gray, fontSize = 18.sp) Spacer(Modifier.height(16.dp)) - PasswordDots(filledCount = input.length, totalCount = PASSWORD_LENGTH) + PasswordDots(filledCount = uiState.input.length, totalCount = PASSWORD_LENGTH) Spacer(Modifier.height(8.dp)) Text( - text = errorMessage ?: "", + text = uiState.errorMessage ?: "", color = Color.LegacyRed, fontSize = 14.sp ) } } - // Keypad — mirrors InputAmountScreen weight(3f) section + // Keypad — stable lambda: viewModel::onKey never changes between recompositions Column( modifier = Modifier .fillMaxWidth() @@ -140,18 +134,13 @@ fun InputPassword( NumericKeypad( keys = passwordKeys, modifier = Modifier.fillMaxSize(), - onKeyClick = { digit -> - if (input.length < PASSWORD_LENGTH) { - input += digit - errorMessage = null - } - } + onKeyClick = viewModel::onKey ) } Spacer(Modifier.height(16.dp)) - // Action buttons — mirrors InputAmountScreen weight(0.5f) section + // Action buttons Box( modifier = Modifier .fillMaxWidth() @@ -174,16 +163,9 @@ fun InputPassword( } Button( - onClick = { - if (input == expectedPassword) { - onPasswordCorrect() - } else { - errorMessage = "Incorrect password" - input = "" - } - }, + onClick = { viewModel.validate(expectedPassword, onPasswordCorrect) }, modifier = Modifier.weight(1f).height(56.dp), - enabled = input.length == PASSWORD_LENGTH, + enabled = uiState.isConfirmEnabled, shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.buttonColors( containerColor = Color.LegacyRed, diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputUiState.kt b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputUiState.kt new file mode 100644 index 0000000..fc00b5c --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputUiState.kt @@ -0,0 +1,11 @@ +package com.mob.utsmyanmar.ui.password_input + +internal const val PASSWORD_LENGTH = 6 + +data class PasswordInputUiState( + val input: String = "", + val errorMessage: String? = null +) { + val isConfirmEnabled: Boolean get() = input.length == PASSWORD_LENGTH + val canDelete: Boolean get() = input.isNotEmpty() +} diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputViewModel.kt b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputViewModel.kt new file mode 100644 index 0000000..37b36d0 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/password_input/PasswordInputViewModel.kt @@ -0,0 +1,40 @@ +package com.mob.utsmyanmar.ui.password_input + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class PasswordInputViewModel : ViewModel() { + + private val _uiState = MutableStateFlow(PasswordInputUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun onKey(digit: String) { + _uiState.update { state -> + if (state.input.length < PASSWORD_LENGTH) + state.copy(input = state.input + digit, errorMessage = null) + else + state + } + } + + fun onDelete() { + _uiState.update { state -> + state.copy(input = state.input.dropLast(1), errorMessage = null) + } + } + + fun validate(expectedPassword: String, onCorrect: () -> Unit) { + if (_uiState.value.input == expectedPassword) { + onCorrect() + } else { + _uiState.update { it.copy(errorMessage = "Incorrect password", input = "") } + } + } + + fun reset() { + _uiState.value = PasswordInputUiState() + } +} From 36b9d83a404e5938418276f1cb6fe96c29943360 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:12:52 +0630 Subject: [PATCH 5/7] pager in dashboard --- .../main/java/com/mob/utsmyanmar/AGENTS.md | 2 + .../ui/dashboard/DashboardScreen2.kt | 158 +++++++++++------- .../utsmyanmar/ui/navigation/AppNavGraph.kt | 29 +--- 3 files changed, 101 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/mob/utsmyanmar/AGENTS.md b/app/src/main/java/com/mob/utsmyanmar/AGENTS.md index 898970e..537e01e 100644 --- a/app/src/main/java/com/mob/utsmyanmar/AGENTS.md +++ b/app/src/main/java/com/mob/utsmyanmar/AGENTS.md @@ -1,6 +1,8 @@ ## Working agreements - use Scaffold and AppBar() in every main Screen +- use MVVM design pattern +- separate UiState and ViewModel for screen for better performance and smooth ### Re-usable Screens - PasswordInput.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt index e071274..5ae5da1 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt @@ -22,11 +22,18 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBalanceWallet import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.CreditCard +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Replay +import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material.icons.filled.Sync +import androidx.compose.material.icons.filled.Undo import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -81,10 +88,10 @@ import kotlinx.coroutines.launch fun DashboardScreen2( onNavigateAmount: (String) -> Unit = {}, onNavigateSignOn: () -> Unit = {}, - onNavigateSeeMore: () -> Unit = {}, onNavigateSettlement: () -> Unit = {}, onNavigateVersion: () -> Unit = {}, onNavigateFunctions: () -> Unit = {}, + onNavigateAction: (String) -> Unit = {}, deviceInfoViewModel: DeviceInfoViewModel = viewModel() ) { val deviceInfo by deviceInfoViewModel.uiState.collectAsState() @@ -381,11 +388,14 @@ fun DashboardScreen2( .weight(1.5f) .fillMaxWidth(), ) { - MenuGrid( - onNavigateAmount = onNavigateAmount, - onNavigateSignOn = onNavigateSignOn, - onNavigateSeeMore = onNavigateSeeMore, - onNavigateSettlement = onNavigateSettlement + MenuPager( + items = buildMenuItems( + onNavigateAmount = onNavigateAmount, + onNavigateSignOn = onNavigateSignOn, + onNavigateSettlement = onNavigateSettlement, + onNavigateAction = onNavigateAction + ), + modifier = Modifier.fillMaxSize() ) } } @@ -562,75 +572,93 @@ private fun IconCircle( } } +private class DashboardMenuItem( + val title: String, + val iconContent: @Composable () -> Unit, + val onClick: () -> Unit +) + @Composable -private fun MenuGrid( +private fun buildMenuItems( onNavigateAmount: (String) -> Unit, onNavigateSignOn: () -> Unit, - onNavigateSeeMore: () -> Unit, - onNavigateSettlement: () -> Unit + onNavigateSettlement: () -> Unit, + onNavigateAction: (String) -> Unit +): List = listOf( + DashboardMenuItem("Sale", { Icon(painterResource(R.drawable.ic_terminal), contentDescription = null, modifier = Modifier.size(40.dp), tint = Color.LegacyRed) }) { onNavigateAmount("Sale") }, + DashboardMenuItem("MMQR", { Image(painter = painterResource(R.drawable.ic_mmqr_logo), contentDescription = null, modifier = Modifier.height(48.dp)) }) { }, + DashboardMenuItem("History", { Icon(painterResource(R.drawable.ic_history), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { }, + DashboardMenuItem("Sign On", { Icon(painterResource(R.drawable.ic_sign_on), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSignOn() }, + DashboardMenuItem("Settlement", { Icon(painterResource(R.drawable.ic_settlement), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSettlement() }, + DashboardMenuItem("Void", { Icon(Icons.Default.Undo, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Void") }, + DashboardMenuItem("Refund", { Icon(Icons.Default.Replay, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Refund") }, + DashboardMenuItem("Pre-Auth", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth") }, + DashboardMenuItem("Pre-Auth Void", { Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Void") }, + DashboardMenuItem("Pre-Auth Complete", { Icon(Icons.Default.CreditCard, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete") }, + DashboardMenuItem("Pre-Auth Complete Void", { Icon(Icons.Default.SwapHoriz, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Complete Void") }, + DashboardMenuItem("Cash Out", { Icon(Icons.Default.AccountBalanceWallet, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Cash Out") }, +) + +@Composable +private fun MenuPager( + items: List, + modifier: Modifier = Modifier ) { + val pages = items.chunked(6) + val pagerState = rememberPagerState(pageCount = { pages.size }) + + Column(modifier = modifier) { + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(1f) + ) { pageIndex -> + MenuPage(items = pages[pageIndex]) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(pages.size) { index -> + val selected = pagerState.currentPage == index + Box( + modifier = Modifier + .padding(horizontal = 4.dp) + .size(if (selected) 8.dp else 6.dp) + .background( + color = if (selected) Color.LegacyRed else Color.Gray.copy(alpha = 0.4f), + shape = CircleShape + ) + ) + } + } + } +} + +@Composable +private fun MenuPage(items: List) { Column( verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(horizontal = 16.dp) ) { Spacer(Modifier.height(8.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - MenuCard(title = "Sale", icon = { - Icon( - painterResource(R.drawable.ic_terminal), - contentDescription = "icon", - modifier = Modifier.size(40.dp), - tint = Color.LegacyRed - ) - }, modifier = Modifier.weight(1f), onClick = { onNavigateAmount("Sale") }) - MenuCard(title = "MMQR", icon = { - Image( - painter = painterResource(R.drawable.ic_mmqr_logo), - contentDescription = "mmqr image", - modifier = Modifier.height(48.dp) - ) - }, modifier = Modifier.weight(1f)) - MenuCard("History", icon = { - Icon( - painterResource(R.drawable.ic_history), - contentDescription = "icon", - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }, modifier = Modifier.weight(1f)) - } - - Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { - MenuCard( - title = "Sign On", icon = { - Icon( - painterResource(R.drawable.ic_sign_on), - contentDescription = "icon", - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed + items.chunked(3).forEach { rowItems -> + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + rowItems.forEach { item -> + MenuCard( + title = item.title, + icon = item.iconContent, + modifier = Modifier.weight(1f), + onClick = item.onClick ) - }, modifier = Modifier.weight(1f), onClick = onNavigateSignOn - ) - MenuCard( - title = "Settlement", icon = { - Icon( - painterResource(R.drawable.ic_settlement), - contentDescription = "icon", - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }, modifier = Modifier.weight(1f), onClick = onNavigateSettlement - ) - MenuCard( - title = "See More", icon = { - Icon( - painterResource(R.drawable.ic_see_more), - contentDescription = "icon", - modifier = Modifier.size(32.dp), - tint = Color.LegacyRed - ) - }, modifier = Modifier.weight(1f), onClick = onNavigateSeeMore - ) + } + repeat(3 - rowItems.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } } } } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt index a88953d..8e0a96b 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt @@ -16,7 +16,6 @@ import com.mob.utsmyanmar.model.ProcessCode import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2 -import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel import com.mob.utsmyanmar.ui.functions.FunctionsScreen import com.mob.utsmyanmar.ui.input_amount.AmountRoute @@ -92,11 +91,6 @@ fun AppNavGraph( launchSingleTop = true } }, - onNavigateSeeMore = { - navController.navigate(Routes.SeeMore.route) { - launchSingleTop = true - } - }, onNavigateSettlement = { navController.navigate(Routes.Settlement.route) { launchSingleTop = true @@ -109,27 +103,16 @@ fun AppNavGraph( navController.navigate(Routes.Password.createRoute(Routes.Functions.route, PasswordType.SETTING)) { launchSingleTop = true } - } - ) - } - - composable(Routes.SeeMore.route) { - SeeMoreScreen( - onBack = { navController.popBackStack() }, - onNavigateAmount = { action -> - if (action == "Void") { - navController.navigate(Routes.VoidTrace.route) { - launchSingleTop = true - } - } else { - navController.navigate(Routes.Amount.createRoute(action)) { - launchSingleTop = true - } + }, + onNavigateAction = { action -> + when (action) { + "Void" -> navController.navigate(Routes.VoidTrace.route) { launchSingleTop = true } + else -> navController.navigate(Routes.Amount.createRoute(action)) { launchSingleTop = true } } } ) } - + composable(Routes.Version.route){ val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel(); VersionScreen( From c32c93338ef0ef3ffe76eb747535f259ebe484bb Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:52:27 +0630 Subject: [PATCH 6/7] dashboard Ui changed --- .../java/com/mob/utsmyanmar/MainActivity.kt | 10 +- .../ui/dashboard/DashboardScreen2.kt | 162 ++++++++++++++---- .../ui/dashboard/DashboardUiState.kt | 15 ++ .../ui/dashboard/DashboardViewModel.kt | 71 ++++++++ .../utsmyanmar/ui/navigation/AppNavGraph.kt | 8 +- .../com/mob/utsmyanmar/ui/preview/Preview.kt | 2 +- 6 files changed, 228 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUiState.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardViewModel.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/MainActivity.kt b/app/src/main/java/com/mob/utsmyanmar/MainActivity.kt index bba17b2..c4965c0 100644 --- a/app/src/main/java/com/mob/utsmyanmar/MainActivity.kt +++ b/app/src/main/java/com/mob/utsmyanmar/MainActivity.kt @@ -3,10 +3,11 @@ package com.mob.utsmyanmar import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity -import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.compose.rememberNavController import com.mob.utsmyanmar.ui.navigation.AppNavGraph import com.mob.utsmyanmar.ui.theme.MOBPOSTheme @@ -17,7 +18,10 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { // installSplashScreen() super.onCreate(savedInstanceState) -// enableEdgeToEdge() + enableEdgeToEdge() + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars()) + windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) setContent { MOBPOSTheme { diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt index 5ae5da1..c39f237 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt @@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape @@ -33,7 +35,6 @@ import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material.icons.filled.Sync -import androidx.compose.material.icons.filled.Undo import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -45,8 +46,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalNavigationDrawer -import androidx.compose.material3.NavigationDrawerItem -import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -67,6 +66,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -83,6 +83,7 @@ import com.utsmyanmar.paylibs.sign_on.EchoTestProcess import com.utsmyanmar.paylibs.sign_on.SignOnListener import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds @Composable fun DashboardScreen2( @@ -92,6 +93,7 @@ fun DashboardScreen2( onNavigateVersion: () -> Unit = {}, onNavigateFunctions: () -> Unit = {}, onNavigateAction: (String) -> Unit = {}, + dashboardUiState: DashboardUiState = DashboardUiState(), deviceInfoViewModel: DeviceInfoViewModel = viewModel() ) { val deviceInfo by deviceInfoViewModel.uiState.collectAsState() @@ -382,10 +384,10 @@ fun DashboardScreen2( ) { SummaryCard() } - //bottom section + //pager section Box( modifier = Modifier - .weight(1.5f) + .weight(1.3f) .fillMaxWidth(), ) { MenuPager( @@ -398,37 +400,18 @@ fun DashboardScreen2( modifier = Modifier.fillMaxSize() ) } + //transactions section + RecentTransactions( + transactions = dashboardUiState.recentTransactions, + modifier = Modifier + .weight(1.2f) + .fillMaxWidth() + ) } } } } -@Composable -private fun DrawerItem( - title: String, icon: ImageVector, onClick: () -> Unit -) { - NavigationDrawerItem( - label = { - Text( - text = title, fontWeight = FontWeight.Medium - ) - }, - selected = false, - onClick = onClick, - icon = { - Icon( - imageVector = icon, contentDescription = title - ) - }, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp), - colors = NavigationDrawerItemDefaults.colors( - unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent, - unselectedIconColor = Color.LegacyRed, - unselectedTextColor = Color.Black - ) - ) -} - @Composable private fun AdvertisingArea() { @@ -441,7 +424,7 @@ private fun AdvertisingArea() { LaunchedEffect(pageState) { while (true) { - delay(10000) + delay(10000.milliseconds) val nextPage = (pageState.currentPage + 1) % imageArray.size pageState.animateScrollToPage( @@ -590,7 +573,7 @@ private fun buildMenuItems( DashboardMenuItem("History", { Icon(painterResource(R.drawable.ic_history), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { }, DashboardMenuItem("Sign On", { Icon(painterResource(R.drawable.ic_sign_on), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSignOn() }, DashboardMenuItem("Settlement", { Icon(painterResource(R.drawable.ic_settlement), contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateSettlement() }, - DashboardMenuItem("Void", { Icon(Icons.Default.Undo, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Void") }, + DashboardMenuItem("Void", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Void") }, DashboardMenuItem("Refund", { Icon(Icons.Default.Replay, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Refund") }, DashboardMenuItem("Pre-Auth", { Icon(Icons.Default.Lock, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth") }, DashboardMenuItem("Pre-Auth Void", { Icon(Icons.Default.LockOpen, contentDescription = null, modifier = Modifier.size(32.dp), tint = Color.LegacyRed) }) { onNavigateAction("Pre-Auth Void") }, @@ -717,16 +700,125 @@ private fun MenuCard( } } +@Composable +private fun RecentTransactions( + transactions: List, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Recent Transactions", + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp + ) + } + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = Color.Gray.copy(alpha = 0.2f) + ) + LazyColumn(modifier = Modifier.fillMaxSize()) { + if (transactions.isEmpty()) { + item { + Text( + text = "No recent transactions", + color = Color.Gray, + fontSize = 13.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp) + ) + } + } else { + items(items = transactions, key = { it.pid }) { record -> + TrnxRow(record = record) + } + } + } + } +} + +@Composable +private fun TrnxRow(record: TrnxRecord) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 4.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color.White) + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(36.dp) + .background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = record.typeLabel.take(1), + color = Color.LegacyRed, + fontWeight = FontWeight.Bold, + fontSize = 15.sp + ) + } + + Spacer(Modifier.width(10.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (record.isVoided) "${record.typeLabel} (Voided)" else record.typeLabel, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = if (record.isVoided) Color.Gray else Color.Black + ) + Text( + text = record.maskedCard, + fontSize = 11.sp, + color = Color.Gray + ) + } + + Column(horizontalAlignment = Alignment.End) { + Text( + text = record.amountDisplay, + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + color = if (record.isVoided) Color.Gray else Color.LegacyRed + ) + Text( + text = record.dateTime, + fontSize = 10.sp, + color = Color.Gray + ) + } + } +} + +private val previewTransactions = listOf( + TrnxRecord(1L, "Sale", "MMK 10,000", "**** 1234", "06/10 14:32", isVoided = false, isApproved = true), + TrnxRecord(2L, "Void", "MMK 5,500", "**** 5678", "06/10 13:10", isVoided = true, isApproved = false), + TrnxRecord(3L, "Refund", "MMK 2,000", "**** 9012", "06/09 09:45", isVoided = false, isApproved = true), + TrnxRecord(4L, "Sale", "MMK 30,000", "**** 3456", "06/09 08:00", isVoided = false, isApproved = true), + TrnxRecord(5L, "Settlement", "MMK 0", "----", "06/08 18:00", isVoided = false, isApproved = true), +) + @P2Preview @Composable fun PreviewDashboardScreen2() { - DashboardScreen2() + DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions)) } @P3Preview @Composable fun PreviewDashboardScreen3() { - DashboardScreen2() + DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions)) } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUiState.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUiState.kt new file mode 100644 index 0000000..41134f1 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardUiState.kt @@ -0,0 +1,15 @@ +package com.mob.utsmyanmar.ui.dashboard + +data class TrnxRecord( + val pid: Long, + val typeLabel: String, + val amountDisplay: String, + val maskedCard: String, + val dateTime: String, + val isVoided: Boolean, + val isApproved: Boolean +) + +data class DashboardUiState( + val recentTransactions: List = emptyList() +) diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..05b0d45 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardViewModel.kt @@ -0,0 +1,71 @@ +package com.mob.utsmyanmar.ui.dashboard + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope +import com.utsmyanmar.baselib.repo.Repository +import com.utsmyanmar.paylibs.model.PayDetail +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DashboardViewModel @Inject constructor( + private val repository: Repository +) : ViewModel() { + + private val _uiState = MutableStateFlow(DashboardUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + repository.getAllTrans().asFlow().collect { payDetails -> + _uiState.update { + it.copy( + recentTransactions = payDetails.take(10).map { pd -> pd.toRecord() } + ) + } + } + } + } +} + +private fun PayDetail.toRecord(): TrnxRecord { + val type = transType?.takeIf { it.isNotBlank() }?.let { formatTypeLabel(it) } ?: "Transaction" + val amount = "MMK %,d".format(amount) + val card = CardNo?.takeIf { it.length >= 4 }?.let { "**** ${it.takeLast(4)}" } ?: "----" + val dt = buildDateTime(TradeDate, TradeTime) + val approved = tradeAnswerCode == "00" || approvalCode?.isNotBlank() == true + + return TrnxRecord( + pid = PID ?: 0L, + typeLabel = type, + amountDisplay = amount, + maskedCard = card, + dateTime = dt, + isVoided = isCanceled, + isApproved = approved + ) +} + +private fun formatTypeLabel(raw: String): String = when { + raw.contains("SALE", ignoreCase = true) -> "Sale" + raw.contains("VOID", ignoreCase = true) -> "Void" + raw.contains("REFUND", ignoreCase = true) -> "Refund" + raw.contains("SETTLEMENT", ignoreCase = true) -> "Settlement" + raw.contains("PRE", ignoreCase = true) -> "Pre-Auth" + raw.contains("CASH", ignoreCase = true) -> "Cash Out" + raw.contains("WAVE", ignoreCase = true) -> "Wave Pay" + else -> raw.lowercase().replaceFirstChar { it.uppercaseChar() } +} + +// TradeDate = "MMDD", TradeTime = "HHmmss" (ISO 8583 fields 13 & 12) +private fun buildDateTime(date: String?, time: String?): String { + val d = date?.padStart(4, '0')?.takeIf { it.length >= 4 } ?: return "--/-- --:--" + val t = (time ?: "").padStart(6, '0') + return "${d.take(2)}/${d.drop(2).take(2)} ${t.take(2)}:${t.drop(2).take(2)}" +} diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt index 8e0a96b..553c204 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt @@ -4,8 +4,10 @@ import android.annotation.SuppressLint import android.net.Uri import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.NavType @@ -16,6 +18,7 @@ import com.mob.utsmyanmar.model.ProcessCode import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingScreen import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2 +import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel import com.mob.utsmyanmar.ui.functions.FunctionsScreen import com.mob.utsmyanmar.ui.input_amount.AmountRoute @@ -74,8 +77,11 @@ fun AppNavGraph( } composable(Routes.Dashboard.route) { - val sharedViewModel: SharedViewModel = hiltViewModel(activity); + val sharedViewModel: SharedViewModel = hiltViewModel(activity) + val dashboardViewModel: DashboardViewModel = hiltViewModel() + val dashboardUiState by dashboardViewModel.uiState.collectAsStateWithLifecycle() DashboardScreen2( + dashboardUiState = dashboardUiState, onNavigateAmount = { action -> if(action == "Sale"){ sharedViewModel.transactionsType.value = TransactionsType.SALE; diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt b/app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt index a450eaa..a568746 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/preview/Preview.kt @@ -13,7 +13,7 @@ annotation class P2Preview @Preview( name = "P3", - device = "spec:width=720px,height=1600px,dpi=270", + device = "spec:width=427dp,height=949dp,dpi=270", showBackground = true, showSystemUi = true ) From b69dfeb292f3d701fc910f0ea14caa052d5de39b Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:03:21 +0630 Subject: [PATCH 7/7] notification --- .../utsmyanmar/ui/components/appbar/AppBar.kt | 4 + .../ui/dashboard/DashboardScreen2.kt | 25 ++- .../utsmyanmar/ui/navigation/AppNavGraph.kt | 34 ++++ .../mob/utsmyanmar/ui/navigation/Routes.kt | 4 + .../notification/NotificationDetailScreen.kt | 116 ++++++++++++ .../ui/notification/NotificationListScreen.kt | 174 ++++++++++++++++++ .../ui/notification/NotificationModel.kt | 9 + .../ui/notification/NotificationViewModel.kt | 37 ++++ 8 files changed, 402 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationDetailScreen.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationListScreen.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationModel.kt create mode 100644 app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationViewModel.kt diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/components/appbar/AppBar.kt b/app/src/main/java/com/mob/utsmyanmar/ui/components/appbar/AppBar.kt index af064ef..f2b4f1d 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/components/appbar/AppBar.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/components/appbar/AppBar.kt @@ -1,5 +1,6 @@ package com.mob.utsmyanmar.ui.components.appbar +import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -20,6 +21,7 @@ fun AppBar( title: String, icon: ImageVector? = null, onIconClick: (() -> Unit)? = null, + actions: @Composable RowScope.() -> Unit = {} ) { CenterAlignedTopAppBar( title = { @@ -44,6 +46,8 @@ fun AppBar( } }, + actions = actions, + colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.LegacyRed ) diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt index c39f237..6ae8f3f 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/dashboard/DashboardScreen2.kt @@ -11,6 +11,7 @@ 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.offset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -32,11 +33,13 @@ import androidx.compose.material.icons.filled.CreditCard import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material.icons.filled.Sync import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.IconButton import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -93,6 +96,7 @@ fun DashboardScreen2( onNavigateVersion: () -> Unit = {}, onNavigateFunctions: () -> Unit = {}, onNavigateAction: (String) -> Unit = {}, + onNavigateNotifications: () -> Unit = {}, dashboardUiState: DashboardUiState = DashboardUiState(), deviceInfoViewModel: DeviceInfoViewModel = viewModel() ) { @@ -360,7 +364,26 @@ fun DashboardScreen2( AppBar( title = "Dashboard", icon = Icons.Default.Menu, - onIconClick = { scope.launch { drawerState.open() } }) + onIconClick = { scope.launch { drawerState.open() } }, + actions = { + IconButton(onClick = onNavigateNotifications) { + Box { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = "Notifications", + tint = Color.White + ) + Box( + modifier = Modifier + .size(8.dp) + .background(Color.GoldenGlow, CircleShape) + .align(Alignment.TopEnd) + .offset(x = 2.dp, y = (-2).dp) + ) + } + } + } + ) }) { paddingValues -> Column( modifier = Modifier diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt index 553c204..0edc797 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/AppNavGraph.kt @@ -43,6 +43,9 @@ import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent import com.mob.utsmyanmar.ui.transaction_result.TransactionResultRoute import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel +import com.mob.utsmyanmar.ui.notification.NotificationDetailScreen +import com.mob.utsmyanmar.ui.notification.NotificationListScreen +import com.mob.utsmyanmar.ui.notification.NotificationViewModel import com.mob.utsmyanmar.ui.version.VersionScreen import com.mob.utsmyanmar.viewmodel.CardReaderViewModel import com.mob.utsmyanmar.viewmodel.EmvTransactionProcessViewModel @@ -115,6 +118,9 @@ fun AppNavGraph( "Void" -> navController.navigate(Routes.VoidTrace.route) { launchSingleTop = true } else -> navController.navigate(Routes.Amount.createRoute(action)) { launchSingleTop = true } } + }, + onNavigateNotifications = { + navController.navigate(Routes.NotificationList.route) { launchSingleTop = true } } ) } @@ -488,6 +494,34 @@ fun AppNavGraph( } ) } + + composable(Routes.NotificationList.route) { + val notificationViewModel: NotificationViewModel = hiltViewModel() + NotificationListScreen( + viewModel = notificationViewModel, + onBack = { navController.popBackStack() }, + onNavigateDetail = { id -> + navController.navigate(Routes.NotificationDetail.createRoute(id)) { + launchSingleTop = true + } + } + ) + } + + composable( + route = Routes.NotificationDetail.route, + arguments = listOf( + navArgument("notificationId") { type = NavType.IntType } + ) + ) { backStackEntry -> + val notificationViewModel: NotificationViewModel = hiltViewModel() + val id = backStackEntry.arguments?.getInt("notificationId") ?: return@composable + NotificationDetailScreen( + viewModel = notificationViewModel, + notificationId = id, + onBack = { navController.popBackStack() } + ) + } } } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt index 9da6e55..253c217 100644 --- a/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt +++ b/app/src/main/java/com/mob/utsmyanmar/ui/navigation/Routes.kt @@ -33,4 +33,8 @@ sealed class Routes(val route: String) { fun createRoute(destination: String, passwordType: String): String = "password/${Uri.encode(destination)}/$passwordType" } + data object NotificationList : Routes("notification_list") + data object NotificationDetail : Routes("notification_detail/{notificationId}") { + fun createRoute(id: Int): String = "notification_detail/$id" + } } diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationDetailScreen.kt b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationDetailScreen.kt new file mode 100644 index 0000000..cdb9634 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationDetailScreen.kt @@ -0,0 +1,116 @@ +package com.mob.utsmyanmar.ui.notification + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mob.utsmyanmar.ui.components.appbar.AppBar +import com.mob.utsmyanmar.ui.theme.Color + +@Composable +fun NotificationDetailScreen( + viewModel: NotificationViewModel, + notificationId: Int, + onBack: () -> Unit +) { + val notification = viewModel.getById(notificationId) + + Scaffold( + containerColor = Color.IvoryBeige, + topBar = { + AppBar( + title = "Notification Detail", + icon = Icons.AutoMirrored.Filled.ArrowBack, + onIconClick = onBack + ) + } + ) { paddingValues -> + if (notification == null) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Text(text = "Notification not found", color = Color.Gray) + } + } else { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + elevation = CardDefaults.cardElevation(4.dp) + ) { + Column(modifier = Modifier.padding(20.dp)) { + Box( + modifier = Modifier + .size(56.dp) + .background(Color.LegacyRed.copy(alpha = 0.1f), CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = null, + tint = Color.LegacyRed, + modifier = Modifier.size(28.dp) + ) + } + + Spacer(Modifier.height(16.dp)) + + Text( + text = notification.title, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.Black + ) + + Spacer(Modifier.height(6.dp)) + + Text( + text = notification.timestamp, + fontSize = 12.sp, + color = Color.Gray + ) + + Spacer(Modifier.height(16.dp)) + + Text( + text = notification.message, + fontSize = 14.sp, + color = Color.Black, + lineHeight = 22.sp + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationListScreen.kt b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationListScreen.kt new file mode 100644 index 0000000..28aa1bc --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationListScreen.kt @@ -0,0 +1,174 @@ +package com.mob.utsmyanmar.ui.notification + +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.mob.utsmyanmar.ui.components.appbar.AppBar +import com.mob.utsmyanmar.ui.theme.Color + +@Composable +fun NotificationListScreen( + viewModel: NotificationViewModel, + onBack: () -> Unit, + onNavigateDetail: (Int) -> Unit +) { + val notifications by viewModel.notifications.collectAsState() + + Scaffold( + containerColor = Color.IvoryBeige, + topBar = { + AppBar( + title = "Notifications", + icon = Icons.AutoMirrored.Filled.ArrowBack, + onIconClick = onBack, + actions = { + if (notifications.any { !it.isRead }) { + TextButton(onClick = { viewModel.markAllAsRead() }) { + Text(text = "Mark all read", color = Color.White, fontSize = 12.sp) + } + } + } + ) + } + ) { paddingValues -> + if (notifications.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = null, + tint = Color.Gray, + modifier = Modifier.size(64.dp) + ) + Text(text = "No notifications", color = Color.Gray, fontSize = 14.sp) + } + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(items = notifications, key = { it.id }) { notification -> + NotificationItem( + notification = notification, + onClick = { + viewModel.markAsRead(notification.id) + onNavigateDetail(notification.id) + } + ) + } + } + } + } +} + +@Composable +private fun NotificationItem( + notification: AppNotification, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + .clickable(onClick = onClick) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(42.dp) + .background( + color = if (notification.isRead) Color.Gray.copy(alpha = 0.12f) + else Color.LegacyRed.copy(alpha = 0.12f), + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = null, + tint = if (notification.isRead) Color.Gray else Color.LegacyRed, + modifier = Modifier.size(22.dp) + ) + } + + Spacer(Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = notification.title, + fontSize = 13.sp, + fontWeight = if (notification.isRead) FontWeight.Normal else FontWeight.SemiBold, + color = if (notification.isRead) Color.Gray else Color.Black, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + if (!notification.isRead) { + Box( + modifier = Modifier + .size(8.dp) + .background(Color.LegacyRed, CircleShape) + ) + } + } + Text( + text = notification.message, + fontSize = 12.sp, + color = Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = notification.timestamp, + fontSize = 11.sp, + color = Color.Gray.copy(alpha = 0.7f) + ) + } + } +} diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationModel.kt b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationModel.kt new file mode 100644 index 0000000..4afee4d --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationModel.kt @@ -0,0 +1,9 @@ +package com.mob.utsmyanmar.ui.notification + +data class AppNotification( + val id: Int, + val title: String, + val message: String, + val timestamp: String, + val isRead: Boolean = false +) diff --git a/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationViewModel.kt b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationViewModel.kt new file mode 100644 index 0000000..458f131 --- /dev/null +++ b/app/src/main/java/com/mob/utsmyanmar/ui/notification/NotificationViewModel.kt @@ -0,0 +1,37 @@ +package com.mob.utsmyanmar.ui.notification + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class NotificationViewModel @Inject constructor() : ViewModel() { + + private val _notifications = MutableStateFlow(sampleNotifications) + val notifications: StateFlow> = _notifications.asStateFlow() + + val unreadCount: Int get() = _notifications.value.count { !it.isRead } + + fun markAsRead(id: Int) { + _notifications.value = _notifications.value.map { + if (it.id == id) it.copy(isRead = true) else it + } + } + + fun markAllAsRead() { + _notifications.value = _notifications.value.map { it.copy(isRead = true) } + } + + fun getById(id: Int): AppNotification? = _notifications.value.find { it.id == id } +} + +private val sampleNotifications = listOf( + AppNotification(1, "Settlement Reminder", "Your daily settlement is pending. Please settle before end of day.", "Today, 17:30", isRead = false), + AppNotification(2, "TMS Update Available", "A new terminal configuration update is ready. Please restart to apply.", "Today, 09:15", isRead = false), + AppNotification(3, "Transaction Approved", "Sale of MMK 50,000 was approved successfully. Trace #001234.", "Yesterday, 14:22", isRead = true), + AppNotification(4, "Network Warning", "Intermittent network issues detected. Contact your administrator if the problem persists.", "Yesterday, 11:05", isRead = true), + AppNotification(5, "Log-On Required", "Your terminal session has expired. Please log on again to continue.", "06/09, 08:00", isRead = true), +)