Merge branch 'tms' into dev

This commit is contained in:
moon 2026-06-11 16:09:17 +06:30
commit fd88dd6dea
25 changed files with 1392 additions and 359 deletions

View File

@ -1,3 +1,9 @@
## Working agreements ## Working agreements
- use Scaffold and AppBar() in every main Screen - 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
- InputAmountScreen.kt

View File

@ -3,10 +3,11 @@ package com.mob.utsmyanmar
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge 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 androidx.navigation.compose.rememberNavController
import com.mob.utsmyanmar.ui.navigation.AppNavGraph import com.mob.utsmyanmar.ui.navigation.AppNavGraph
import com.mob.utsmyanmar.ui.theme.MOBPOSTheme import com.mob.utsmyanmar.ui.theme.MOBPOSTheme
@ -17,7 +18,10 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// installSplashScreen() // installSplashScreen()
super.onCreate(savedInstanceState) 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) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setContent { setContent {
MOBPOSTheme { MOBPOSTheme {

View File

@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Backspace import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
@ -26,9 +25,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.mob.utsmyanmar.ui.theme.Color import com.mob.utsmyanmar.ui.theme.Color
@ -181,62 +178,3 @@ fun NumericEntryScreen(
} }
} }
} }
@Composable
private fun NumericKeypad(
keys: List<List<String>>,
onKeyClick: (String) -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
keys.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
row.forEach { key ->
KeypadButton(
text = key,
modifier = Modifier.weight(1f),
onClick = { onKeyClick(key) }
)
}
}
}
}
}
@Composable
private fun KeypadButton(
text: String,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
val enabled = text.isNotBlank()
Box(
modifier = modifier
.height(66.dp)
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(8.dp),
clip = false
)
.background(
color = Color.White,
shape = RoundedCornerShape(8.dp)
)
.clickable(enabled = enabled) { onClick() },
contentAlignment = Alignment.Center
) {
Text(
text = text,
color = if (enabled) Color.LegacyRed else Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center
)
}
}

View File

@ -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<List<String>>,
) {
// val keys : List<List<String>> = 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<List<String>> = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf(".", "0", "00")
)
NumericKeypad(
keys = keys,
onKeyClick = {}
)
}

View File

@ -1,5 +1,6 @@
package com.mob.utsmyanmar.ui.components.appbar package com.mob.utsmyanmar.ui.components.appbar
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -20,6 +21,7 @@ fun AppBar(
title: String, title: String,
icon: ImageVector? = null, icon: ImageVector? = null,
onIconClick: (() -> Unit)? = null, onIconClick: (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {}
) { ) {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
@ -44,6 +46,8 @@ fun AppBar(
} }
}, },
actions = actions,
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.LegacyRed containerColor = Color.LegacyRed
) )

View File

