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) modifier = Modifier.height(48.dp)
) )
} }
) { } ) { onNavigateAction("MMQR") }
) )
} }
add( add(

View File

@ -5,10 +5,21 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -74,11 +85,14 @@ fun AppNavGraph(
val activity = LocalContext.current as ComponentActivity val activity = LocalContext.current as ComponentActivity
val currentBackStackEntry by navController.currentBackStackEntryAsState() val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.destination?.route 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 || if (!autoSettlementRequested ||
currentRoute == null || currentRoute == null ||
currentRoute == Routes.TmsSetup.route currentRoute == Routes.TmsSetup.route ||
isSyncBlockedGlobal
) return@LaunchedEffect ) return@LaunchedEffect
if (currentRoute == Routes.Settlement.route) { if (currentRoute == Routes.Settlement.route) {
return@LaunchedEffect return@LaunchedEffect
@ -98,12 +112,11 @@ fun AppNavGraph(
startDestination = Routes.TmsSetup.route startDestination = Routes.TmsSetup.route
) { ) {
composable(Routes.TmsSetup.route) { composable(Routes.TmsSetup.route) {
val tmsSetupViewModel: TmsSetupViewModel = hiltViewModel()
TmsSetupRoute( TmsSetupRoute(
viewModel = tmsSetupViewModel, viewModel = tmsSetupViewModel,
onNavigateDashboard = { onNavigateDashboard = {
navController.navigate( 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) { popUpTo(Routes.TmsSetup.route) {
inclusive = true inclusive = true
@ -150,6 +163,9 @@ fun AppNavGraph(
} }
val tmsSetupUiState by tmsSetupViewModel.uiState.collectAsStateWithLifecycle() 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) { LaunchedEffect(tmsSetupViewModel) {
tmsSetupViewModel.onSyncCompleted.collect { tmsSetupViewModel.onSyncCompleted.collect {
@ -201,6 +217,9 @@ fun AppNavGraph(
} }
}, },
onNavigateAmount = { action -> onNavigateAmount = { action ->
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
when (action) { when (action) {
"Pre-Auth", "Pre-Auth",
"Cash Adv", "Cash Adv",
@ -209,12 +228,6 @@ fun AppNavGraph(
launchSingleTop = true launchSingleTop = true
} }
} }
"Pre-Auth",
"Cash Adv" -> {
navController.navigate(Routes.Amount.createRoute(action)) {
launchSingleTop = true
}
}
else -> { else -> {
navController.navigate( navController.navigate(
@ -227,8 +240,12 @@ fun AppNavGraph(
} }
} }
} }
}
}, },
onNavigateAction = { action -> onNavigateAction = { action ->
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
when (action) { when (action) {
"Void" -> { "Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID sharedViewModel.transactionsType.value = TransactionsType.VOID
@ -265,6 +282,9 @@ fun AppNavGraph(
} }
} }
"MMQR" -> {
}
else -> { else -> {
navController.navigate( navController.navigate(
Routes.Password.createRoute( Routes.Password.createRoute(
@ -276,14 +296,22 @@ fun AppNavGraph(
} }
} }
} }
}
}, },
onNavigateSignOn = { onNavigateSignOn = {
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
navController.navigate(Routes.SignOn.route) { navController.navigate(Routes.SignOn.route) {
launchSingleTop = true launchSingleTop = true
} }
}
}, },
onNavigateSettlement = { onNavigateSettlement = {
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
navController.navigate( navController.navigate(
Routes.Password.createRoute( Routes.Password.createRoute(
destination = Routes.Settlement.route, destination = Routes.Settlement.route,
@ -292,6 +320,7 @@ fun AppNavGraph(
) { ) {
launchSingleTop = true launchSingleTop = true
} }
}
}, },
onNavigateVersion = { onNavigateVersion = {
navController.navigate( navController.navigate(
@ -317,13 +346,54 @@ fun AppNavGraph(
navController.navigate(Routes.History.route) 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) { composable(Routes.SeeMore.route) {
val sharedViewModel: SharedViewModel = hiltViewModel(activity) val sharedViewModel: SharedViewModel = hiltViewModel(activity)
val isSyncBlocked by tmsSetupViewModel.isSyncBlocked.collectAsStateWithLifecycle()
val syncBlockedMessage by tmsSetupViewModel.syncBlockedMessage.collectAsStateWithLifecycle()
var showBlockedSaleAlert by remember { mutableStateOf(false) }
SeeMoreScreen( SeeMoreScreen(
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onNavigateAmount = { action -> onNavigateAmount = { action ->
if (isSyncBlocked) {
showBlockedSaleAlert = true
} else {
when (action) { when (action) {
"Void" -> { "Void" -> {
sharedViewModel.transactionsType.value = TransactionsType.VOID sharedViewModel.transactionsType.value = TransactionsType.VOID
@ -371,7 +441,42 @@ fun AppNavGraph(
} }
} }
} }
}
) )
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){ composable(Routes.Version.route){

View File

@ -32,6 +32,9 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import sunmi.sunmiui.utils.LogUtil import sunmi.sunmiui.utils.LogUtil
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -40,7 +43,9 @@ data class TmsSetupUiState(
val statusText: String = "Initializing...", val statusText: String = "Initializing...",
val isError: Boolean = false, val isError: Boolean = false,
val errorMessage: String = "", val errorMessage: String = "",
val isTerminalDisabled: Boolean = false val isTerminalDisabled: Boolean = false,
val isSyncBlocked: Boolean = false,
val syncBlockedMessage: String = ""
) )
@HiltViewModel @HiltViewModel
@ -49,8 +54,30 @@ class TmsSetupViewModel @Inject constructor(
private val emvParamOperation: EmvParamOperation, private val emvParamOperation: EmvParamOperation,
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : ViewModel() { ) : 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 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()) private val _dashboardUiState = MutableStateFlow(DashboardUiState())
val uiState: StateFlow<TmsSetupUiState> = _uiState.asStateFlow() val uiState: StateFlow<TmsSetupUiState> = _uiState.asStateFlow()
@ -100,7 +127,16 @@ class TmsSetupViewModel @Inject constructor(
.doFinally { requestInProgress = false } .doFinally { requestInProgress = false }
.subscribe( .subscribe(
{ response -> { response ->
prefs.edit {
putBoolean(PREF_KEY_SYNC_BLOCKED, false)
remove(PREF_KEY_SYNC_BLOCKED_MSG)
}
_isSyncBlocked.value = false
_syncBlockedMessage.value = ""
_uiState.update { it.copy( _uiState.update { it.copy(
isSyncBlocked = false,
syncBlockedMessage = "",
statusText = "Applying configuration...") statusText = "Applying configuration...")
} }
@ -117,24 +153,59 @@ class TmsSetupViewModel @Inject constructor(
onConfigApplied(onSuccess) onConfigApplied(onSuccess)
}, },
{ error -> { 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 { try {
val body = error.response()?.errorBody()?.string() val body = error.response()?.errorBody()?.string()
if (!body.isNullOrBlank()) { 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 { } else {
"Empty error response (${error.code()})" if (httpCode == 423) {
isSyncBlocked = true
}
errorMessage = "Empty error response ($httpCode)"
} }
} catch (e: Exception) { } catch (e: Exception) {
"Server error: ${error.code()}" if (httpCode == 423) {
isSyncBlocked = true
} }
} else { errorMessage = "Server error: $httpCode"
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 { _uiState.update {
it.copy( it.copy(
isError = true, isError = true,
isLoading = false, isLoading = false,
isSyncBlocked = isSyncBlocked,
syncBlockedMessage = if (isSyncBlocked) errorMessage else "",
statusText = "Download failed!", statusText = "Download failed!",
errorMessage = errorMessage errorMessage = errorMessage
) )
@ -181,12 +252,20 @@ class TmsSetupViewModel @Inject constructor(
val validity = TMSUtil.getInstance().checkParams() val validity = TMSUtil.getInstance().checkParams()
if (validity.status == ValidityStatus.SUCCESS) { if (validity.status == ValidityStatus.SUCCESS) {
context.getSharedPreferences("mobpos_prefs", Context.MODE_PRIVATE) prefs.edit {
.edit {
putLong("last_sync_time", System.currentTimeMillis()) putLong("last_sync_time", System.currentTimeMillis())
putBoolean(PREF_KEY_SYNC_BLOCKED, false)
remove(PREF_KEY_SYNC_BLOCKED_MSG)
} }
_isSyncBlocked.value = false
_syncBlockedMessage.value = ""
onSuccess() onSuccess()
_uiState.update { it.copy(isLoading = false, statusText = "Ready.") } _uiState.update { it.copy(
isLoading = false,
isSyncBlocked = false,
syncBlockedMessage = "",
statusText = "Ready."
) }
viewModelScope.launch { viewModelScope.launch {
_onSyncCompleted.emit(Unit) _onSyncCompleted.emit(Unit)
_navigateToDashboard.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() { override fun onCleared() {
super.onCleared() super.onCleared()
disposables.clear() disposables.clear()

View File

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

View File

@ -38,9 +38,10 @@ public class SystemParamsSettings implements Serializable {
private boolean checkExpSwitch = false; 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 = "https://sirius-nest.utsmyanmar.com/api/v1";
// private String tmsAddress = "http://128.199.170.203"; // 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 terminalCapability = "E0E8C8";
private String masterTerminalCapability = "E008C8"; private String masterTerminalCapability = "E008C8";