system maintainment alert

This commit is contained in:
moon 2026-09-29 16:20:54 +06:30
parent 0234c05e63
commit 1f3beeb165
5 changed files with 370 additions and 125 deletions

View File

@ -71,7 +71,7 @@ fun buildMenuItems(
modifier = Modifier.height(48.dp)
)
}
) { }
) { onNavigateAction("MMQR") }
)
}
add(

View File

@ -5,10 +5,21 @@ import android.os.Build
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mob.utsmyanmar.ui.theme.Color as AppColor
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -74,11 +85,14 @@ fun AppNavGraph(
val activity = LocalContext.current as ComponentActivity
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.destination?.route
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel(activity)
val isSyncBlockedGlobal by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle()
LaunchedEffect(autoSettlementRequested, currentRoute) {
LaunchedEffect(autoSettlementRequested, currentRoute, isSyncBlockedGlobal) {
if (!autoSettlementRequested ||
currentRoute == null ||
currentRoute == Routes.TmsSetup.route
currentRoute == Routes.TmsSetup.route ||
isSyncBlockedGlobal
) return@LaunchedEffect
if (currentRoute == Routes.Settlement.route) {
return@LaunchedEffect
@ -98,12 +112,11 @@ fun AppNavGraph(
startDestination = Routes.TmsSetup.route
) {
composable(Routes.TmsSetup.route) {
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel()
TmsSetupRoute(
viewModel = tmsSetupViewModel,
onNavigateDashboard = {
navController.navigate(
if (autoSettlementRequested) Routes.Settlement.route else Routes.Dashboard.route
if (autoSettlementRequested && !isSyncBlockedGlobal) Routes.Settlement.route else Routes.Dashboard.route
) {
popUpTo(Routes.TmsSetup.route) {
inclusive = true
@ -150,6 +163,9 @@ fun AppNavGraph(
}
val tmsSetupUiState by tmsSetupViewModel.uiState.collectAsStateWithLifecycle()
val isSyncBlocked by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle()
val syncBlockedMessage by tmsSetupViewModel.syncBlockedMessage.collectAsStateWithLifecycle()
var showBlockedSaleAlert by remember { mutableStateOf(false) }
LaunchedEffect(tmsSetupViewModel) {
tmsSetupViewModel.onSyncCompleted.collect {
@ -201,96 +217,109 @@ fun AppNavGraph(
}
},
onNavigateAmount = { action ->
when (action) {
"Pre-Auth",
"Cash Adv",
"Sale" -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
when (action) {
"Pre-Auth",
"Cash Adv",
"Sale" -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
}
"Pre-Auth",
"Cash Adv" -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
}
},
onNavigateAction = { action ->
when (action) {
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
when (action) {
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
"Pre-Auth",
"Cash Adv" -> {
sharedViewModel.transactionsType.value = when (action) {
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
else -> TransactionsType.CASH_OUT
"Pre-Auth",
"Cash Adv" -> {
sharedViewModel.transactionsType.value = when (action) {
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
else -> TransactionsType.CASH_OUT
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
"MMQR" -> {
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
}
},
onNavigateSignOn = {
navController.navigate(Routes.SignOn.route) {
launchSingleTop = true
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
navController.navigate(Routes.SignOn.route) {
launchSingleTop = true
}
}
},
onNavigateSettlement = {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Settlement.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Settlement.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
},
onNavigateVersion = {
@ -317,61 +346,137 @@ fun AppNavGraph(
navController.navigate(Routes.History.route)
}
)
if (showBlockedSaleAlert) {
AlertDialog(
onDismissRequest = { showBlockedSaleAlert = false },
title = {
Text(
text = "Maintenance Alert",
color = AppColor.LegacyRed,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleMedium
)
},
text = {
Text(
text = syncBlockedMessage.ifBlank {
"Terminal sync is blocked due to an active maintenance window."
},
color = AppColor.Black,
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(onClick = { showBlockedSaleAlert = false }) {
Text(
text = "OK",
color = AppColor.LegacyRed,
fontWeight = FontWeight.Bold
)
}
},
containerColor = AppColor.White,
shape = RoundedCornerShape(16.dp)
)
}
}
composable(Routes.SeeMore.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val isSyncBlocked by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle()
val syncBlockedMessage by tmsSetupViewModel.syncBlockedMessage.collectAsStateWithLifecycle()
var showBlockedSaleAlert by remember { mutableStateOf(false) }
SeeMoreScreen(
onBack = { navController.popBackStack() },
onNavigateAmount = { action ->
when (action) {
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
when (action) {
"Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
"Pre-Auth",
"Cash Adv" -> {
sharedViewModel.transactionsType.value = when (action) {
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
else -> TransactionsType.CASH_OUT
"Pre-Auth",
"Cash Adv" -> {
sharedViewModel.transactionsType.value = when (action) {
"Pre-Auth" -> TransactionsType.PRE_AUTH_SALE
else -> TransactionsType.CASH_OUT
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
"Pre-Auth Complete Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.PRE_AUTH_COMPLETE_VOID
navController.navigate(
Routes.Password.createRoute(
destination = Routes.VoidTrace.route,
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
else -> {
navController.navigate(
Routes.Password.createRoute(
destination = Routes.Amount.createRoute(action),
passwordType = PasswordType.SYSTEM
)
) {
launchSingleTop = true
}
}
}
}
}
)
if (showBlockedSaleAlert) {
AlertDialog(
onDismissRequest = { showBlockedSaleAlert = false },
title = {
Text(
text = "Maintenance Alert",
color = AppColor.LegacyRed,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleMedium
)
},
text = {
Text(
text = syncBlockedMessage.ifBlank {
"Terminal sync is blocked due to an active maintenance window."
},
color = AppColor.Black,
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(onClick = { showBlockedSaleAlert = false }) {
Text(
text = "OK",
color = AppColor.LegacyRed,
fontWeight = FontWeight.Bold
)
}
},
containerColor = AppColor.White,
shape = RoundedCornerShape(16.dp)
)
}
}
composable(Routes.Version.route){

View File

@ -32,6 +32,9 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.json.JSONObject
import sunmi.sunmiui.utils.LogUtil
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@ -40,7 +43,9 @@ data class TmsSetupUiState(
val statusText: String = "Initializing...",
val isError: Boolean = false,
val errorMessage: String = "",
val isTerminalDisabled: Boolean = false
val isTerminalDisabled: Boolean = false,
val isSyncBlocked: Boolean = false,
val syncBlockedMessage: String = ""
)
@HiltViewModel
@ -49,8 +54,30 @@ class TmsSetupViewModel @Inject constructor(
private val emvParamOperation: EmvParamOperation,
@ApplicationContext private val context: Context
) : ViewModel() {
companion object {
const val PREF_KEY_SYNC_BLOCKED = "is_sync_blocked"
const val PREF_KEY_SYNC_BLOCKED_MSG = "sync_blocked_message"
}
private val TAG = TmsSetupViewModel::class.simpleName
private val _uiState = MutableStateFlow(TmsSetupUiState())
private val prefs = context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE)
private val _isSyncBlocked = MutableStateFlow(
prefs.getBoolean(PREF_KEY_SYNC_BLOCKED, false)
)
val isSyncBlocked: StateFlow<Boolean> = _isSyncBlocked.asStateFlow()
private val _syncBlockedMessage = MutableStateFlow(
prefs.getString(PREF_KEY_SYNC_BLOCKED_MSG, "").orEmpty()
)
val syncBlockedMessage: StateFlow<String> = _syncBlockedMessage.asStateFlow()
private val _uiState = MutableStateFlow(
TmsSetupUiState(
isSyncBlocked = prefs.getBoolean(PREF_KEY_SYNC_BLOCKED, false),
syncBlockedMessage = prefs.getString(PREF_KEY_SYNC_BLOCKED_MSG, "").orEmpty()
)
)
private val _dashboardUiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<TmsSetupUiState> = _uiState.asStateFlow()
@ -100,7 +127,16 @@ class TmsSetupViewModel @Inject constructor(
.doFinally { requestInProgress = false }
.subscribe(
{ response ->
prefs.edit {
putBoolean(PREF_KEY_SYNC_BLOCKED, false)
remove(PREF_KEY_SYNC_BLOCKED_MSG)
}
_isSyncBlocked.value = false
_syncBlockedMessage.value = ""
_uiState.update { it.copy(
isSyncBlocked = false,
syncBlockedMessage = "",
statusText = "Applying configuration...")
}
@ -117,24 +153,59 @@ class TmsSetupViewModel @Inject constructor(
onConfigApplied(onSuccess)
},
{ error ->
val errorMessage = if (error is retrofit2.HttpException) {
var isSyncBlocked = false
var errorMessage = error.message.toString()
if (error is retrofit2.HttpException) {
val httpCode = error.code()
try {
val body = error.response()?.errorBody()?.string()
if (!body.isNullOrBlank()) {
JSONObject(body).optString("message", "Unknown server error")
val json = JSONObject(body)
val code = json.optString("code")
val statusCode = json.optInt("statusCode", httpCode)
val msg = json.optString("message", "")
if (msg.isNotBlank()) {
errorMessage = msg
}
if (httpCode == 423 || statusCode == 423 ||
code.equals("TERMINAL_SYNC_BLOCKED", ignoreCase = true) ||
(msg.contains("blocked", ignoreCase = true) && msg.contains("maintenance", ignoreCase = true))
) {
isSyncBlocked = true
errorMessage = buildAlertDetailsMessage(json, msg.ifBlank { errorMessage })
}
} else {
"Empty error response (${error.code()})"
if (httpCode == 423) {
isSyncBlocked = true
}
errorMessage = "Empty error response ($httpCode)"
}
} catch (e: Exception) {
"Server error: ${error.code()}"
if (httpCode == 423) {
isSyncBlocked = true
}
errorMessage = "Server error: $httpCode"
}
} else {
error.message.toString()
}
if (isSyncBlocked) {
LogUtil.i(TAG, "Terminal sync is blocked due to maintenance window ($errorMessage).")
prefs.edit {
putBoolean(PREF_KEY_SYNC_BLOCKED, true)
putString(PREF_KEY_SYNC_BLOCKED_MSG, errorMessage)
}
_isSyncBlocked.value = true
_syncBlockedMessage.value = errorMessage
}
_uiState.update {
it.copy(
isError = true,
isLoading = false,
isSyncBlocked = isSyncBlocked,
syncBlockedMessage = if (isSyncBlocked) errorMessage else "",
statusText = "Download failed!",
errorMessage = errorMessage
)
@ -181,12 +252,20 @@ class TmsSetupViewModel @Inject constructor(
val validity = TMSUtil.getInstance().checkParams()
if (validity.status == ValidityStatus.SUCCESS) {
context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE)
.edit {
putLong("last_sync_time", System.currentTimeMillis())
}
prefs.edit {
putLong("last_sync_time", System.currentTimeMillis())
putBoolean(PREF_KEY_SYNC_BLOCKED, false)
remove(PREF_KEY_SYNC_BLOCKED_MSG)
}
_isSyncBlocked.value = false
_syncBlockedMessage.value = ""
onSuccess()
_uiState.update { it.copy(isLoading = false, statusText = "Ready.") }
_uiState.update { it.copy(
isLoading = false,
isSyncBlocked = false,
syncBlockedMessage = "",
statusText = "Ready."
) }
viewModelScope.launch {
_onSyncCompleted.emit(Unit)
_navigateToDashboard.emit(Unit)
@ -222,6 +301,65 @@ class TmsSetupViewModel @Inject constructor(
}
}
private fun buildAlertDetailsMessage(json: JSONObject, defaultMessage: String): String {
val builder = StringBuilder()
val msg = json.optString("message", defaultMessage).trim()
if (msg.isNotEmpty()) {
builder.append(msg)
}
val alertsArray = json.optJSONArray("alerts")
if (alertsArray != null && alertsArray.length() > 0) {
for (i in 0 until alertsArray.length()) {
val alert = alertsArray.optJSONObject(i) ?: continue
if (builder.isNotEmpty()) {
builder.append("\n\n")
}
val title = alert.optString("title").trim()
val desc = alert.optString("description").trim()
val type = alert.optString("type").trim()
val startAt = alert.optString("startAt").trim()
val endAt = alert.optString("endAt").trim()
val lines = mutableListOf<String>()
if (title.isNotEmpty()) lines.add("Title: $title")
if (desc.isNotEmpty()) lines.add("Description: $desc")
if (type.isNotEmpty()) lines.add("Type: $type")
if (startAt.isNotEmpty()) lines.add("Start At: ${formatDateTime(startAt)}")
if (endAt.isNotEmpty()) lines.add("End At: ${formatDateTime(endAt)}")
builder.append(lines.joinToString("\n"))
}
}
return builder.toString().ifBlank { defaultMessage }
}
private fun formatDateTime(isoString: String): String {
if (isoString.isBlank()) return ""
val patterns = arrayOf(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ss"
)
for (pattern in patterns) {
try {
val parser = SimpleDateFormat(pattern, Locale.US).apply {
if (pattern.endsWith("'Z'")) {
timeZone = TimeZone.getTimeZone("UTC")
}
}
val date = parser.parse(isoString)
if (date != null) {
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return formatter.format(date)
}
} catch (_: Exception) {}
}
return isoString
}
override fun onCleared() {
super.onCleared()
disposables.clear()

View File

@ -3,6 +3,7 @@
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">posuat.myanmarorientalbank.com:5033</domain>
<domain includeSubdomains="true">192.168.10.104</domain>
</domain-config>
</network-security-config>

View File

@ -38,9 +38,10 @@ public class SystemParamsSettings implements Serializable {
private boolean checkExpSwitch = false;
private String tmsAddress = "https://posmgmt.myanmarorientalbank.com/api/v1";
// private String tmsAddress = "https://posmgmt.myanmarorientalbank.com/api/v1";
// private String tmsAddress = "https://sirius-nest.utsmyanmar.com/api/v1";
// private String tmsAddress = "http://128.199.170.203";
private String tmsAddress = "http://192.168.10.104:3333/api/v1";
private String terminalCapability = "E0E8C8";
private String masterTerminalCapability = "E008C8";