@ -6,7 +6,20 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.offset
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@ -15,6 +28,42 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.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
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.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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -23,7 +72,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -39,15 +90,18 @@ import com.utsmyanmar.paylibs.sign_on.SignOnListener
import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
@Composable @Composable
fun DashboardScreen2( fun DashboardScreen2(
onNavigateAmount: (String) -> Unit = {}, onNavigateAmount: (String) -> Unit = {},
onNavigateSignOn: () -> Unit = {}, onNavigateSignOn: () -> Unit = {},
onNavigateSeeMore: () -> Unit = {},
onNavigateSettlement: () -> Unit = {}, onNavigateSettlement: () -> Unit = {},
onNavigateVersion: () -> Unit = {}, onNavigateVersion: () -> Unit = {},
onNavigateFunctions: () -> Unit = {}, onNavigateFunctions: () -> Unit = {},
onNavigateAction: (String) -> Unit = {},
onNavigateNotifications: () -> Unit = {},
dashboardUiState: DashboardUiState = DashboardUiState(),
deviceInfoViewModel: DeviceInfoViewModel = viewModel() deviceInfoViewModel: DeviceInfoViewModel = viewModel()
) { ) {
val deviceInfo by deviceInfoViewModel.uiState.collectAsState() val deviceInfo by deviceInfoViewModel.uiState.collectAsState()
@ -217,19 +271,58 @@ fun DashboardScreen2(
} }
Spacer(modifier = Modifier.height(10.dp)) 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(
text = "System Management", text = "System Management",
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
@ -257,6 +350,38 @@ fun DashboardScreen2(
scope.launch { drawerState.close() } scope.launch { drawerState.close() }
onNavigateVersion() 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( Scaffold(
@ -264,7 +389,26 @@ fun DashboardScreen2(
AppBar( AppBar(
title = "Dashboard", title = "Dashboard",
icon = Icons.Default.Menu, 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 -> }) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
@ -288,73 +432,34 @@ fun DashboardScreen2(
) { ) {
SummaryCard() SummaryCard()
} }
//bottom section //pager section
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1.5f) .weight(1.3f)
.fillMaxWidth(), .fillMaxWidth(),
) { ) {
MenuGrid( MenuPager(
onNavigateAmount = onNavigateAmount, items = buildMenuItems(
onNavigateSignOn = onNavigateSignOn, onNavigateAmount = onNavigateAmount,
onNavigateSeeMore = onNavigateSeeMore, onNavigateSignOn = onNavigateSignOn,
onNavigateSettlement = onNavigateSettlement onNavigateSettlement = onNavigateSettlement,
onNavigateAction = onNavigateAction
),
modifier = Modifier.fillMaxSize()
) )
} }
//transactions section
RecentTransactions(
transactions = dashboardUiState.recentTransactions,
modifier = Modifier
.weight(1.2f)
.fillMaxWidth()
)
} }
} }
} }
} }
@Composable
private fun DrawerItem(
title: String,
icon: ImageVector,
showSwitch: Boolean = false, // New: Flag to enable switch mode
isChecked: Boolean = false, // New: Switch state
onCheckedChange: (Boolean) -> Unit = {}, // New: Switch callback
onClick: () -> Unit
) {
NavigationDrawerItem(
label = {
Text(
text = title,
fontWeight = FontWeight.Medium
)
},
selected = false,
// If it's a switch item, clicking the whole row toggles the switch instead of navigating
onClick = {
if (showSwitch) {
onCheckedChange(!isChecked)
} else {
onClick()
}
},
icon = {
Icon(
imageVector = icon,
contentDescription = title
)
},
badge = {
if (showSwitch) {
Switch(
checked = isChecked,
onCheckedChange = onCheckedChange
)
}
},
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 @Composable
private fun AdvertisingArea() { private fun AdvertisingArea() {
@ -367,7 +472,7 @@ private fun AdvertisingArea() {
LaunchedEffect(pageState) { LaunchedEffect(pageState) {
while (true) { while (true) {
delay(10000) delay(10000.milliseconds)
val nextPage = (pageState.currentPage + 1) % imageArray.size val nextPage = (pageState.currentPage + 1) % imageArray.size
pageState.animateScrollToPage( pageState.animateScrollToPage(
@ -498,75 +603,93 @@ private fun IconCircle(
} }
} }
private class DashboardMenuItem(
val title: String,
val iconContent: @Composable () -> Unit,
val onClick: () -> Unit
)
@Composable @Composable
private fun MenuGrid( private fun buildMenuItems(
onNavigateAmount: (String) -> Unit, onNavigateAmount: (String) -> Unit,
onNavigateSignOn: () -> Unit, onNavigateSignOn: () -> Unit,
onNavigateSeeMore: () -> Unit, onNavigateSettlement: () -> Unit,
onNavigateSettlement: () -> Unit onNavigateAction: (String) -> Unit
): List<DashboardMenuItem> = 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.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") },
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<DashboardMenuItem>,
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<DashboardMenuItem>) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) { ) {
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { items.chunked(3).forEach { rowItems ->
MenuCard(title = "Sale", icon = { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Icon( rowItems.forEach { item ->
painterResource(R.drawable.ic_terminal), MenuCard(
contentDescription = "icon", title = item.title,
modifier = Modifier.size(40.dp), icon = item.iconContent,
tint = Color.LegacyRed modifier = Modifier.weight(1f),
) onClick = item.onClick
}, 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
) )
}, modifier = Modifier.weight(1f), onClick = onNavigateSignOn }
) repeat(3 - rowItems.size) {
MenuCard( Spacer(modifier = Modifier.weight(1f))
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
)
} }
} }
} }
@ -625,14 +748,191 @@ private fun MenuCard(
} }
} }
@Composable
private fun RecentTransactions(
transactions: List<TrnxRecord>,
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 @P2Preview
@Composable @Composable
fun PreviewDashboardScreen2() { fun PreviewDashboardScreen2() {
DashboardScreen2() DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions))
} }
@P3Preview @P3Preview
@Composable @Composable
fun PreviewDashboardScreen3() { fun PreviewDashboardScreen3() {
DashboardScreen2() DashboardScreen2(dashboardUiState = DashboardUiState(previewTransactions))
} }
@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()
}
}
}

View File

@ -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<TrnxRecord> = emptyList()
)

View File

@ -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<DashboardUiState> = _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)}"
}

View File

@ -1,13 +1,11 @@
package com.mob.utsmyanmar.ui.functions package com.mob.utsmyanmar.ui.functions
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.OnDeviceTraining
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -30,15 +27,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.R
import com.mob.utsmyanmar.ui.components.appbar.AppBar import com.mob.utsmyanmar.ui.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color import com.mob.utsmyanmar.ui.theme.Color
import com.mob.utsmyanmar.R import com.utsmyanmar.paylibs.utils.core_utils.SystemParamsOperation
@Composable @Composable
fun FunctionsScreen( fun FunctionsScreen(
onBack: () -> Unit = {} onBack: () -> Unit = {}
) { ) {
val tmsAddress = SystemParamsOperation.getInstance().tmsAddress
Scaffold( Scaffold(
containerColor = Color.IvoryBeige, containerColor = Color.IvoryBeige,
topBar = { topBar = {
@ -139,7 +140,7 @@ fun FunctionsScreen(
FunctionButton( FunctionButton(
onClick = {}, onClick = {},
title = "TMS Server Url", title = "TMS Server Url",
subTitle = "Detail for bound hosts", subTitle = tmsAddress,
leadingIcon = { leadingIcon = {
Icon( Icon(
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
@ -232,4 +233,4 @@ fun PreviewFunctionButton() {
title = "title", title = "title",
subTitle = "sub-title" subTitle = "sub-title"
) )
} }

View File

@ -1,20 +1,17 @@
package com.mob.utsmyanmar.ui.input_amount package com.mob.utsmyanmar.ui.input_amount
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Backspace
import androidx.compose.material.icons.rounded.Backspace import androidx.compose.material.icons.rounded.Backspace
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@ -30,16 +27,21 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.font.FontWeight 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.dp
import androidx.compose.ui.unit.sp 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.components.appbar.AppBar
import com.mob.utsmyanmar.ui.preview.P2Preview import com.mob.utsmyanmar.ui.preview.P2Preview
import com.mob.utsmyanmar.ui.preview.P3Preview import com.mob.utsmyanmar.ui.preview.P3Preview
import com.mob.utsmyanmar.ui.theme.Color 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 @Composable
fun InputAmount( fun InputAmount(
@ -140,6 +142,7 @@ fun InputAmount(
verticalArrangement = Arrangement.Bottom verticalArrangement = Arrangement.Bottom
){ ){
NumericKeypad( NumericKeypad(
keys = amountKeys,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
onKeyClick = { value -> onKeyClick = { value ->
amount = appendAmountValue(amount, value) amount = appendAmountValue(amount, value)
@ -202,69 +205,6 @@ fun InputAmount(
} }
} }
@Composable
private fun NumericKeypad(
modifier: Modifier = Modifier,
onKeyClick: (String) -> Unit
) {
val keys : List<List<String>> = 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 { private fun appendAmountValue(current: String, value: String): String {
if (value == ".") { if (value == ".") {

View File

@ -1,49 +1,56 @@
package com.mob.utsmyanmar.ui.navigation package com.mob.utsmyanmar.ui.navigation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.Uri
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.mob.utsmyanmar.model.ProcessCode 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.CardWaitingScreen
import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel import com.mob.utsmyanmar.ui.cardwaiting.CardWaitingViewModel
import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2 import com.mob.utsmyanmar.ui.dashboard.DashboardScreen2
import com.mob.utsmyanmar.ui.dashboard.SeeMoreScreen import com.mob.utsmyanmar.ui.dashboard.DashboardViewModel
import com.mob.utsmyanmar.ui.device_info.DeviceInfoViewModel 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.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.ProcessingCardRoute
import com.mob.utsmyanmar.ui.processing_card.ProcessingCardViewModel 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.refund_rrn.InputRrnRoute
import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen import com.mob.utsmyanmar.ui.sale_void.TranDetailPage
import com.mob.utsmyanmar.ui.sign_on.SignOnRoute 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.sending_to_host.ProcessingRoute
import com.mob.utsmyanmar.ui.settlement.SettlementScreen 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.settlement.SettlementViewModel
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultEvent import com.mob.utsmyanmar.ui.sign_on.SignOnResultScreen
import com.mob.utsmyanmar.ui.transaction_result.TransactionResultViewModel import com.mob.utsmyanmar.ui.sign_on.SignOnRoute
import com.mob.utsmyanmar.ui.functions.FunctionsScreen
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute import com.mob.utsmyanmar.ui.tms_setup.TmsSetupRoute
import com.mob.utsmyanmar.ui.tms_setup.TmsSetupViewModel 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.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.SharedViewModel
import com.mob.utsmyanmar.viewmodel.TransProcessViewModel import com.mob.utsmyanmar.viewmodel.TransProcessViewModel
import com.utsmyanmar.ecr.data.TransType
import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType import com.utsmyanmar.paylibs.utils.iso_utils.TransactionsType
@SuppressLint("ContextCastToActivity") @SuppressLint("ContextCastToActivity")
@ -73,8 +80,11 @@ fun AppNavGraph(
} }
composable(Routes.Dashboard.route) { 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( DashboardScreen2(
dashboardUiState = dashboardUiState,
onNavigateAmount = { action -> onNavigateAmount = { action ->
if(action == "Sale"){ if(action == "Sale"){
sharedViewModel.transactionsType.value = TransactionsType.SALE; sharedViewModel.transactionsType.value = TransactionsType.SALE;
@ -90,11 +100,6 @@ fun AppNavGraph(
launchSingleTop = true launchSingleTop = true
} }
}, },
onNavigateSeeMore = {
navController.navigate(Routes.SeeMore.route) {
launchSingleTop = true
}
},
onNavigateSettlement = { onNavigateSettlement = {
navController.navigate(Routes.Settlement.route) { navController.navigate(Routes.Settlement.route) {
launchSingleTop = true launchSingleTop = true
@ -104,30 +109,22 @@ fun AppNavGraph(
navController.navigate(Routes.Version.route) navController.navigate(Routes.Version.route)
}, },
onNavigateFunctions = { onNavigateFunctions = {
navController.navigate(Routes.Functions.route) { navController.navigate(Routes.Password.createRoute(Routes.Functions.route, PasswordType.SETTING)) {
launchSingleTop = true launchSingleTop = true
} }
},
onNavigateAction = { action ->
when (action) {
"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 }
} }
) )
} }
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
}
}
}
)
}
composable(Routes.Version.route){ composable(Routes.Version.route){
val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel(); val deviceInfoViewModel: DeviceInfoViewModel = hiltViewModel();
VersionScreen( VersionScreen(
@ -136,6 +133,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) { composable(Routes.Functions.route) {
FunctionsScreen( FunctionsScreen(
onBack = { navController.popBackStack() } onBack = { navController.popBackStack() }
@ -484,6 +502,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() }
)
}
} }
} }

View File

@ -29,4 +29,12 @@ sealed class Routes(val route: String) {
data object PrintReceipt : Routes("print_receipt") data object PrintReceipt : Routes("print_receipt")
data object Version : Routes("version") data object Version : Routes("version")
data object Functions : Routes("functions") data object Functions : Routes("functions")
data object Password : Routes("password/{destination}/{passwordType}") {
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"
}
} }

View File

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

View File

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

View File

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

View File

@ -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<List<AppNotification>> = _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),
)

View File

@ -0,0 +1,210 @@
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.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.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.LaunchedEffect
import androidx.compose.runtime.getValue
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
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 val passwordKeys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "")
)
@Composable
fun InputPassword(
passwordType: String,
onBack: () -> Unit = {},
onPasswordCorrect: () -> Unit = {},
viewModel: PasswordInputViewModel = viewModel()
) {
val expectedPassword = passwords[passwordType] ?: passwords[PasswordType.SETTING]!!
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { viewModel.reset() }
Scaffold(
topBar = {
AppBar(title = "Password", icon = Icons.Default.ArrowBackIosNew, onIconClick = onBack)
},
containerColor = Color.IvoryBeige
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(16.dp)
) {
Spacer(Modifier.height(8.dp))
// Display area
Box(
modifier = Modifier
.fillMaxSize()
.weight(2f)
) {
Card(
modifier = Modifier
.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)
) {
Icon(
imageVector = Icons.Rounded.Backspace,
contentDescription = "Delete",
tint = if (uiState.canDelete) Color.LegacyRed else Color.Gray,
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp)
)
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Enter Password", color = Color.Gray, fontSize = 18.sp)
Spacer(Modifier.height(16.dp))
PasswordDots(filledCount = uiState.input.length, totalCount = PASSWORD_LENGTH)
Spacer(Modifier.height(8.dp))
Text(
text = uiState.errorMessage ?: "",
color = Color.LegacyRed,
fontSize = 14.sp
)
}
}
// Keypad — stable lambda: viewModel::onKey never changes between recompositions
Column(
modifier = Modifier
.fillMaxWidth()
.weight(3f),
verticalArrangement = Arrangement.Bottom
) {
NumericKeypad(
keys = passwordKeys,
modifier = Modifier.fillMaxSize(),
onKeyClick = viewModel::onKey
)
}
Spacer(Modifier.height(16.dp))
// Action buttons
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 = { viewModel.validate(expectedPassword, onPasswordCorrect) },
modifier = Modifier.weight(1f).height(56.dp),
enabled = uiState.isConfirmEnabled,
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) {
Row(
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
)
)
}
}
}
@P3Preview
@P2Preview
@Composable
fun PreviewInputPassword() {
InputPassword(passwordType = PasswordType.SETTING)
}

View File

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

View File

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

View File

@ -13,7 +13,7 @@ annotation class P2Preview
@Preview( @Preview(
name = "P3", name = "P3",
device = "spec:width=720px,height=1600px,dpi=270", device = "spec:width=427dp,height=949dp,dpi=270",
showBackground = true, showBackground = true,
showSystemUi = true showSystemUi = true
) )

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M10.771,8.518c-1.144,0.215 -2.83,2.171 -2.086,2.915l4.573,4.571 -4.573,4.571c-0.915,0.915 1.829,3.656 2.744,2.742l4.573,-4.571 4.573,4.571c0.915,0.915 3.658,-1.829 2.744,-2.742l-4.573,-4.571 4.573,-4.571c0.915,-0.915 -1.829,-3.656 -2.744,-2.742l-4.573,4.571 -4.573,-4.571c-0.173,-0.171 -0.394,-0.223 -0.657,-0.173v0zM16,1c-8.285,0 -15,6.716 -15,15s6.715,15 15,15 15,-6.716 15,-15 -6.715,-15 -15,-15zM16,4.75c6.213,0 11.25,5.037 11.25,11.25s-5.037,11.25 -11.25,11.25 -11.25,-5.037 -11.25,-11.25c0.001,-6.213 5.037,-11.25 11.25,-11.25z"
android:fillColor="#000000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M0,512h232.7V279.3H0V512zM0,232.7h232.7V0H0V232.7zM279.3,512H512V279.3H279.3V512zM279.3,0v232.7H512V0H279.3z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,25 +1,9 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="11dp" android:width="24dp"
android:height="14dp" android:height="24dp"
android:viewportWidth="11" android:viewportWidth="960"
android:viewportHeight="14"> android:viewportHeight="960">
<path <path
android:pathData="M1.306,13.716C0.947,13.716 0.64,13.588 0.384,13.333C0.128,13.077 0,12.77 0,12.41V5.878C0,5.519 0.128,5.212 0.384,4.956C0.64,4.701 0.948,4.573 1.306,4.572H1.959V3.266C1.959,2.362 2.278,1.592 2.915,0.956C3.552,0.319 4.322,0 5.225,0C6.128,-0 6.899,0.318 7.536,0.956C8.174,1.593 8.492,2.363 8.491,3.266V4.572H9.144C9.503,4.572 9.811,4.7 10.067,4.956C10.323,5.212 10.451,5.52 10.45,5.878V12.41C10.45,12.769 10.323,13.077 10.067,13.333C9.811,13.589 9.504,13.717 9.144,13.716H1.306ZM1.306,12.41H9.144V5.878H1.306V12.41ZM6.148,10.066C6.404,9.811 6.532,9.504 6.532,9.144C6.532,8.784 6.404,8.477 6.148,8.222C5.893,7.967 5.585,7.839 5.225,7.838C4.866,7.837 4.558,7.965 4.303,8.222C4.048,8.479 3.92,8.786 3.919,9.144C3.918,9.502 4.046,9.81 4.303,10.067C4.56,10.324 4.867,10.452 5.225,10.45C5.583,10.449 5.891,10.32 6.148,10.066ZM3.266,4.572H7.185V3.266C7.185,2.721 6.994,2.259 6.613,1.878C6.232,1.497 5.77,1.306 5.225,1.306C4.681,1.306 4.218,1.497 3.837,1.878C3.456,2.259 3.266,2.721 3.266,3.266V4.572Z" 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="#6F0D1E" android:fillColor="#e3e3e3"/>
android:fillAlpha="0.8"/>
</vector> </vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M320,520v-287L217,336l-57,-56 200,-200 200,200 -57,56 -103,-103v287h-80ZM600,880 L400,680l57,-56 103,103v-287h80v287l103,-103 57,56L600,880Z"
android:fillColor="#e3e3e3"/>
</vector>

View File

@ -1,24 +1,12 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp" android:width="800dp"
android:height="14dp" android:height="800dp"
android:viewportWidth="12" android:viewportWidth="24"
android:viewportHeight="14"> android:viewportHeight="24">
<path <path
android:pathData="M11.212,7.908C11.822,8.309 11.794,9.239 11.127,9.591L6.436,12.071C6.247,12.17 6.038,12.223 5.825,12.223C5.612,12.223 5.402,12.17 5.214,12.071L0.522,9.591C-0.144,9.238 -0.173,8.309 0.438,7.908L0.479,7.934L5.214,10.438C5.402,10.537 5.612,10.589 5.825,10.589C6.038,10.589 6.247,10.537 6.436,10.438L11.127,7.958C11.156,7.942 11.184,7.926 11.212,7.908ZM11.212,5.295C11.346,5.384 11.457,5.505 11.534,5.648C11.61,5.79 11.65,5.95 11.65,6.112C11.65,6.273 11.61,6.433 11.534,6.575C11.457,6.718 11.346,6.839 11.212,6.929L11.127,6.978L6.436,9.458C6.265,9.548 6.077,9.599 5.884,9.608C5.691,9.617 5.498,9.582 5.32,9.508L5.215,9.458L0.522,6.978C-0.144,6.625 -0.173,5.696 0.438,5.295L0.479,5.321L5.214,7.824C5.385,7.915 5.573,7.966 5.766,7.975C5.959,7.983 6.152,7.949 6.33,7.875L6.436,7.824L11.127,5.344C11.156,5.329 11.184,5.312 11.212,5.295ZM6.436,0.152L11.127,2.632C11.824,2.999 11.824,3.997 11.127,4.364L6.436,6.845C6.247,6.945 6.038,6.997 5.825,6.997C5.612,6.997 5.402,6.945 5.214,6.845L0.522,4.364C-0.174,3.996 -0.174,2.999 0.522,2.632L5.214,0.152C5.402,0.052 5.612,0 5.825,0C6.038,0 6.247,0.052 6.436,0.152Z" 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:fillColor="#6F0D1E"/> android:strokeWidth="1"
android:fillColor="#09244B"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector> </vector>